From 2c7b16ed27f40d0f9c78b48d46e3c2d520961a6a Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 6 Jun 2021 18:54:24 -0400 Subject: [PATCH] Respond to termination signals and delete PID file --- lib/CLI.php | 26 +++++++++++++++++++++++++- lib/Service.php | 28 ++++++++++++++++++++++++---- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/lib/CLI.php b/lib/CLI.php index 5b3614d8..825441e2 100644 --- a/lib/CLI.php +++ b/lib/CLI.php @@ -94,10 +94,14 @@ USAGE_TEXT; return 0; case "daemon": if ($args['--fork'] !== null) { - $this->fork($args['--fork']); + $pidfile = $this->resolvePID($args['--fork']); + $this->fork($pidfile); } $this->loadConf(); Arsse::$obj->get(Service::class)->watch(true); + if (isset($pidfile)) { + unlink($pidfile); + } return 0; case "feed refresh": return (int) !Arsse::$db->feedUpdate((int) $args[''], true); @@ -282,6 +286,7 @@ USAGE_TEXT; default: fclose($pipe[1]); fread($pipe[0], 100); + fclose($pipe[0]); # Call exit() in the original process. The process that invoked the daemon must be able to rely on that this exit() happens after initialization is complete and all external communication channels are established and accessible. exit; } @@ -320,4 +325,23 @@ USAGE_TEXT; } } } + + /** Resolves the PID file path and ensures the file or parent directory is writable */ + protected function resolvePID(string $pidfile): string { + $dir = dirname($pidfile); + $file = basename($pidfile); + if ($base = @realpath($dir)) { + $out = "$base/$file"; + if (file_exists($out)) { + if (!is_writable($out)) { + throw new \Exception("PID file is not writable"); + } + } elseif (!is_writable($base)) { + throw new \Exception("Cannot create PID file"); + } + } else { + throw new \Exception("Parent directory of PID file does not exist"); + } + return $out; + } } diff --git a/lib/Service.php b/lib/Service.php index a69b12c0..beeaced9 100644 --- a/lib/Service.php +++ b/lib/Service.php @@ -16,6 +16,7 @@ class Service { /** @var Service\Driver */ protected $drv; + protected $loop = false; public function __construct() { $driver = Arsse::$conf->serviceDriver; @@ -23,6 +24,8 @@ class Service { } public function watch(bool $loop = true): \DateTimeInterface { + $this->loop = $loop; + $this->signalInit(); $t = new \DateTime(); do { $this->checkIn(); @@ -37,13 +40,14 @@ class Service { static::cleanupPost(); $t->add(Arsse::$conf->serviceFrequency); // @codeCoverageIgnoreStart - if ($loop) { + if ($this->loop) { do { - @time_sleep_until($t->getTimestamp()); - } while ($t->getTimestamp() > time()); + sleep((int) max(0, $t->getTimestamp() - time())); + pcntl_signal_dispatch(); + } while ($this->loop && $t->getTimestamp() > time()); } // @codeCoverageIgnoreEnd - } while ($loop); + } while ($this->loop); return $t; } @@ -88,4 +92,20 @@ class Service { } return true; } + + protected function signalInit(): void { + if (function_exists("pcntl_async_signals") && function_exists("pcntl_signal")) { + // receive asynchronous signals if supported + pcntl_async_signals(true); + foreach ([\SIGABRT, \SIGINT, \SIGTERM] as $sig) { + pcntl_signal($sig, [$this, "sigTerm"]); + } + } + } + + protected function sigTerm(int $signo): void { + $this->loop = false; + } + + }