def run_maestral_daemon(config_name="maestral", run=True, log_to_stdout=False): """ Wraps :class:`maestral.main.Maestral` as Pyro daemon object, creates a new instance and start Pyro's event loop to listen for requests on a unix domain socket. This call will block until the event loop shuts down. This command will return silently if the daemon is already running. :param str config_name: The name of the Maestral configuration to use. :param bool run: If ``True``, start syncing automatically. Defaults to ``True``. :param bool log_to_stdout: If ``True``, write logs to stdout. Defaults to ``False``. """ from maestral.main import Maestral sock_name = sockpath_for_config(config_name) pid_name = pidpath_for_config(config_name) lockfile = PIDLockFile(pid_name) # acquire PID lock file try: lockfile.acquire(timeout=1) except AlreadyLocked: if is_pidfile_stale(lockfile): lockfile.break_lock() else: logger.debug(f"Maestral already running") return logger.debug(f"Starting Maestral daemon on socket '{sock_name}'") try: # clean up old socket, create new one try: os.remove(sock_name) except FileNotFoundError: pass daemon = Daemon(unixsocket=sock_name) # start Maestral as Pyro server ExposedMaestral = expose(Maestral) # mark stop_sync and shutdown_daemon as oneway methods # so that they don't block on call ExposedMaestral.stop_sync = oneway(ExposedMaestral.stop_sync) ExposedMaestral.shutdown_pyro_daemon = oneway( ExposedMaestral.shutdown_pyro_daemon) m = ExposedMaestral(config_name, run=run, log_to_stdout=log_to_stdout) daemon.register(m, f"maestral.{config_name}") daemon.requestLoop(loopCondition=m._loop_condition) daemon.close() except Exception: traceback.print_exc() finally: # remove PID lock lockfile.release()
def run_maestral_daemon(config_name='maestral', run=True, log_to_stdout=False): """ Wraps :class:`maestral.main.Maestral` as Pyro daemon object, creates a new instance and start Pyro's event loop to listen for requests on a unix domain socket. This call will block until the event loop shuts down. This command will return silently if the daemon is already running. :param str config_name: The name of the Maestral configuration to use. :param bool run: If ``True``, start syncing automatically. Defaults to ``True``. :param bool log_to_stdout: If ``True``, write logs to stdout. Defaults to ``False``. """ import threading from maestral.main import Maestral sock_name = sockpath_for_config(config_name) pid_name = pidpath_for_config(config_name) lockfile = PIDLockFile(pid_name) if threading.current_thread() is threading.main_thread(): signal.signal(signal.SIGTERM, _sigterm_handler) # acquire PID lock file try: lockfile.acquire(timeout=1) except (AlreadyLocked, LockTimeout): if is_pidfile_stale(lockfile): lockfile.break_lock() else: logger.debug(f'Maestral already running') return # Nice ourselves give other processes priority. We will likely only # have significant CPU usage in case of many concurrent downloads. os.nice(10) logger.debug(f'Starting Maestral daemon on socket "{sock_name}"') try: # clean up old socket try: os.remove(sock_name) except FileNotFoundError: pass daemon = Daemon(unixsocket=sock_name) # start Maestral as Pyro server ExposedMaestral = expose(Maestral) # mark stop_sync and shutdown_daemon as one way # methods so that they don't block on call ExposedMaestral.stop_sync = oneway(ExposedMaestral.stop_sync) ExposedMaestral.pause_sync = oneway(ExposedMaestral.pause_sync) ExposedMaestral.shutdown_pyro_daemon = oneway( ExposedMaestral.shutdown_pyro_daemon) m = ExposedMaestral(config_name, run=run, log_to_stdout=log_to_stdout) daemon.register(m, f'maestral.{config_name}') daemon.requestLoop(loopCondition=m._loop_condition) daemon.close() except Exception: traceback.print_exc() except (KeyboardInterrupt, SystemExit): logger.info('Received system exit') sys.exit(0) finally: lockfile.release()
def start_maestral_daemon(config_name: str = "maestral", log_to_stderr: bool = False, start_sync: bool = False) -> None: """ Starts the Maestral daemon with event loop in the current thread. Startup is race free: there will never be more than one daemon running with the same config name. The daemon is a :class:`maestral.main.Maestral` instance which is exposed as Pyro daemon object and listens for requests on a unix domain socket. This call starts an asyncio event loop to process client requests and blocks until the event loop shuts down. On macOS, the event loop is integrated with Cocoa's CFRunLoop. This allows processing Cocoa events and callbacks, for instance for desktop notifications. :param config_name: The name of the Maestral configuration to use. :param log_to_stderr: If ``True``, write logs to stderr. :param start_sync: If ``True``, start syncing once the daemon has started. If the ``start_sync`` call fails, an error will be logged but not raised. :raises RuntimeError: if a daemon for the given ``config_name`` is already running. """ import asyncio from .main import Maestral from .logging import scoped_logger, setup_logging setup_logging(config_name, log_to_stderr) dlogger = scoped_logger(__name__, config_name) dlogger.info("Starting daemon") if threading.current_thread() is not threading.main_thread(): dlogger.error("Must run daemon in main thread") raise RuntimeError("Must run daemon in main thread") dlogger.debug("Environment:\n%s", pformat(os.environ.copy())) # acquire PID lock file lock = maestral_lock(config_name) if lock.acquire(): dlogger.debug("Acquired daemon lock: %s", lock.path) else: dlogger.error("Could not acquire lock, daemon is already running") raise RuntimeError("Daemon is already running") # Nice ourselves to give other processes priority. os.nice(10) # Integrate with CFRunLoop in macOS. if IS_MACOS: dlogger.debug("Integrating with CFEventLoop") from rubicon.objc.eventloop import EventLoopPolicy # type: ignore asyncio.set_event_loop_policy(EventLoopPolicy()) # Get the default event loop. loop = asyncio.get_event_loop() sd_notifier = sdnotify.SystemdNotifier() # Notify systemd that we have started. if NOTIFY_SOCKET: dlogger.debug("Running as systemd notify service") dlogger.debug("NOTIFY_SOCKET = %s", NOTIFY_SOCKET) sd_notifier.notify("READY=1") # Notify systemd periodically if alive. if IS_WATCHDOG and WATCHDOG_USEC: async def periodic_watchdog() -> None: if WATCHDOG_USEC: sleep = int(WATCHDOG_USEC) while True: sd_notifier.notify("WATCHDOG=1") await asyncio.sleep(sleep / (2 * 10**6)) dlogger.debug("Running as systemd watchdog service") dlogger.debug("WATCHDOG_USEC = %s", WATCHDOG_USEC) dlogger.debug("WATCHDOG_PID = %s", WATCHDOG_PID) loop.create_task(periodic_watchdog()) # Get socket for config name. sockpath = sockpath_for_config(config_name) dlogger.debug(f"Socket path: '{sockpath}'") # Clean up old socket. try: os.remove(sockpath) except FileNotFoundError: pass # Expose maestral as Pyro server. Convert management # methods to one way calls so that they don't block. dlogger.debug("Creating Pyro daemon") ExposedMaestral = expose(Maestral) ExposedMaestral.start_sync = oneway(ExposedMaestral.start_sync) ExposedMaestral.stop_sync = oneway(ExposedMaestral.stop_sync) ExposedMaestral.shutdown_daemon = oneway(ExposedMaestral.shutdown_daemon) maestral_daemon = ExposedMaestral(config_name, log_to_stderr=log_to_stderr) if start_sync: dlogger.debug("Starting sync") maestral_daemon.start_sync() try: dlogger.debug("Starting event loop") with Daemon(unixsocket=sockpath) as daemon: daemon.register(maestral_daemon, f"maestral.{config_name}") # Reduce Pyro's housekeeping frequency from 2 sec to 20 sec. # This avoids constantly waking the CPU when we are idle. if daemon.transportServer.housekeeper: daemon.transportServer.housekeeper.waittime = 20 for socket in daemon.sockets: loop.add_reader(socket.fileno(), daemon.events, daemon.sockets) # Handle sigterm gracefully. signals = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT) for s in signals: loop.add_signal_handler(s, maestral_daemon.shutdown_daemon) loop.run_until_complete(maestral_daemon.shutdown_complete) for socket in daemon.sockets: loop.remove_reader(socket.fileno()) # Prevent Pyro housekeeping from blocking shutdown. daemon.transportServer.housekeeper = None except Exception as exc: dlogger.error(exc.args[0], exc_info=True) finally: if NOTIFY_SOCKET: # Notify systemd that we are shutting down. sd_notifier.notify("STOPPING=1")
def start_maestral_daemon(config_name: str = "maestral", log_to_stdout: bool = False, start_sync: bool = False) -> None: """ Starts the Maestral daemon with event loop in the current thread. Startup is race free: there will never be two daemons running for the same config. Wraps :class:`main.Maestral` as Pyro daemon object, creates a new instance and starts an asyncio event loop to listen for requests on a unix domain socket. This call will block until the event loop shuts down. When this function is called from the main thread on macOS, the asyncio event loop uses Cocoa's CFRunLoop to process event. This allows integration with Cocoa frameworks which use callbacks to process use input such as clicked notifications, etc, and potentially allows showing a GUI. :param config_name: The name of the Maestral configuration to use. :param log_to_stdout: If ``True``, write logs to stdout. :param start_sync: If ``True``, start syncing once the daemon has started. :raises: :class:`RuntimeError` if a daemon for the given ``config_name`` is already running. """ import asyncio from maestral.main import Maestral if threading.current_thread() is not threading.main_thread(): raise RuntimeError("Must run daemon in main thread") # acquire PID lock file lock = maestral_lock(config_name) if not lock.acquire(): raise RuntimeError("Maestral daemon is already running") # Nice ourselves to give other processes priority. We will likely only # have significant CPU usage in case of many concurrent downloads. os.nice(10) # catch sigterm and shut down gracefully signal.signal(signal.SIGTERM, _sigterm_handler) # integrate with CFRunLoop in macOS, only works in main thread if sys.platform == "darwin": logger.debug("Cancelling all tasks from asyncio event loop") from rubicon.objc.eventloop import EventLoopPolicy # type: ignore # clean up any pending tasks before we change the event loop policy # this is necessary if previous code has run an asyncio loop loop = asyncio.get_event_loop() try: # Python 3.7 and higher all_tasks = asyncio.all_tasks(loop) except AttributeError: # Python 3.6 all_tasks = asyncio.Task.all_tasks(loop) pending_tasks = [t for t in all_tasks if not t.done()] for task in pending_tasks: task.cancel() loop.run_until_complete( asyncio.gather(*pending_tasks, return_exceptions=True)) loop.close() logger.debug("Integrating with CFEventLoop") # set new event loop policy asyncio.set_event_loop_policy(EventLoopPolicy()) # get the default event loop loop = asyncio.get_event_loop() sd_notifier = sdnotify.SystemdNotifier() # notify systemd that we have started if NOTIFY_SOCKET: logger.debug("Running as systemd notify service") logger.debug("NOTIFY_SOCKET = %s", NOTIFY_SOCKET) sd_notifier.notify("READY=1") # notify systemd periodically if alive if IS_WATCHDOG and WATCHDOG_USEC: async def periodic_watchdog() -> None: if WATCHDOG_USEC: sleep = int(WATCHDOG_USEC) while True: sd_notifier.notify("WATCHDOG=1") await asyncio.sleep(sleep / (2 * 10**6)) logger.debug("Running as systemd watchdog service") logger.debug("WATCHDOG_USEC = %s", WATCHDOG_USEC) logger.debug("WATCHDOG_PID = %s", WATCHDOG_PID) loop.create_task(periodic_watchdog()) # get socket for config name sockpath = sockpath_for_config(config_name) logger.debug(f"Socket path for '{config_name}' daemon: '{sockpath}'") # clean up old socket try: os.remove(sockpath) except FileNotFoundError: pass # expose maestral as Pyro server # convert management methods to one way calls so that they don't block logger.debug("Creating Pyro daemon") ExposedMaestral = expose(Maestral) ExposedMaestral.start_sync = oneway(ExposedMaestral.start_sync) ExposedMaestral.stop_sync = oneway(ExposedMaestral.stop_sync) ExposedMaestral.pause_sync = oneway(ExposedMaestral.pause_sync) ExposedMaestral.resume_sync = oneway(ExposedMaestral.resume_sync) ExposedMaestral.shutdown_daemon = oneway(ExposedMaestral.shutdown_daemon) maestral_daemon = ExposedMaestral(config_name, log_to_stdout=log_to_stdout) if start_sync: logger.debug("Starting sync") maestral_daemon.start_sync() try: logger.debug("Starting event loop") with Daemon(unixsocket=sockpath) as daemon: daemon.register(maestral_daemon, f"maestral.{config_name}") for socket in daemon.sockets: loop.add_reader(socket.fileno(), daemon.events, daemon.sockets) loop.run_until_complete(maestral_daemon.shutdown_complete) for socket in daemon.sockets: loop.remove_reader(socket.fileno()) except Exception: traceback.print_exc() finally: if NOTIFY_SOCKET: # notify systemd that we are shutting down sd_notifier.notify("STOPPING=1")