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`.
This commit is contained in:
Goffi 2020-05-25 15:50:01 +02:00
commit a1eb68c1e7
6 changed files with 240 additions and 149 deletions

112
bin/sat
View file

@ -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

228
sat/core/launcher.py Normal file
View file

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

View file

@ -235,8 +235,6 @@ class Memory(object):
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)

View file

@ -34,8 +34,10 @@ log = getLogger(__name__)
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 @@ def fixConfigOption(section, option, value, silent=True):
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

View file

@ -110,7 +110,12 @@ setup(
["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,

View file

@ -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