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
« prev ^ index » next coverage.py v7.9.1, created at 2025-08-23 19:38 +0900
1#!/usr/bin/env python3
2"""
3電動シャッターの開閉を自動化するアプリのサーバーです
5Usage:
6 app.py [-c CONFIG] [-p PORT] [-d] [-D]
8Options:
9 -c CONFIG : CONFIG を設定ファイルとして読み込んで実行します。[default: config.yaml]
10 -p PORT : WEB サーバのポートを指定します。[default: 5000]
11 -d : ダミーモードで実行します。CI テストで利用することを想定しています。
12 -D : デバッグモードで動作します。
13"""
15import atexit
16import contextlib
17import logging
18import os
19import signal
20import sys
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
29import flask
31SCHEMA_CONFIG = "config.schema"
34def term():
35 import rasp_shutter.control.scheduler
37 rasp_shutter.control.scheduler.term()
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")
50 my_lib.webapp.log.term()
52 # 子プロセスを終了
53 my_lib.proc_util.kill_child()
55 # プロセス終了
56 logging.info("Graceful shutdown completed")
57 sys.exit(0)
60def sig_handler(num, frame): # noqa: ARG001
61 logging.warning("receive signal %d", num)
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
76 term()
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"
86 # NOTE: テストのため、環境変数 DUMMY_MODE をセットしてからロードしたいのでこの位置
87 import my_lib.webapp.config
89 my_lib.webapp.config.URL_PREFIX = "/rasp-shutter"
90 my_lib.webapp.config.init(config)
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
97 app = flask.Flask("rasp-shutter")
99 # NOTE: アクセスログは無効にする
100 logging.getLogger("werkzeug").setLevel(logging.ERROR)
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
108 rasp_shutter.control.webapi.control.init()
109 rasp_shutter.control.webapi.schedule.init(config)
110 my_lib.webapp.log.init(config)
112 def notify_terminate(): # pragma: no cover
113 my_lib.webapp.log.info("🏃 アプリを再起動します。")
114 my_lib.webapp.log.term()
116 atexit.register(notify_terminate)
117 else: # pragma: no cover
118 pass
120 flask_cors.CORS(app)
122 app.config["CONFIG"] = config
123 app.config["DUMMY_MODE"] = dummy_mode
125 app.json.compat = True
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 )
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)
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
149 app.register_blueprint(
150 rasp_shutter.control.webapi.test.time.blueprint, url_prefix=my_lib.webapp.config.URL_PREFIX
151 )
153 my_lib.webapp.config.show_handler_list(app)
155 return app
158if __name__ == "__main__":
159 import pathlib
161 import docopt
162 import my_lib.config
163 import my_lib.logger
165 args = docopt.docopt(__doc__)
167 config_file = args["-c"]
168 port = args["-p"]
169 dummy_mode = args["-d"]
170 debug_mode = args["-D"]
172 my_lib.logger.init("hems.rasp-shutter", level=logging.DEBUG if debug_mode else logging.INFO)
174 config = my_lib.config.load(config_file, pathlib.Path(SCHEMA_CONFIG))
176 app = create_app(config, dummy_mode)
178 # プロセスグループリーダーとして実行(リローダープロセスの適切な管理のため)
179 with contextlib.suppress(PermissionError):
180 os.setpgrp()
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
193 atexit.register(cleanup_on_exit)
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)