Mercurial > libervia-backend
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sat/core/launcher.py Mon May 25 15:50:01 2020 +0200 @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 + +# SàT: an XMPP client +# Copyright (C) 2009-2020 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +"""Script launching SàT backend""" + +import sys +import os +import argparse +from pathlib import Path +from configparser import ConfigParser +from os.path import expanduser, join +from twisted.application import app +from twisted.python import usage +from sat.core.constants import Const as C + + +class SatLogger(app.AppLogger): + + def start(self, application): + # logging is initialised by sat.core.log_config via the Twisted plugin, nothing + # to do here + self._initialLog() + + def stop(self): + pass + + +class Launcher: + APP_NAME=C.APP_NAME + APP_NAME_FILE=C.APP_NAME_FILE + + @property + def NOT_RUNNING_MSG(self): + return f"{self.APP_NAME} is *NOT* running" + + def cmd_background(self, args): + self.run_twistd(args) + + def cmd_foreground(self, args): + self.run_twistd(args, twistd_opts=['--nodaemon']) + + def cmd_debug(self, args): + self.run_twistd(args, twistd_opts=['--debug']) + + def cmd_stop(self, args): + import signal + import time + config = self.get_config() + pid_file = self.get_pid_file(config) + if not pid_file.is_file(): + print(self.NOT_RUNNING_MSG) + sys.exit(0) + try: + pid = int(pid_file.read_text()) + except Exception as e: + print(f"Can't read PID file at {pid_file}: {e}") + # we use the same exit code as DATA_ERROR in jp + sys.exit(17) + print(f"Terminating {self.APP_NAME}…") + os.kill(pid, signal.SIGTERM) + kill_started = time.time() + state = "init" + import errno + while True: + try: + os.kill(pid, 0) + except OSError as e: + if e.errno == errno.ESRCH: + break + elif e.errno == errno.EPERM: + print(f"Can't kill {self.APP_NAME}, the process is owned by an other user", file=sys.stderr) + sys.exit(18) + else: + raise e + time.sleep(0.2) + now = time.time() + if state == 'init' and now - kill_started > 5: + if state == 'init': + state = 'waiting' + print(f"Still waiting for {self.APP_NAME} to be terminated…") + elif state == 'waiting' and now - kill_started > 10: + state == 'killing' + print(f"Waiting for too long, we kill the process") + os.kill(pid, signal.SIGKILL) + sys.exit(1) + + sys.exit(0) + + def cmd_status(self, args): + config = self.get_config() + pid_file = self.get_pid_file(config) + if pid_file.is_file(): + import errno + try: + pid = int(pid_file.read_text()) + except Exception as e: + print(f"Can't read PID file at {pid_file}: {e}") + # we use the same exit code as DATA_ERROR in jp + sys.exit(17) + # we check if there is a process + # inspired by https://stackoverflow.com/a/568285 and https://stackoverflow.com/a/6940314 + try: + os.kill(pid, 0) + except OSError as e: + if e.errno == errno.ESRCH: + running = False + elif e.errno == errno.EPERM: + print("Process {pid} is run by an other user") + running = True + else: + running = True + + if running: + print(f"{self.APP_NAME} is running (pid: {pid})") + sys.exit(0) + else: + print(f"{self.NOT_RUNNING_MSG}, but a pid file is present (bad exit ?): {pid_file}") + sys.exit(2) + else: + print(self.NOT_RUNNING_MSG) + sys.exit(1) + + def parse_args(self): + parser = argparse.ArgumentParser(description=f"Launch {self.APP_NAME} backend") + parser.set_defaults(cmd=self.cmd_background) + subparsers = parser.add_subparsers() + + bg_parser = subparsers.add_parser( + 'background', + aliases=['bg'], + help=f"run {self.APP_NAME} backend in background (as a daemon)") + bg_parser.set_defaults(cmd=self.cmd_background) + + fg_parser = subparsers.add_parser( + 'foreground', + aliases=['fg'], + help=f"run {self.APP_NAME} backend in foreground") + fg_parser.set_defaults(cmd=self.cmd_foreground) + + dbg_parser = subparsers.add_parser( + 'debug', + help=f"run {self.APP_NAME} backend in debug mode") + dbg_parser.set_defaults(cmd=self.cmd_debug) + + stop_parser = subparsers.add_parser( + 'stop', + help=f"stop running {self.APP_NAME} backend") + stop_parser.set_defaults(cmd=self.cmd_stop) + + status_parser = subparsers.add_parser( + 'status', + help=f"indicate if {self.APP_NAME} backend is running") + status_parser.set_defaults(cmd=self.cmd_status) + + return parser.parse_args() + + def get_config(self): + config = ConfigParser(defaults=C.DEFAULT_CONFIG) + try: + config.read(C.CONFIG_FILES) + except Exception as e: + print (rf"/!\ Can't read main config! {e}") + sys.exit(1) + return config + + def get_pid_file(self, config): + pid_dir = Path(config.get('DEFAULT', 'pid_dir')).expanduser() + return pid_dir / f"{self.APP_NAME_FILE}.pid" + + def run_twistd(self, args, twistd_opts=None): + """Run twistd settings options with args""" + from twisted.python.runtime import platformType + if platformType == "win32": + from twisted.scripts._twistw import (ServerOptions, + WindowsApplicationRunner as app_runner) + else: + from twisted.scripts._twistd_unix import (ServerOptions, + UnixApplicationRunner as app_runner) + + app_runner.loggerFactory = SatLogger + server_options = ServerOptions() + config = self.get_config() + pid_file = self.get_pid_file(config) + log_dir = Path(config.get('DEFAULT', 'log_dir')).expanduser() + log_file = log_dir / f"{self.APP_NAME_FILE}.log" + server_opts = [ + '--no_save', + '--pidfile', str(pid_file), + '--logfile', str(log_file), + ] + if twistd_opts is not None: + server_opts.extend(twistd_opts) + server_opts.append(self.APP_NAME_FILE) + try: + server_options.parseOptions(server_opts) + except usage.error as ue: + print(server_options) + print("%s: %s" % (sys.argv[0], ue)) + sys.exit(1) + else: + runner = app_runner(server_options) + runner.run() + if runner._exitSignal is not None: + app._exitWithSignal(runner._exitSignal) + + @classmethod + def run(cls): + args = cls().parse_args() + args.cmd(args) + + +if __name__ == '__main__': + Launcher.run()