Source code for pyrolab.pyrolabd
# Copyright © PyroLab Project Contributors
# Licensed under the terms of the GNU GPLv3+ License
# (see pyrolab/__init__.py for details)
"""
PyroLab Daemon
===============
Submodule defining the background PyroLab daemon.
"""
import logging
import os
import shutil
from typing import NamedTuple
import Pyro5.api as api
from pydantic import BaseModel
from tabulate import tabulate
from pyrolab import LOCKFILE, RUNTIME_CONFIG, USER_CONFIG_FILE
from pyrolab.configure import GlobalConfiguration
from pyrolab.manager import ProcessManager
log = logging.getLogger("pyrolab.pyrolabd")
[docs]
class InstanceInfo(BaseModel):
"""
Model for storing information about a running instance.
"""
pid: int
uri: str
[docs]
class NameServerInfo(NamedTuple):
"""
Named tuple for storing information about a running nameserver.
"""
name: str
created: str
status: str
uri: str
[docs]
class DaemonInfo(NamedTuple):
"""
Named tuple for storing information about a running daemon.
"""
name: str
created: str
status: str
uri: str
[docs]
class PSInfo(NamedTuple):
"""
Named tuple for storing information about a running service.
"""
name: str
daemon: str
uri: str
[docs]
@api.expose
@api.behavior(instance_mode="single")
class PyroLabDaemon:
"""
The PyroLab daemon runs continuously in the background.
The daemon and controls all PyroLab entities through the PyroLabManager
singleton. The main purpose of the daemon is to listen for requests and
commands, usually sent through the command line interface (CLI).
No script should ever need to import or instantiate the PyroLabDaemon.
To preserve its "single instance" nature, the daemon should only be created
and run through the CLI (which in turn, runs this module as a script).
Limiting daemon manipulation to the CLI guarantees that only one daemon
will be running at any given time (courtesy of the Lockfile this script
creates and checks).
By default, the daemon will load the user configuration file (manipulatable
via the CLI) and write a runtime configuration file (not manipulatable via
the CLI). The daemon will not change its configuration unless a call to the
:py:func:`reload` method is made, usually by the CLI. Even if the user
configuration file is changed, the daemon will not reload unless explictly
instructed to do so. It is therefore of the utmost importance that the
runtime configuration file be managed solely by the daemon! No touchy!
.. note::
As a Pyro5 object, no method of the daemon should return any types other
than Python builtins, due to serialization issues.
"""
def __init__(self):
log.info("Starting PyroLab daemon.")
self.manager = ProcessManager.instance()
if USER_CONFIG_FILE.exists():
self.gconfig = GlobalConfiguration.instance()
self.gconfig.load_config(USER_CONFIG_FILE)
self.gconfig.save_config(RUNTIME_CONFIG)
else:
self.gconfig = GlobalConfiguration.instance()
log.info("Autolaunching PyroLab entities.")
autodetails = self.gconfig.config.autolaunch
for ns in autodetails.nameservers:
self.start_nameserver(ns)
for daemon in autodetails.daemons:
self.start_daemon(daemon)
log.info("PyroLab background daemon started.")
[docs]
def reload(self) -> bool:
"""
Reloads the latest configuration file and restarts services that were
running.
Returns
-------
bool
True if the reload was successful, False otherwise.
"""
log.debug("Daemon reload requested.")
shutil.copy(USER_CONFIG_FILE, RUNTIME_CONFIG)
self.gconfig.load_config(RUNTIME_CONFIG)
return self.manager.reload()
[docs]
def whoami(self) -> str:
"""
Returns the object ID of the daemon, and it's PID number.
"""
return f"{id(self)} at {os.getpid()}"
[docs]
def ps(self) -> str:
"""
List all known processes grouped as nameservers, daemons, and services.
Lists process names, status (i.e. running, stopped, etc.), start time,
URI/ports, etc.
"""
log.debug("Daemon process listing requested.")
listing = []
for ns in self.gconfig.get_config().nameservers.keys():
info = self.manager.get_nameserver_process_info(ns)
listing.append(NameServerInfo(name=ns, **info))
nsstring = tabulate(listing, headers=["NAMESERVER", "CREATED", "STATUS", "URI"])
listing = []
for daemon in self.gconfig.get_config().daemons.keys():
info = self.manager.get_daemon_process_info(daemon)
listing.append(DaemonInfo(name=daemon, **info))
daemonstring = tabulate(listing, headers=["DAEMON", "CREATED", "STATUS", "URI"])
listing = []
for service in self.gconfig.get_config().services.keys():
info = self.manager.get_service_process_info(service)
listing.append(PSInfo(service, **info))
servicestring = tabulate(listing, headers=["SERVICE", "DAEMON", "URI"])
return f"\n{nsstring}\n\n{daemonstring}\n\n{servicestring}\n"
[docs]
def start_nameserver(self, nameserver: str) -> None:
"""
Starts a nameserver.
Parameters
----------
nameserver : str
The name of the nameserver to start.
"""
log.debug(f"Starting nameserver '{nameserver}'.")
self.manager.launch_nameserver(nameserver)
[docs]
def start_daemon(self, daemon: str) -> None:
"""
Starts a daemon.
Parameters
----------
daemon : str
The name of the daemon to start.
"""
log.debug(f"Starting daemon '{daemon}'.")
self.manager.launch_daemon(daemon)
[docs]
def stop_nameserver(self, nameserver: str) -> None:
"""
Stops a nameserver.
Parameters
----------
nameserver : str
The name of the nameserver to stop.
"""
log.debug(f"Stopping nameserver '{nameserver}'.")
self.manager.shutdown_nameserver(nameserver)
[docs]
def stop_daemon(self, daemon: str) -> None:
"""
Stops a daemon.
Parameters
----------
daemon : str
The name of the daemon to stop.
"""
log.debug(f"Stopping daemon '{daemon}'.")
self.manager.shutdown_daemon(daemon)
[docs]
def restart_nameserver(self, name: str) -> None:
"""
Restarts a nameserver.
Parameters
----------
name : str
The name of the nameserver to restart.
"""
log.debug(f"Restarting nameserver '{name}'.")
self.manager.shutdown_nameserver(name)
self.manager.launch_nameserver(name)
[docs]
def restart_daemon(self, name: str) -> None:
"""
Restarts a daemon.
Parameters
----------
name : str
The name of the daemon to restart.
"""
log.debug(f"Restarting daemon '{name}'.")
self.manager.shutdown_daemon(name)
self.manager.launch_daemon(name)
[docs]
@api.oneway
def shutdown(self) -> None:
"""
Shuts down the daemon.
This method does not return a confirmation since, by nature of the
shutdown request, the daemon will not be able to respond.
"""
log.info("Daemon shutdown requested.")
self.manager.shutdown_all()
self._pyroDaemon.shutdown()
log.info("Daemon shutdown complete.")
if __name__ == "__main__":
if LOCKFILE.exists():
raise RuntimeError(f"Lockfile already exists. Is another instance running?")
else:
try:
LOCKFILE.touch(exist_ok=False)
import sys
if len(sys.argv) > 1:
port = int(sys.argv[1])
else:
port = 0
daemon = api.Daemon(port=port)
pyrolabd = PyroLabDaemon()
uri = daemon.register(pyrolabd, "pyrolabd")
ii = InstanceInfo(pid=os.getpid(), uri=str(uri))
with LOCKFILE.open("w") as f:
f.write(ii.json())
daemon.requestLoop()
finally:
LOCKFILE.unlink()
try:
RUNTIME_CONFIG.unlink()
except FileNotFoundError:
print("Runtime configuration not found (and therefore not removed).")