Skip to content

Commit

Permalink
feature: add daemon layer
Browse files Browse the repository at this point in the history
  • Loading branch information
u8slvn committed Jul 7, 2019
1 parent 4ca8008 commit fb80470
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 16 deletions.
30 changes: 27 additions & 3 deletions rerwatcher/__main__.py
Original file line number Diff line number Diff line change
@@ -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)
20 changes: 9 additions & 11 deletions rerwatcher/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -23,7 +26,6 @@ def __init__(self):
api=transilien_api,
formatter=transilien_formatter
)
logger.info("RERWatcher app built.")

@staticmethod
def load_config():
Expand All @@ -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:
Expand Down
80 changes: 80 additions & 0 deletions rerwatcher/daemon.py
Original file line number Diff line number Diff line change
@@ -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
7 changes: 5 additions & 2 deletions tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit fb80470

Please sign in to comment.