Skip to content

Commit

Permalink
Implemented --pid-file to write the process id to a file (#327)
Browse files Browse the repository at this point in the history
* Implemented --pid to write the process id to a file

* Minor refactor

* Fix PID flow
in case of existing process from same file

---------

Co-authored-by: Giovanni Barillari <[email protected]>
  • Loading branch information
stefins and gi0baro committed Jul 4, 2024
1 parent 6efcf3c commit 0a85e88
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 6 deletions.
11 changes: 9 additions & 2 deletions granian/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import click

from .constants import HTTPModes, Interfaces, Loops, ThreadModes
from .errors import ConfigurationError
from .errors import FatalError
from .http import HTTP1Settings, HTTP2Settings
from .log import LogLevels
from .server import Granian
Expand Down Expand Up @@ -198,6 +198,11 @@ def option(*param_decls: str, cls: Optional[Type[click.Option]] = None, **attrs:
'--process-name',
help='Set a custom name for processes (requires granian[pname] extra)',
)
@option(
'--pid-file',
type=click.Path(exists=False, file_okay=True, dir_okay=False, writable=True, path_type=pathlib.Path),
help='A path to write the PID file to',
)
@click.version_option(message='%(prog)s %(version)s')
def cli(
app: str,
Expand Down Expand Up @@ -238,6 +243,7 @@ def cli(
respawn_interval: float,
reload: bool,
process_name: Optional[str],
pid_file: Optional[pathlib.Path],
) -> None:
log_dictconfig = None
if log_config:
Expand Down Expand Up @@ -289,11 +295,12 @@ def cli(
respawn_interval=respawn_interval,
reload=reload,
process_name=process_name,
pid_file=pid_file,
)

try:
server.serve()
except ConfigurationError:
except FatalError:
raise click.exceptions.Exit(1)


Expand Down
10 changes: 9 additions & 1 deletion granian/errors.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
class ConfigurationError(Exception):
class FatalError(Exception):
...


class ConfigurationError(FatalError):
...


class PidFileError(FatalError):
...
58 changes: 55 additions & 3 deletions granian/server.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import contextvars
import errno
import multiprocessing
import os
import signal
Expand All @@ -19,7 +20,7 @@
from ._internal import load_target
from .asgi import LifespanProtocol, _callback_wrapper as _asgi_call_wrap
from .constants import HTTPModes, Interfaces, Loops, ThreadModes
from .errors import ConfigurationError
from .errors import ConfigurationError, PidFileError
from .http import HTTP1Settings, HTTP2Settings
from .log import DEFAULT_ACCESSLOG_FMT, LogLevels, configure_logging, logger
from .net import SocketHolder
Expand Down Expand Up @@ -95,6 +96,7 @@ def __init__(
respawn_interval: float = 3.5,
reload: bool = False,
process_name: Optional[str] = None,
pid_file: Optional[Path] = None,
):
self.target = target
self.bind_addr = address
Expand Down Expand Up @@ -128,6 +130,7 @@ def __init__(
self.reload_on_changes = reload
self.respawn_interval = respawn_interval
self.process_name = process_name
self.pid_file = pid_file

configure_logging(self.log_level, self.log_config, self.log_enabled)

Expand All @@ -140,6 +143,7 @@ def __init__(
self.interrupt_children = []
self.respawned_procs = {}
self.reload_signal = False
self.pid = None

def build_ssl_context(self, cert: Optional[Path], key: Optional[Path]):
if not (cert and key):
Expand Down Expand Up @@ -461,9 +465,56 @@ def setup_signals(self):
if sys.platform != 'win32':
signal.signal(signal.SIGHUP, self.signal_handler_reload)

def startup(self, spawn_target, target_loader):
logger.info(f'Starting granian (main PID: {os.getpid()})')
def _write_pid(self):
with self.pid_file.open('w') as pid_file:
pid_file.write(str(self.pid))

def _write_pidfile(self):
if not self.pid_file:
return

existing_pid = None

if self.pid_file.exists():
try:
with self.pid_file.open('r') as pid_file:
existing_pid = int(pid_file.read())
except Exception:
logger.error(f'Unable to read existing PID file {self.pid_file}')
raise PidFileError

if existing_pid is not None and existing_pid != self.pid:
existing_process = True
try:
os.kill(existing_pid, 0)
except OSError as e:
if e.args[0] == errno.ESRCH:
existing_process = False

if existing_process:
logger.error(f'The PID file {self.pid_file} already exists for {existing_pid}')
raise PidFileError

self._write_pid()

def _unlink_pidfile(self):
if not (self.pid_file and self.pid_file.exists()):
return

try:
with self.pid_file.open('r') as pid_file:
file_pid = int(pid_file.read())
except Exception:
logger.error(f'Unable to read PID file {self.pid_file}')
return

if file_pid == self.pid:
self.pid_file.unlink()

def startup(self, spawn_target, target_loader):
self.pid = os.getpid()
logger.info(f'Starting granian (main PID: {self.pid})')
self._write_pidfile()
self.setup_signals()
self._init_shared_socket()
sock = socket.socket(fileno=self._sfd)
Expand All @@ -477,6 +528,7 @@ def startup(self, spawn_target, target_loader):
def shutdown(self, exit_code=0):
logger.info('Shutting down granian')
self._stop_workers()
self._unlink_pidfile()
if not exit_code and self.interrupt_children:
exit_code = 1
if exit_code:
Expand Down

0 comments on commit 0a85e88

Please sign in to comment.