Coverage for flask/src/app.py: 61%

79 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-08-23 19:38 +0900

1#!/usr/bin/env python3 

2""" 

3電動シャッターの開閉を自動化するアプリのサーバーです 

4 

5Usage: 

6 app.py [-c CONFIG] [-p PORT] [-d] [-D] 

7 

8Options: 

9 -c CONFIG : CONFIG を設定ファイルとして読み込んで実行します。[default: config.yaml] 

10 -p PORT : WEB サーバのポートを指定します。[default: 5000] 

11 -d : ダミーモードで実行します。CI テストで利用することを想定しています。 

12 -D : デバッグモードで動作します。 

13""" 

14 

15import atexit 

16import contextlib 

17import logging 

18import os 

19import signal 

20import sys 

21 

22import flask_cors 

23import my_lib.proc_util 

24import my_lib.webapp.base 

25import my_lib.webapp.event 

26import my_lib.webapp.log 

27import my_lib.webapp.util 

28 

29import flask 

30 

31SCHEMA_CONFIG = "config.schema" 

32 

33 

34def term(): 

35 import rasp_shutter.control.scheduler 

36 

37 rasp_shutter.control.scheduler.term() 

38 

39 # スケジュールワーカーの終了を待機(最大10秒) 

40 try: 

41 schedule_worker = rasp_shutter.control.webapi.schedule.get_worker_thread() 

42 if schedule_worker: 

43 logging.info("Waiting for schedule worker to finish...") 

44 schedule_worker.join(timeout=10) 

45 if schedule_worker.is_alive(): 

46 logging.warning("Schedule worker did not finish within timeout") 

47 except Exception: 

48 logging.exception("Error waiting for schedule worker") 

49 

50 my_lib.webapp.log.term() 

51 

52 # 子プロセスを終了 

53 my_lib.proc_util.kill_child() 

54 

55 # プロセス終了 

56 logging.info("Graceful shutdown completed") 

57 sys.exit(0) 

58 

59 

60def sig_handler(num, frame): # noqa: ARG001 

61 logging.warning("receive signal %d", num) 

62 

63 if num in (signal.SIGTERM, signal.SIGINT): 

64 # Flask reloader の子プロセスも含めて終了する 

65 try: 

66 # 現在のプロセスがプロセスグループリーダーの場合、全体を終了 

67 current_pid = os.getpid() 

68 pgid = os.getpgid(current_pid) 

69 if current_pid == pgid: 

70 logging.info("Terminating process group %d", pgid) 

71 os.killpg(pgid, signal.SIGTERM) 

72 except (ProcessLookupError, PermissionError): 

73 # プロセスグループ操作に失敗した場合は通常の終了処理 

74 pass 

75 

76 term() 

77 

78 

79def create_app(config, dummy_mode=False): 

80 # NOTE: オプションでダミーモードが指定された場合、環境変数もそれに揃えておく 

81 if dummy_mode: 

82 os.environ["DUMMY_MODE"] = "true" 

83 else: # pragma: no cover 

84 os.environ["DUMMY_MODE"] = "false" 

85 

86 # NOTE: テストのため、環境変数 DUMMY_MODE をセットしてからロードしたいのでこの位置 

87 import my_lib.webapp.config 

88 

89 my_lib.webapp.config.URL_PREFIX = "/rasp-shutter" 

90 my_lib.webapp.config.init(config) 

91 

92 import rasp_shutter.control.webapi.control 

93 import rasp_shutter.control.webapi.schedule 

94 import rasp_shutter.control.webapi.sensor 

95 import rasp_shutter.metrics.webapi.page 

96 

97 app = flask.Flask("rasp-shutter") 

98 

99 # NOTE: アクセスログは無効にする 

100 logging.getLogger("werkzeug").setLevel(logging.ERROR) 

101 

102 if os.environ.get("WERKZEUG_RUN_MAIN") == "true": 

103 if dummy_mode: 

104 logging.warning("Set dummy mode") 

105 else: # pragma: no cover 

106 pass 

107 

108 rasp_shutter.control.webapi.control.init() 

109 rasp_shutter.control.webapi.schedule.init(config) 

110 my_lib.webapp.log.init(config) 

111 

112 def notify_terminate(): # pragma: no cover 

113 my_lib.webapp.log.info("🏃 アプリを再起動します。") 

114 my_lib.webapp.log.term() 

115 

116 atexit.register(notify_terminate) 

117 else: # pragma: no cover 

118 pass 

119 

120 flask_cors.CORS(app) 

121 

122 app.config["CONFIG"] = config 

123 app.config["DUMMY_MODE"] = dummy_mode 

124 

125 app.json.compat = True 

126 

127 app.register_blueprint( 

128 rasp_shutter.control.webapi.control.blueprint, url_prefix=my_lib.webapp.config.URL_PREFIX 

129 ) 

130 app.register_blueprint( 

131 rasp_shutter.control.webapi.schedule.blueprint, url_prefix=my_lib.webapp.config.URL_PREFIX 

132 ) 

133 app.register_blueprint( 

134 rasp_shutter.control.webapi.sensor.blueprint, url_prefix=my_lib.webapp.config.URL_PREFIX 

135 ) 

136 app.register_blueprint( 

137 rasp_shutter.metrics.webapi.page.blueprint, url_prefix=my_lib.webapp.config.URL_PREFIX 

138 ) 

139 

140 app.register_blueprint(my_lib.webapp.base.blueprint_default) 

141 app.register_blueprint(my_lib.webapp.base.blueprint, url_prefix=my_lib.webapp.config.URL_PREFIX) 

142 app.register_blueprint(my_lib.webapp.event.blueprint, url_prefix=my_lib.webapp.config.URL_PREFIX) 

143 app.register_blueprint(my_lib.webapp.log.blueprint, url_prefix=my_lib.webapp.config.URL_PREFIX) 

144 app.register_blueprint(my_lib.webapp.util.blueprint, url_prefix=my_lib.webapp.config.URL_PREFIX) 

145 

146 if os.environ.get("TEST") == "true": 146 ↛ 153line 146 didn't jump to line 153 because the condition on line 146 was always true

147 import rasp_shutter.control.webapi.test.time 

148 

149 app.register_blueprint( 

150 rasp_shutter.control.webapi.test.time.blueprint, url_prefix=my_lib.webapp.config.URL_PREFIX 

151 ) 

152 

153 my_lib.webapp.config.show_handler_list(app) 

154 

155 return app 

156 

157 

158if __name__ == "__main__": 

159 import pathlib 

160 

161 import docopt 

162 import my_lib.config 

163 import my_lib.logger 

164 

165 args = docopt.docopt(__doc__) 

166 

167 config_file = args["-c"] 

168 port = args["-p"] 

169 dummy_mode = args["-d"] 

170 debug_mode = args["-D"] 

171 

172 my_lib.logger.init("hems.rasp-shutter", level=logging.DEBUG if debug_mode else logging.INFO) 

173 

174 config = my_lib.config.load(config_file, pathlib.Path(SCHEMA_CONFIG)) 

175 

176 app = create_app(config, dummy_mode) 

177 

178 # プロセスグループリーダーとして実行(リローダープロセスの適切な管理のため) 

179 with contextlib.suppress(PermissionError): 

180 os.setpgrp() 

181 

182 # 異常終了時のクリーンアップ処理を登録 

183 def cleanup_on_exit(): 

184 try: 

185 current_pid = os.getpid() 

186 pgid = os.getpgid(current_pid) 

187 if current_pid == pgid: 

188 # プロセスグループ内の他のプロセスを終了 

189 os.killpg(pgid, signal.SIGTERM) 

190 except (ProcessLookupError, PermissionError): 

191 pass 

192 

193 atexit.register(cleanup_on_exit) 

194 

195 # Flaskアプリケーションを実行 

196 try: 

197 # NOTE: スクリプトの自動リロード停止したい場合は use_reloader=False にする 

198 app.run(host="0.0.0.0", port=port, threaded=True, use_reloader=True) # noqa: S104 

199 except KeyboardInterrupt: 

200 logging.info("Received KeyboardInterrupt, shutting down...") 

201 sig_handler(signal.SIGINT, None)