core (launcher): new wait_for_service option:
if `wait_for_service` is used in `[DEFAULT]`, the service at the specified host/port is waited for before starting the backend. This is useful in the situation where we need to be sure that something is started before running, e.g. in a container.
This commit is contained in:
parent
e14dd9da8b
commit
6fa46e20fd
1 changed files with 133 additions and 41 deletions
|
|
@ -29,7 +29,6 @@ from libervia.backend.core.constants import Const as C
|
|||
|
||||
|
||||
class LiberviaLogger(app.AppLogger):
|
||||
|
||||
def start(self, application):
|
||||
# logging is initialised by libervia.baceknd.core.log_config via the Twisted
|
||||
# plugin, nothing to do here
|
||||
|
|
@ -40,8 +39,8 @@ class LiberviaLogger(app.AppLogger):
|
|||
|
||||
|
||||
class Launcher:
|
||||
APP_NAME=C.APP_NAME
|
||||
APP_NAME_FILE=C.APP_NAME_FILE
|
||||
APP_NAME = C.APP_NAME
|
||||
APP_NAME_FILE = C.APP_NAME_FILE
|
||||
|
||||
@property
|
||||
def NOT_RUNNING_MSG(self):
|
||||
|
|
@ -56,14 +55,15 @@ class Launcher:
|
|||
self.run_twistd(args)
|
||||
|
||||
def cmd_foreground(self, args):
|
||||
self.run_twistd(args, twistd_opts=['--nodaemon'])
|
||||
self.run_twistd(args, twistd_opts=["--nodaemon"])
|
||||
|
||||
def cmd_debug(self, args):
|
||||
self.run_twistd(args, twistd_opts=['--debug'])
|
||||
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():
|
||||
|
|
@ -80,6 +80,7 @@ class Launcher:
|
|||
kill_started = time.time()
|
||||
state = "init"
|
||||
import errno
|
||||
|
||||
while True:
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
|
|
@ -87,18 +88,21 @@ class Launcher:
|
|||
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)
|
||||
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'
|
||||
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'
|
||||
elif state == "waiting" and now - kill_started > 10:
|
||||
state == "killing"
|
||||
print("Waiting for too long, we kill the process")
|
||||
os.kill(pid, signal.SIGKILL)
|
||||
sys.exit(1)
|
||||
|
|
@ -110,6 +114,7 @@ class Launcher:
|
|||
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:
|
||||
|
|
@ -133,7 +138,9 @@ class Launcher:
|
|||
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}")
|
||||
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)
|
||||
|
|
@ -146,34 +153,35 @@ class Launcher:
|
|||
extra_help = f"arguments to pass to {self.APP_NAME} service"
|
||||
|
||||
bg_parser = subparsers.add_parser(
|
||||
'background',
|
||||
aliases=['bg'],
|
||||
help=f"run {self.APP_NAME} backend in background (as a daemon)")
|
||||
bg_parser.add_argument('extra_args', nargs=argparse.REMAINDER, help=extra_help)
|
||||
"background",
|
||||
aliases=["bg"],
|
||||
help=f"run {self.APP_NAME} backend in background (as a daemon)",
|
||||
)
|
||||
bg_parser.add_argument("extra_args", nargs=argparse.REMAINDER, help=extra_help)
|
||||
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.add_argument('extra_args', nargs=argparse.REMAINDER, help=extra_help)
|
||||
"foreground",
|
||||
aliases=["fg"],
|
||||
help=f"run {self.APP_NAME} backend in foreground",
|
||||
)
|
||||
fg_parser.add_argument("extra_args", nargs=argparse.REMAINDER, help=extra_help)
|
||||
fg_parser.set_defaults(cmd=self.cmd_foreground)
|
||||
|
||||
dbg_parser = subparsers.add_parser(
|
||||
'debug',
|
||||
aliases=['dbg'],
|
||||
help=f"run {self.APP_NAME} backend in debug mode")
|
||||
dbg_parser.add_argument('extra_args', nargs=argparse.REMAINDER, help=extra_help)
|
||||
"debug", aliases=["dbg"], help=f"run {self.APP_NAME} backend in debug mode"
|
||||
)
|
||||
dbg_parser.add_argument("extra_args", nargs=argparse.REMAINDER, help=extra_help)
|
||||
dbg_parser.set_defaults(cmd=self.cmd_debug)
|
||||
|
||||
stop_parser = subparsers.add_parser(
|
||||
'stop',
|
||||
help=f"stop running {self.APP_NAME} backend")
|
||||
"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", help=f"indicate if {self.APP_NAME} backend is running"
|
||||
)
|
||||
status_parser.set_defaults(cmd=self.cmd_status)
|
||||
|
||||
return parser.parse_args()
|
||||
|
|
@ -183,41 +191,125 @@ class Launcher:
|
|||
try:
|
||||
config.read(C.CONFIG_FILES)
|
||||
except Exception as e:
|
||||
print (rf"/!\ Can't read main config! {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()
|
||||
pid_dir = Path(config.get("DEFAULT", "pid_dir")).expanduser()
|
||||
return pid_dir / f"{self.APP_NAME_FILE}.pid"
|
||||
|
||||
def wait_for_service(
|
||||
self,
|
||||
service_host: str,
|
||||
service_port: int,
|
||||
timeout: int,
|
||||
service_name: str
|
||||
) -> None:
|
||||
"""Waits for a network service to become available.
|
||||
|
||||
@param service_host: The hostname or IP address of the service.
|
||||
@param service_port: The port number of the service.
|
||||
@param timeout: The maximum number of seconds to wait for the service.
|
||||
@param service_name: The name of the service.
|
||||
|
||||
@raise TimeoutError: If the service is not available within the specified timeout.
|
||||
"""
|
||||
import socket
|
||||
import time
|
||||
|
||||
start_time = time.time()
|
||||
wait_interval = 5
|
||||
|
||||
while True:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.settimeout(1)
|
||||
try:
|
||||
sock.connect((service_host, service_port))
|
||||
return
|
||||
except socket.error:
|
||||
elapsed_time = time.time() - start_time
|
||||
if elapsed_time % wait_interval < 1:
|
||||
print(f"Waiting for {service_name}…")
|
||||
if elapsed_time > timeout:
|
||||
raise TimeoutError(
|
||||
f"{service_name} on {service_host}:{service_port} not "
|
||||
f"available after {timeout} seconds."
|
||||
)
|
||||
time.sleep(1)
|
||||
|
||||
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)
|
||||
from twisted.scripts._twistw import (
|
||||
ServerOptions,
|
||||
WindowsApplicationRunner as app_runner,
|
||||
)
|
||||
else:
|
||||
from twisted.scripts._twistd_unix import (ServerOptions,
|
||||
UnixApplicationRunner as app_runner)
|
||||
from twisted.scripts._twistd_unix import (
|
||||
ServerOptions,
|
||||
UnixApplicationRunner as app_runner,
|
||||
)
|
||||
|
||||
app_runner.loggerFactory = LiberviaLogger
|
||||
server_options = ServerOptions()
|
||||
config = self.get_config()
|
||||
|
||||
# wait for a service (e.g. XMPP server)
|
||||
wait_for_service_value = config.get(
|
||||
"DEFAULT", "init_wait_for_service", fallback=None
|
||||
)
|
||||
|
||||
if wait_for_service_value is not None:
|
||||
try:
|
||||
# Syntax: [ipv6_address]:port[:timeout][:service_name]
|
||||
# or hostname:port[:timeout][:service_name]
|
||||
parts = wait_for_service_value.split(":")
|
||||
if parts[0] and parts[0][0] == "[" and parts[0][-1] == "]":
|
||||
# IPv6 address
|
||||
host = parts[0][1:-1]
|
||||
else:
|
||||
# Hostname or IPv4
|
||||
host = parts[0]
|
||||
|
||||
port = int(parts[1])
|
||||
|
||||
# Defaults
|
||||
timeout = 60
|
||||
service_name = "service"
|
||||
|
||||
if len(parts) > 2:
|
||||
timeout_part = parts[2]
|
||||
# Check if timeout is skipped (double colon for service_name)
|
||||
if timeout_part:
|
||||
timeout = int(timeout_part)
|
||||
if len(parts) > 3:
|
||||
service_name = parts[3]
|
||||
except (ValueError, IndexError):
|
||||
raise ValueError(
|
||||
f'Invalid "init_wait_for_service" value: {wait_for_service_value!r}'
|
||||
)
|
||||
else:
|
||||
self.wait_for_service(host, port, timeout, service_name)
|
||||
|
||||
pid_file = self.get_pid_file(config)
|
||||
log_dir = Path(config.get('DEFAULT', 'log_dir')).expanduser()
|
||||
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),
|
||||
]
|
||||
"--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)
|
||||
if args.extra_args:
|
||||
try:
|
||||
args.extra_args.remove('--')
|
||||
args.extra_args.remove("--")
|
||||
except ValueError:
|
||||
pass
|
||||
server_opts.extend(args.extra_args)
|
||||
|
|
@ -243,5 +335,5 @@ class Launcher:
|
|||
args.cmd(args)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
Launcher.run()
|
||||
|
|
|
|||
Loading…
Reference in a new issue