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