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()