# HG changeset patch # User Goffi # Date 1590414601 -7200 # Node ID a3639d6d96436b47e09d9a1e00d664b44fdf0860 # Parent 96b9b65b43682cc0aa53923c8e44bf095843ecae 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`. diff -r 96b9b65b4368 -r a3639d6d9643 bin/sat --- a/bin/sat Mon May 25 15:46:21 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,112 +0,0 @@ -#!/bin/sh - -DEBUG="" -DAEMON="" -PYTHON="python3" - -TWISTD="$(which twistd 2>/dev/null)" -if [ $? -ne 0 ]; then - # for Python 3, "twistd" is named "twistd3" on some distros - TWISTD="$(which twistd3 2>/dev/null)" -fi -if [ $? -ne 0 ]; then - printf "Can't find \"twistd\" script, are you sure that Twisted is installed?\n" - exit 1 -fi - -kill_process() { - # $1 is the file containing the PID to kill, $2 is the process name - if [ -f $1 ]; then - PID=`cat $1` - if ps -p $PID > /dev/null; then - printf "Terminating $2... " - kill $PID - while ps -p $PID > /dev/null; do - sleep 0.2 - done - printf "OK\n" - else - echo "No running process of ID $PID... removing PID file" - rm -f $1 - fi - else - echo "$2 is probably not running (PID file doesn't exist)" - fi -} - -#We use python to parse config files -eval `/usr/bin/env "$PYTHON" << PYTHONEND -from sat.core.constants import Const as C -from sat.tools.config import fixLocalDir -from configparser import ConfigParser -from os.path import expanduser, join -import sys - -fixLocalDir() # XXX: tmp update code, will be removed in the future - -config = ConfigParser(defaults=C.DEFAULT_CONFIG) -try: - config.read(C.CONFIG_FILES) -except: - print ("echo \"/!\\ Can't read main config ! Please check the syntax\";") - print ("exit 1") - sys.exit() - -env=[] -env.append("PID_DIR='%s'" % join(expanduser(config.get('DEFAULT', 'pid_dir')),'')) -env.append("LOG_DIR='%s'" % join(expanduser(config.get('DEFAULT', 'log_dir')),'')) -env.append("APP_NAME='%s'" % C.APP_NAME) -env.append("APP_NAME_FILE='%s'" % C.APP_NAME_FILE) -print (";".join(env)) -PYTHONEND -` -APP_NAME="$APP_NAME" -PID_FILE="$PID_DIR$APP_NAME_FILE.pid" -LOG_FILE="$LOG_DIR$APP_NAME_FILE.log" -RUNNING_MSG="$APP_NAME is running" -NOT_RUNNING_MSG="$APP_NAME is *NOT* running" - -# if there is one argument which is "stop", then we kill SaT -if [ $# -eq 1 ];then - if [ $1 = "stop" ];then - kill_process $PID_FILE "$APP_NAME" - exit 0 - elif [ $1 = "debug" ];then - echo "Launching $APP_NAME in debug mode" - DEBUG="--debug" - elif [ $1 = "fg" ];then - echo "Launching $APP_NAME in foreground mode" - DAEMON="n" - elif [ $1 = "status" ];then - if [ -f $PID_FILE ]; then - PID=`cat $PID_FILE` - ps -p$PID 2>&1 > /dev/null - if [ $? = 0 ];then - echo "$RUNNING_MSG (pid: $PID)" - exit 0 - else - echo "$NOT_RUNNING_MSG, but a pid file is present (bad exit ?): $PID_FILE" - exit 2 - fi - else - echo "$NOT_RUNNING_MSG" - exit 1 - fi - else - echo "bad argument, please use one of (stop, debug, fg, status) or no argument" - exit 1 - fi -fi - -MAIN_OPTIONS="-${DAEMON}o" - -#Don't change the next lines -AUTO_OPTIONS="" -ADDITIONAL_OPTIONS="--pidfile $PID_FILE --logfile $LOG_FILE $AUTO_OPTIONS $DEBUG" - -log_dir=`dirname "$LOG_FILE"` -if [ ! -d $log_dir ] ; then - mkdir $log_dir -fi - -exec /usr/bin/env $PYTHON $TWISTD $MAIN_OPTIONS $ADDITIONAL_OPTIONS $APP_NAME_FILE diff -r 96b9b65b4368 -r a3639d6d9643 sat/core/launcher.py --- /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 . + +"""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() diff -r 96b9b65b4368 -r a3639d6d9643 sat/memory/memory.py --- a/sat/memory/memory.py Mon May 25 15:46:21 2020 +0200 +++ b/sat/memory/memory.py Mon May 25 15:50:01 2020 +0200 @@ -235,8 +235,6 @@ self.subscriptions = {} self.auth_sessions = PasswordSessions() # remember the authenticated profiles self.disco = Discovery(host) - # XXX: tmp update code, will be removed in the future - tools_config.fixLocalDir(False) self.config = tools_config.parseMainConf(log_filenames=True) database_file = os.path.expanduser( os.path.join(self.getConfig("", "local_dir"), C.SAVEFILE_DATABASE) diff -r 96b9b65b4368 -r a3639d6d9643 sat/tools/config.py --- a/sat/tools/config.py Mon May 25 15:46:21 2020 +0200 +++ b/sat/tools/config.py Mon May 25 15:50:01 2020 +0200 @@ -34,8 +34,10 @@ def fixConfigOption(section, option, value, silent=True): - """Force a configuration option value, writing it in the first found user - config file, eventually creating a new user config file if none is found. + """Force a configuration option value + + the option will be written in the first found user config file, a new user + config will be created if none is found. @param section (str): the config section @param option (str): the config option @@ -70,35 +72,6 @@ config_file=target_file)) -# XXX: tmp update code, will be removed in the future -# When you remove this, please add the default value for -# 'local_dir' in sat.core.constants.Const.DEFAULT_CONFIG -def fixLocalDir(silent=True): - """Retro-compatibility with the previous local_dir default value. - - @param silent (boolean): toggle logging output (must be True when called from sat.sh) - """ - user_config = ConfigParser() - try: - user_config.read(C.CONFIG_FILES) - except: - pass # file is readable but its structure if wrong - try: - current_value = user_config.get("DEFAULT", "local_dir") - except (NoOptionError, NoSectionError): - current_value = "" - if current_value: - return # nothing to do - old_default = "~/.sat" - if os.path.isfile(os.path.expanduser(old_default) + "/" + C.SAVEFILE_DATABASE): - if not silent: - log.warning(_( - "A database has been found in the default local_dir for previous " - "versions (< 0.5)" - )) - fixConfigOption("", "local_dir", old_default, silent) - - def parseMainConf(log_filenames=False): """Look for main .ini configuration file, and parse it diff -r 96b9b65b4368 -r a3639d6d9643 setup.py --- a/setup.py Mon May 25 15:46:21 2020 +0200 +++ b/setup.py Mon May 25 15:50:01 2020 +0200 @@ -110,7 +110,12 @@ ["CHANGELOG", "COPYING", "INSTALL", "README", "README4TRANSLATORS"]), (os.path.join("share", DBUS_DIR), [DBUS_FILE]), ], - scripts=["sat_frontends/jp/jp", "sat_frontends/primitivus/primitivus", "bin/sat"], + entry_points={ + "console_scripts": [ + "sat = sat.core.launcher:Launcher.run", + ], + }, + scripts=["sat_frontends/jp/jp", "sat_frontends/primitivus/primitivus"], zip_safe=False, setup_requires=["setuptools_scm"] if is_dev_version else [], use_scm_version=sat_dev_version if is_dev_version else False, diff -r 96b9b65b4368 -r a3639d6d9643 twisted/plugins/sat_plugin.py --- a/twisted/plugins/sat_plugin.py Mon May 25 15:46:21 2020 +0200 +++ b/twisted/plugins/sat_plugin.py Mon May 25 15:50:01 2020 +0200 @@ -1,8 +1,7 @@ #!/usr/bin/env python3 - -# SAT: a jabber client -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) +# 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