Example #1
0
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()
Example #2
0
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()
Example #3
0
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")
Example #4
0
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")