Mercurial > libervia-backend
comparison libervia/backend/core/launcher.py @ 4071:4b842c1fb686
refactoring: renamed `sat` package to `libervia.backend`
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 11:49:51 +0200 |
parents | sat/core/launcher.py@7550ae9cfbac |
children | 47401850dec6 |
comparison
equal
deleted
inserted
replaced
4070:d10748475025 | 4071:4b842c1fb686 |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 # Libervia: an XMPP client | |
4 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) | |
5 | |
6 # This program is free software: you can redistribute it and/or modify | |
7 # it under the terms of the GNU Affero General Public License as published by | |
8 # the Free Software Foundation, either version 3 of the License, or | |
9 # (at your option) any later version. | |
10 | |
11 # This program is distributed in the hope that it will be useful, | |
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 # GNU Affero General Public License for more details. | |
15 | |
16 # You should have received a copy of the GNU Affero General Public License | |
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
18 | |
19 """Script launching SàT backend""" | |
20 | |
21 import sys | |
22 import os | |
23 import argparse | |
24 from pathlib import Path | |
25 from configparser import ConfigParser | |
26 from twisted.application import app | |
27 from twisted.python import usage | |
28 from libervia.backend.core.constants import Const as C | |
29 | |
30 | |
31 class LiberviaLogger(app.AppLogger): | |
32 | |
33 def start(self, application): | |
34 # logging is initialised by sat.core.log_config via the Twisted plugin, nothing | |
35 # to do here | |
36 self._initialLog() | |
37 | |
38 def stop(self): | |
39 pass | |
40 | |
41 | |
42 class Launcher: | |
43 APP_NAME=C.APP_NAME | |
44 APP_NAME_FILE=C.APP_NAME_FILE | |
45 | |
46 @property | |
47 def NOT_RUNNING_MSG(self): | |
48 return f"{self.APP_NAME} is *NOT* running" | |
49 | |
50 def cmd_no_subparser(self, args): | |
51 """Command launched by default""" | |
52 args.extra_args = [] | |
53 self.cmd_background(args) | |
54 | |
55 def cmd_background(self, args): | |
56 self.run_twistd(args) | |
57 | |
58 def cmd_foreground(self, args): | |
59 self.run_twistd(args, twistd_opts=['--nodaemon']) | |
60 | |
61 def cmd_debug(self, args): | |
62 self.run_twistd(args, twistd_opts=['--debug']) | |
63 | |
64 def cmd_stop(self, args): | |
65 import signal | |
66 import time | |
67 config = self.get_config() | |
68 pid_file = self.get_pid_file(config) | |
69 if not pid_file.is_file(): | |
70 print(self.NOT_RUNNING_MSG) | |
71 sys.exit(0) | |
72 try: | |
73 pid = int(pid_file.read_text()) | |
74 except Exception as e: | |
75 print(f"Can't read PID file at {pid_file}: {e}") | |
76 # we use the same exit code as DATA_ERROR in jp | |
77 sys.exit(17) | |
78 print(f"Terminating {self.APP_NAME}…") | |
79 os.kill(pid, signal.SIGTERM) | |
80 kill_started = time.time() | |
81 state = "init" | |
82 import errno | |
83 while True: | |
84 try: | |
85 os.kill(pid, 0) | |
86 except OSError as e: | |
87 if e.errno == errno.ESRCH: | |
88 break | |
89 elif e.errno == errno.EPERM: | |
90 print(f"Can't kill {self.APP_NAME}, the process is owned by an other user", file=sys.stderr) | |
91 sys.exit(18) | |
92 else: | |
93 raise e | |
94 time.sleep(0.2) | |
95 now = time.time() | |
96 if state == 'init' and now - kill_started > 5: | |
97 if state == 'init': | |
98 state = 'waiting' | |
99 print(f"Still waiting for {self.APP_NAME} to be terminated…") | |
100 elif state == 'waiting' and now - kill_started > 10: | |
101 state == 'killing' | |
102 print("Waiting for too long, we kill the process") | |
103 os.kill(pid, signal.SIGKILL) | |
104 sys.exit(1) | |
105 | |
106 sys.exit(0) | |
107 | |
108 def cmd_status(self, args): | |
109 config = self.get_config() | |
110 pid_file = self.get_pid_file(config) | |
111 if pid_file.is_file(): | |
112 import errno | |
113 try: | |
114 pid = int(pid_file.read_text()) | |
115 except Exception as e: | |
116 print(f"Can't read PID file at {pid_file}: {e}") | |
117 # we use the same exit code as DATA_ERROR in jp | |
118 sys.exit(17) | |
119 # we check if there is a process | |
120 # inspired by https://stackoverflow.com/a/568285 and https://stackoverflow.com/a/6940314 | |
121 try: | |
122 os.kill(pid, 0) | |
123 except OSError as e: | |
124 if e.errno == errno.ESRCH: | |
125 running = False | |
126 elif e.errno == errno.EPERM: | |
127 print("Process {pid} is run by an other user") | |
128 running = True | |
129 else: | |
130 running = True | |
131 | |
132 if running: | |
133 print(f"{self.APP_NAME} is running (pid: {pid})") | |
134 sys.exit(0) | |
135 else: | |
136 print(f"{self.NOT_RUNNING_MSG}, but a pid file is present (bad exit ?): {pid_file}") | |
137 sys.exit(2) | |
138 else: | |
139 print(self.NOT_RUNNING_MSG) | |
140 sys.exit(1) | |
141 | |
142 def parse_args(self): | |
143 parser = argparse.ArgumentParser(description=f"Launch {self.APP_NAME} backend") | |
144 parser.set_defaults(cmd=self.cmd_no_subparser) | |
145 subparsers = parser.add_subparsers() | |
146 extra_help = f"arguments to pass to {self.APP_NAME} service" | |
147 | |
148 bg_parser = subparsers.add_parser( | |
149 'background', | |
150 aliases=['bg'], | |
151 help=f"run {self.APP_NAME} backend in background (as a daemon)") | |
152 bg_parser.add_argument('extra_args', nargs=argparse.REMAINDER, help=extra_help) | |
153 bg_parser.set_defaults(cmd=self.cmd_background) | |
154 | |
155 fg_parser = subparsers.add_parser( | |
156 'foreground', | |
157 aliases=['fg'], | |
158 help=f"run {self.APP_NAME} backend in foreground") | |
159 fg_parser.add_argument('extra_args', nargs=argparse.REMAINDER, help=extra_help) | |
160 fg_parser.set_defaults(cmd=self.cmd_foreground) | |
161 | |
162 dbg_parser = subparsers.add_parser( | |
163 'debug', | |
164 aliases=['dbg'], | |
165 help=f"run {self.APP_NAME} backend in debug mode") | |
166 dbg_parser.add_argument('extra_args', nargs=argparse.REMAINDER, help=extra_help) | |
167 dbg_parser.set_defaults(cmd=self.cmd_debug) | |
168 | |
169 stop_parser = subparsers.add_parser( | |
170 'stop', | |
171 help=f"stop running {self.APP_NAME} backend") | |
172 stop_parser.set_defaults(cmd=self.cmd_stop) | |
173 | |
174 status_parser = subparsers.add_parser( | |
175 'status', | |
176 help=f"indicate if {self.APP_NAME} backend is running") | |
177 status_parser.set_defaults(cmd=self.cmd_status) | |
178 | |
179 return parser.parse_args() | |
180 | |
181 def get_config(self): | |
182 config = ConfigParser(defaults=C.DEFAULT_CONFIG) | |
183 try: | |
184 config.read(C.CONFIG_FILES) | |
185 except Exception as e: | |
186 print (rf"/!\ Can't read main config! {e}") | |
187 sys.exit(1) | |
188 return config | |
189 | |
190 def get_pid_file(self, config): | |
191 pid_dir = Path(config.get('DEFAULT', 'pid_dir')).expanduser() | |
192 return pid_dir / f"{self.APP_NAME_FILE}.pid" | |
193 | |
194 def run_twistd(self, args, twistd_opts=None): | |
195 """Run twistd settings options with args""" | |
196 from twisted.python.runtime import platformType | |
197 if platformType == "win32": | |
198 from twisted.scripts._twistw import (ServerOptions, | |
199 WindowsApplicationRunner as app_runner) | |
200 else: | |
201 from twisted.scripts._twistd_unix import (ServerOptions, | |
202 UnixApplicationRunner as app_runner) | |
203 | |
204 app_runner.loggerFactory = LiberviaLogger | |
205 server_options = ServerOptions() | |
206 config = self.get_config() | |
207 pid_file = self.get_pid_file(config) | |
208 log_dir = Path(config.get('DEFAULT', 'log_dir')).expanduser() | |
209 log_file = log_dir / f"{self.APP_NAME_FILE}.log" | |
210 server_opts = [ | |
211 '--no_save', | |
212 '--pidfile', str(pid_file), | |
213 '--logfile', str(log_file), | |
214 ] | |
215 if twistd_opts is not None: | |
216 server_opts.extend(twistd_opts) | |
217 server_opts.append(self.APP_NAME_FILE) | |
218 if args.extra_args: | |
219 try: | |
220 args.extra_args.remove('--') | |
221 except ValueError: | |
222 pass | |
223 server_opts.extend(args.extra_args) | |
224 try: | |
225 server_options.parseOptions(server_opts) | |
226 except usage.error as ue: | |
227 print(server_options) | |
228 print("%s: %s" % (sys.argv[0], ue)) | |
229 sys.exit(1) | |
230 else: | |
231 runner = app_runner(server_options) | |
232 runner.run() | |
233 if runner._exitSignal is not None: | |
234 app._exitWithSignal(runner._exitSignal) | |
235 try: | |
236 sys.exit(app._exitCode) | |
237 except AttributeError: | |
238 pass | |
239 | |
240 @classmethod | |
241 def run(cls): | |
242 args = cls().parse_args() | |
243 args.cmd(args) | |
244 | |
245 | |
246 if __name__ == '__main__': | |
247 Launcher.run() |