diff --git a/rerwatcher/__main__.py b/rerwatcher/__main__.py index 3da42c4..5bb8bbf 100644 --- a/rerwatcher/__main__.py +++ b/rerwatcher/__main__.py @@ -1,10 +1,34 @@ #!/usr/bin/env python3 # coding: utf-8 +import os +import sys + from loguru import logger from rerwatcher.app import RerWatcher -logger.add("log/file_{time}.log", rotation="00:00", retention="2 days") +log_success = f'{os.path.dirname(__file__)}/../log/rerwatcher.success.log' +log_error = f'{os.path.dirname(__file__)}/../log/rerwatcher.error.log' + +logger.remove() + +logger.add(log_success, rotation='00:00', retention='2 days', level='DEBUG') +logger.add(log_error, rotation='00:00', retention='2 days', level='ERROR') + +daemon = RerWatcher( + pidfile='/tmp/rerwatcher.pid', + stdout=log_success, + stderr=log_error +) + +if len(sys.argv) != 2: + print(f"Usage: {sys.argv[0]} [start|stop].", file=sys.stderr) + raise SystemExit(1) -rer_watcher = RerWatcher() -rer_watcher.start() +if 'start' == sys.argv[1]: + daemon.start() +elif 'stop' == sys.argv[1]: + daemon.stop() +else: + print(f"Unknown command {sys.argv[1]!r}.", file=sys.stderr) + raise SystemExit(1) diff --git a/rerwatcher/app.py b/rerwatcher/app.py index 22f371a..9075193 100644 --- a/rerwatcher/app.py +++ b/rerwatcher/app.py @@ -4,14 +4,17 @@ import yaml from loguru import logger -from rerwatcher.formatter import TransilienApiFormatter from rerwatcher.api import TransilienApi +from rerwatcher.daemon import Daemon from rerwatcher.display import DisplayDeviceFactory +from rerwatcher.formatter import TransilienApiFormatter -class RerWatcher: - def __init__(self): - logger.info("Building RERWatcher app...") +class RerWatcher(Daemon): + def __init__(self, *args, **kwargs): + app_name = self.__class__.__name__ + super(RerWatcher, self).__init__(app_name=app_name, *args, **kwargs) + config = RerWatcher.load_config() matrix_display = DisplayDeviceFactory.build(config) transilien_api = TransilienApi(config) @@ -23,7 +26,6 @@ def __init__(self): api=transilien_api, formatter=transilien_formatter ) - logger.info("RERWatcher app built.") @staticmethod def load_config(): @@ -39,13 +41,9 @@ def load_config(): return config - def start(self): + def run(self): logger.info("Starting RERWatcher app...") - try: - self._app.start() - except KeyboardInterrupt: - logger.info("RERWatcher app stopped.") - pass + self._app.start() class _App: diff --git a/rerwatcher/daemon.py b/rerwatcher/daemon.py new file mode 100644 index 0000000..279af06 --- /dev/null +++ b/rerwatcher/daemon.py @@ -0,0 +1,80 @@ +import atexit +import os +import signal +import sys +from abc import ABC, abstractmethod + + +class Daemon(ABC): + def __init__(self, + pidfile: str, + app_name: str, + stdin: str = '/dev/null', + stdout: str = '/dev/null', + stderr: str = '/dev/null' + ): + self.pidfile = pidfile + self.app_name = app_name + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + + def daemonize(self): + if self._is_running(): + raise RuntimeError(f"{self.app_name} is already Running.") + + try: + if os.fork() > 0: + raise SystemExit(0) + except OSError: + raise RuntimeError("Fork #1 failed.") + + os.chdir('/') + os.umask(0) + os.setsid() + + try: + if os.fork() > 0: + raise SystemExit(0) + except OSError: + raise RuntimeError("Fork #2 failed.") + + sys.stdout.flush() + sys.stderr.flush() + + with open(self.stdin, 'rb') as f: + os.dup2(f.fileno(), sys.stdin.fileno()) + with open(self.stdout, 'ab') as f: + os.dup2(f.fileno(), sys.stdout.fileno()) + with open(self.stderr, 'ab') as f: + os.dup2(f.fileno(), sys.stderr.fileno()) + + with open(self.pidfile, 'w') as f: + print(os.getpid(), file=f) + + atexit.register(lambda: os.remove(self.pidfile)) + + def sigterm_handler(signo, frame): + raise SystemExit(1) + + signal.signal(signal.SIGTERM, sigterm_handler) + + def _is_running(self): + return os.path.exists(self.pidfile) + + def start(self): + self.daemonize() + self.run() + + def stop(self): + if not self._is_running(): + print(f"{self.app_name} is not running.") + raise SystemExit(1) + + with open(self.pidfile) as f: + pid = int(f.read()) + os.kill(pid, signal.SIGTERM) + + @abstractmethod + def run(self): + raise NotImplementedError diff --git a/tests/test_app.py b/tests/test_app.py index bda46ab..82af486 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -4,6 +4,8 @@ import os from unittest.mock import patch, sentinel, call +import pytest + from rerwatcher.app import RerWatcher from tests.conftest import FAKE_CONFIG @@ -35,8 +37,9 @@ def test_rerwatcher_workflow(mocker, mock_config): return_value=[sentinel.timetable1, sentinel.timetable2] ) - app = RerWatcher() - app.start() + app = RerWatcher(None) + with pytest.raises(KeyboardInterrupt): + app._app.start() display_builder.assert_called_once_with(FAKE_CONFIG) api.assert_called_once_with(FAKE_CONFIG)