# HG changeset patch # User Goffi # Date 1702501225 -3600 # Node ID 3dbaf179c50dae0214d8511c55798306d5b1349a # Parent 730f542e4ad0121f2c74e7028e9284992fdd2838 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. diff -r 730f542e4ad0 -r 3dbaf179c50d libervia/backend/core/launcher.py --- a/libervia/backend/core/launcher.py Wed Dec 13 22:00:22 2023 +0100 +++ b/libervia/backend/core/launcher.py Wed Dec 13 22:00:25 2023 +0100 @@ -29,7 +29,6 @@ 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 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 @@ 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 @@ kill_started = time.time() state = "init" import errno + while True: try: os.kill(pid, 0) @@ -87,18 +88,21 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ args.cmd(args) -if __name__ == '__main__': +if __name__ == "__main__": Launcher.run()