Mercurial > libervia-backend
comparison sat/core/launcher.py @ 3281:a3639d6d9643
core: replaced `sat` shell script by a python script:
the backend is now launched by `sat.core.launcher`, and the script is generated during
installation with a new entry point in `setup.py`.
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 25 May 2020 15:50:01 +0200 |
parents | bin/sat@e81ad34e8af8 |
children | e7e7be79fbcd |
comparison
equal
deleted
inserted
replaced
3280:96b9b65b4368 | 3281:a3639d6d9643 |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 # SàT: an XMPP client | |
4 # Copyright (C) 2009-2020 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 os.path import expanduser, join | |
27 from twisted.application import app | |
28 from twisted.python import usage | |
29 from sat.core.constants import Const as C | |
30 | |
31 | |
32 class SatLogger(app.AppLogger): | |
33 | |
34 def start(self, application): | |
35 # logging is initialised by sat.core.log_config via the Twisted plugin, nothing | |
36 # to do here | |
37 self._initialLog() | |
38 | |
39 def stop(self): | |
40 pass | |
41 | |
42 | |
43 class Launcher: | |
44 APP_NAME=C.APP_NAME | |
45 APP_NAME_FILE=C.APP_NAME_FILE | |
46 | |
47 @property | |
48 def NOT_RUNNING_MSG(self): | |
49 return f"{self.APP_NAME} is *NOT* running" | |
50 | |
51 def cmd_background(self, args): | |
52 self.run_twistd(args) | |
53 | |
54 def cmd_foreground(self, args): | |
55 self.run_twistd(args, twistd_opts=['--nodaemon']) | |
56 | |
57 def cmd_debug(self, args): | |
58 self.run_twistd(args, twistd_opts=['--debug']) | |
59 | |
60 def cmd_stop(self, args): | |
61 import signal | |
62 import time | |
63 config = self.get_config() | |
64 pid_file = self.get_pid_file(config) | |
65 if not pid_file.is_file(): | |
66 print(self.NOT_RUNNING_MSG) | |
67 sys.exit(0) | |
68 try: | |
69 pid = int(pid_file.read_text()) | |
70 except Exception as e: | |
71 print(f"Can't read PID file at {pid_file}: {e}") | |
72 # we use the same exit code as DATA_ERROR in jp | |
73 sys.exit(17) | |
74 print(f"Terminating {self.APP_NAME}…") | |
75 os.kill(pid, signal.SIGTERM) | |
76 kill_started = time.time() | |
77 state = "init" | |
78 import errno | |
79 while True: | |
80 try: | |
81 os.kill(pid, 0) | |
82 except OSError as e: | |
83 if e.errno == errno.ESRCH: | |
84 break | |
85 elif e.errno == errno.EPERM: | |
86 print(f"Can't kill {self.APP_NAME}, the process is owned by an other user", file=sys.stderr) | |
87 sys.exit(18) | |
88 else: | |
89 raise e | |
90 time.sleep(0.2) | |
91 now = time.time() | |
92 if state == 'init' and now - kill_started > 5: | |
93 if state == 'init': | |
94 state = 'waiting' | |
95 print(f"Still waiting for {self.APP_NAME} to be terminated…") | |
96 elif state == 'waiting' and now - kill_started > 10: | |
97 state == 'killing' | |
98 print(f"Waiting for too long, we kill the process") | |
99 os.kill(pid, signal.SIGKILL) | |
100 sys.exit(1) | |
101 | |
102 sys.exit(0) | |
103 | |
104 def cmd_status(self, args): | |
105 config = self.get_config() | |
106 pid_file = self.get_pid_file(config) | |
107 if pid_file.is_file(): | |
108 import errno | |
109 try: | |
110 pid = int(pid_file.read_text()) | |
111 except Exception as e: | |
112 print(f"Can't read PID file at {pid_file}: {e}") | |
113 # we use the same exit code as DATA_ERROR in jp | |
114 sys.exit(17) | |
115 # we check if there is a process | |
116 # inspired by https://stackoverflow.com/a/568285 and https://stackoverflow.com/a/6940314 | |
117 try: | |
118 os.kill(pid, 0) | |
119 except OSError as e: | |
120 if e.errno == errno.ESRCH: | |
121 running = False | |
122 elif e.errno == errno.EPERM: | |
123 print("Process {pid} is run by an other user") | |
124 running = True | |
125 else: | |
126 running = True | |
127 | |
128 if running: | |
129 print(f"{self.APP_NAME} is running (pid: {pid})") | |
130 sys.exit(0) | |
131 else: | |
132 print(f"{self.NOT_RUNNING_MSG}, but a pid file is present (bad exit ?): {pid_file}") | |
133 sys.exit(2) | |
134 else: | |
135 print(self.NOT_RUNNING_MSG) | |
136 sys.exit(1) | |
137 | |
138 def parse_args(self): | |
139 parser = argparse.ArgumentParser(description=f"Launch {self.APP_NAME} backend") | |
140 parser.set_defaults(cmd=self.cmd_background) | |
141 subparsers = parser.add_subparsers() | |
142 | |
143 bg_parser = subparsers.add_parser( | |
144 'background', | |
145 aliases=['bg'], | |
146 help=f"run {self.APP_NAME} backend in background (as a daemon)") | |
147 bg_parser.set_defaults(cmd=self.cmd_background) | |
148 | |
149 fg_parser = subparsers.add_parser( | |
150 'foreground', | |
151 aliases=['fg'], | |
152 help=f"run {self.APP_NAME} backend in foreground") | |
153 fg_parser.set_defaults(cmd=self.cmd_foreground) | |
154 | |
155 dbg_parser = subparsers.add_parser( | |
156 'debug', | |
157 help=f"run {self.APP_NAME} backend in debug mode") | |
158 dbg_parser.set_defaults(cmd=self.cmd_debug) | |
159 | |
160 stop_parser = subparsers.add_parser( | |
161 'stop', | |
162 help=f"stop running {self.APP_NAME} backend") | |
163 stop_parser.set_defaults(cmd=self.cmd_stop) | |
164 | |
165 status_parser = subparsers.add_parser( | |
166 'status', | |
167 help=f"indicate if {self.APP_NAME} backend is running") | |
168 status_parser.set_defaults(cmd=self.cmd_status) | |
169 | |
170 return parser.parse_args() | |
171 | |
172 def get_config(self): | |
173 config = ConfigParser(defaults=C.DEFAULT_CONFIG) | |
174 try: | |
175 config.read(C.CONFIG_FILES) | |
176 except Exception as e: | |
177 print (rf"/!\ Can't read main config! {e}") | |
178 sys.exit(1) | |
179 return config | |
180 | |
181 def get_pid_file(self, config): | |
182 pid_dir = Path(config.get('DEFAULT', 'pid_dir')).expanduser() | |
183 return pid_dir / f"{self.APP_NAME_FILE}.pid" | |
184 | |
185 def run_twistd(self, args, twistd_opts=None): | |
186 """Run twistd settings options with args""" | |
187 from twisted.python.runtime import platformType | |
188 if platformType == "win32": | |
189 from twisted.scripts._twistw import (ServerOptions, | |
190 WindowsApplicationRunner as app_runner) | |
191 else: | |
192 from twisted.scripts._twistd_unix import (ServerOptions, | |
193 UnixApplicationRunner as app_runner) | |
194 | |
195 app_runner.loggerFactory = SatLogger | |
196 server_options = ServerOptions() | |
197 config = self.get_config() | |
198 pid_file = self.get_pid_file(config) | |
199 log_dir = Path(config.get('DEFAULT', 'log_dir')).expanduser() | |
200 log_file = log_dir / f"{self.APP_NAME_FILE}.log" | |
201 server_opts = [ | |
202 '--no_save', | |
203 '--pidfile', str(pid_file), | |
204 '--logfile', str(log_file), | |
205 ] | |
206 if twistd_opts is not None: | |
207 server_opts.extend(twistd_opts) | |
208 server_opts.append(self.APP_NAME_FILE) | |
209 try: | |
210 server_options.parseOptions(server_opts) | |
211 except usage.error as ue: | |
212 print(server_options) | |
213 print("%s: %s" % (sys.argv[0], ue)) | |
214 sys.exit(1) | |
215 else: | |
216 runner = app_runner(server_options) | |
217 runner.run() | |
218 if runner._exitSignal is not None: | |
219 app._exitWithSignal(runner._exitSignal) | |
220 | |
221 @classmethod | |
222 def run(cls): | |
223 args = cls().parse_args() | |
224 args.cmd(args) | |
225 | |
226 | |
227 if __name__ == '__main__': | |
228 Launcher.run() |