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:
parent
c0a3f8d141
commit
a1eb68c1e7
6 changed files with 240 additions and 149 deletions
112
bin/sat
112
bin/sat
|
|
@ -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
228
sat/core/launcher.py
Normal 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()
|
||||||
|
|
@ -235,8 +235,6 @@ class Memory(object):
|
||||||
self.subscriptions = {}
|
self.subscriptions = {}
|
||||||
self.auth_sessions = PasswordSessions() # remember the authenticated profiles
|
self.auth_sessions = PasswordSessions() # remember the authenticated profiles
|
||||||
self.disco = Discovery(host)
|
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)
|
self.config = tools_config.parseMainConf(log_filenames=True)
|
||||||
database_file = os.path.expanduser(
|
database_file = os.path.expanduser(
|
||||||
os.path.join(self.getConfig("", "local_dir"), C.SAVEFILE_DATABASE)
|
os.path.join(self.getConfig("", "local_dir"), C.SAVEFILE_DATABASE)
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,10 @@ log = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def fixConfigOption(section, option, value, silent=True):
|
def fixConfigOption(section, option, value, silent=True):
|
||||||
"""Force a configuration option value, writing it in the first found user
|
"""Force a configuration option value
|
||||||
config file, eventually creating a new user config file if none is found.
|
|
||||||
|
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 section (str): the config section
|
||||||
@param option (str): the config option
|
@param option (str): the config option
|
||||||
|
|
@ -70,35 +72,6 @@ def fixConfigOption(section, option, value, silent=True):
|
||||||
config_file=target_file))
|
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):
|
def parseMainConf(log_filenames=False):
|
||||||
"""Look for main .ini configuration file, and parse it
|
"""Look for main .ini configuration file, and parse it
|
||||||
|
|
||||||
|
|
|
||||||
7
setup.py
7
setup.py
|
|
@ -110,7 +110,12 @@ setup(
|
||||||
["CHANGELOG", "COPYING", "INSTALL", "README", "README4TRANSLATORS"]),
|
["CHANGELOG", "COPYING", "INSTALL", "README", "README4TRANSLATORS"]),
|
||||||
(os.path.join("share", DBUS_DIR), [DBUS_FILE]),
|
(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,
|
zip_safe=False,
|
||||||
setup_requires=["setuptools_scm"] if is_dev_version else [],
|
setup_requires=["setuptools_scm"] if is_dev_version else [],
|
||||||
use_scm_version=sat_dev_version if is_dev_version else False,
|
use_scm_version=sat_dev_version if is_dev_version else False,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# SàT: an XMPP client
|
||||||
# SAT: a jabber client
|
# Copyright (C) 2009-2020 Jérôme Poisson (goffi@goffi.org)
|
||||||
# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
|
|
||||||
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue