| # Copyright 2024 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """A daemon base class.""" |
| |
| from __future__ import annotations |
| |
| import abc |
| import enum |
| import logging |
| import os |
| import signal |
| import sys |
| import time |
| from typing import NoReturn, Optional |
| |
| |
| class State(enum.Enum): |
| """An enum class for the ehide states.""" |
| |
| OFF = "off" |
| SET_UP = "set_up" |
| ON = "on" |
| TEAR_DOWN = "tear_down" |
| |
| @classmethod |
| def from_str(cls, s: str) -> Optional[State]: |
| """Creates a State enum from string. |
| |
| Args: |
| s: The string. |
| |
| Returns: |
| A state enum. If fails then return None. |
| """ |
| for state in State: |
| if state.value == s: |
| return state |
| return None |
| |
| |
| class Daemon(abc.ABC): |
| """A abstract daemon class. |
| |
| When started, the daemon is forked twice. The first fork is to detach the |
| process from the current terminal session, and the second fork is to ensure |
| that the new process is not a session leader, so it can never acquire a |
| controlling terminal again. This ensures its independence. |
| |
| When stop() is called, the process sends a SIGTERM to the running daemon |
| with the pid recorded in |pidfile| and terminates the daemon. |
| |
| To use Daemon, subclass it and overrides the set_up(), tear_down() and |
| optionally the loop() methods. |
| |
| Attributes: |
| log_path: The path for logging. |
| pidfile: The path to store the daemon pid. |
| state_file: The path to store the daemon state. |
| stdin: The path to redirect stdin to. Normally this should be /dev/null. |
| stdout: The path to redirect stdout to. |
| stderr: The path to redirect stderr to. |
| """ |
| |
| def __init__( |
| self, |
| pidfile: str, |
| state_file: str, |
| stdin: str = "/dev/null", |
| stdout: str = "/dev/null", |
| stderr: str = "/dev/null", |
| ): |
| self.stdin = stdin |
| self.stdout = stdout |
| self.stderr = stderr |
| self.pidfile = pidfile |
| self.state_file = state_file |
| |
| def start(self) -> None: |
| """Starts the daemon.""" |
| state = self.get_state() |
| if state != State.OFF: |
| logging.error( |
| "expected state %s but got state %s", |
| State.OFF.value, |
| state.value, |
| ) |
| sys.exit(1) |
| |
| # Start the daemon. |
| self._change_state_to(State.SET_UP) |
| self._daemonize() |
| signal.signal(signal.SIGTERM, self._on_sigterm) |
| if self.set_up(): |
| self._change_state_to(State.ON) |
| self.loop() |
| else: |
| logging.error("Failed to set up the daemon.") |
| self.stop() |
| # Sleep a while to make sure self.on_sigterm() is called. |
| time.sleep(1) |
| |
| def stop(self) -> None: |
| """Stops the daemon. |
| |
| Note that the process that runs stop() will be different from the |
| daemon. The process that runs stop() will sends SIGTERM to the |
| daemon process to kill it. |
| """ |
| state = self.get_state() |
| if state not in (State.SET_UP, State.ON): |
| logging.error( |
| "expected state %s or %s but got state %s", |
| State.SET_UP.value, |
| State.ON.value, |
| state.value, |
| ) |
| sys.exit(1) |
| |
| self._change_state_to(State.TEAR_DOWN) |
| try: |
| os.kill(self.read_pid(), signal.SIGTERM) |
| except (OSError, TypeError) as e: |
| logging.error(e) |
| |
| @abc.abstractmethod |
| def set_up(self) -> bool: |
| """Sets up the daemon. |
| |
| Override this method when subclassing Daemon. It will be called after |
| the process is daemonized. |
| |
| Returns: |
| A bool value indicates whether the setup is successful. |
| """ |
| |
| @abc.abstractmethod |
| def tear_down(self) -> None: |
| """Tears down before stopping. |
| |
| This method will be called before the daemon stops. Override this method |
| when subclassing Daemon. |
| """ |
| |
| def loop(self) -> NoReturn: |
| """An infinite loop for the daemon. |
| |
| Can be overridden to do customized things, such as daemon state check, |
| network health check, etc. |
| """ |
| while True: |
| pass |
| |
| def get_state(self) -> State: |
| """Gets the daemon state. |
| |
| Returns: |
| The daemon state enum. If the state file does not exist or records |
| invalid state, then returns State.OFF by default. |
| """ |
| try: |
| with open(self.state_file, "r", encoding="utf-8") as f: |
| state_str = f.read().strip() |
| except IOError: |
| return State.OFF |
| state = State.from_str(state_str) |
| if not state: |
| state = State.OFF |
| logging.warning( |
| "Read invalid state %s, treat it as %s", |
| state_str, |
| state.value, |
| ) |
| return state |
| |
| def read_pid(self) -> Optional[int]: |
| """Reads the daemon pid from the pid file. |
| |
| Returns: |
| The daemon pid. If the read fails, returns None. |
| """ |
| # Check the pidfile to see if the daemon already runs. |
| try: |
| with open(self.pidfile, "r", encoding="utf-8") as f: |
| pid = int(f.read().strip()) |
| except IOError: |
| pid = None |
| return pid |
| |
| def _daemonize(self) -> None: |
| """Daemonizes itself with double fork.""" |
| try: |
| pid = os.fork() |
| if pid > 0: |
| # Exit first parent. |
| sys.exit(0) |
| except OSError as e: |
| logging.error("Fork #1 failed: %s", e) |
| sys.exit(1) |
| |
| # Decouple from parent environment. |
| os.chdir("/") |
| os.setsid() |
| os.umask(0) |
| |
| # Do second fork. |
| try: |
| pid = os.fork() |
| if pid > 0: |
| # Exit from second parent. |
| sys.exit(0) |
| except OSError as e: |
| logging.error("Fork #2 failed: %s", e) |
| sys.exit(1) |
| |
| # Redirect standard file descriptors. |
| sys.stdout.flush() |
| sys.stderr.flush() |
| with open(self.stdin, "rb") as si: |
| os.dup2(si.fileno(), sys.stdin.fileno()) |
| with open(self.stdout, "ab", 0) as so: |
| os.dup2(so.fileno(), sys.stdout.fileno()) |
| with open(self.stderr, "ab", 0) as se: |
| os.dup2(se.fileno(), sys.stderr.fileno()) |
| |
| self._write_pid() |
| |
| def _write_pid(self) -> None: |
| """Writes the pid of the current process to the pid file.""" |
| with open(self.pidfile, "w", encoding="utf-8") as f: |
| f.write(f"{os.getpid()}\n") |
| |
| def _del_pid(self) -> None: |
| """Deletes the pid file.""" |
| try: |
| os.remove(self.pidfile) |
| except OSError: |
| pass |
| |
| def _on_sigterm(self, signum, frame) -> None: |
| """This method is used to handle the SIGTERM signal. |
| |
| It tears down the daemon, deletes the pid file, changes the daemon to |
| state off and terminates the process. |
| |
| The handler parameter in signal.signal() requires two arguments, but |
| we don't need to use these arguments, so we set the arguments to *_. |
| |
| Args: |
| signum: The number of the signal received. Unused. |
| frame: A frame object containing information about where the program |
| was interrupted. Unused. |
| """ |
| # Unused. |
| del signum, frame |
| self.tear_down() |
| self._del_pid() |
| self._change_state_to(State.OFF) |
| sys.exit(0) |
| |
| def _change_state_to(self, state: State) -> None: |
| """Changes the daemon state and writes to the state file. |
| |
| Args: |
| state: The new state. |
| """ |
| with open(self.state_file, "w", encoding="utf-8") as f: |
| f.write(f"{state.value}\n") |