use Friendica\Core\Update;
use Friendica\Core\Worker;
use Friendica\Database\Database;
+use Friendica\System\Daemon as SysDaemon;
use Friendica\Util\BasePath;
use Friendica\Util\DateTimeFormat;
use Psr\Log\LoggerInterface;
private System $system;
private LoggerInterface $logger;
private Database $dba;
+ private SysDaemon $daemon;
/**
* @param Mode $mode
* @param System $system
* @param LoggerInterface $logger
* @param Database $dba
+ * @param SysDaemon $daemon
* @param array|null $argv
*/
- public function __construct(Mode $mode, IManageConfigValues $config, IManageKeyValuePairs $keyValue, BasePath $basePath, System $system, LoggerInterface $logger, Database $dba, array $argv = null)
+ public function __construct(Mode $mode, IManageConfigValues $config, IManageKeyValuePairs $keyValue, BasePath $basePath, System $system, LoggerInterface $logger, Database $dba, SysDaemon $daemon, array $argv = null)
{
parent::__construct($argv);
$this->system = $system;
$this->logger = $logger;
$this->dba = $dba;
+ $this->daemon = $daemon;
}
protected function getHelp(): string
return <<<HELP
Daemon - Interact with the Friendica daemon
Synopsis
- bin/console daemon [-h|--help|-?] [-v] [-a] [-f]
+ bin/console daemon start [-h|--help|-?] [-v] [-f]
+ bin/console daemon stop [-h|--help|-?] [-v]
+ bin/console daemon status [-h|--help|-?] [-v]
Description
Interact with the Friendica daemon
'system' => [
'pidfile' => '/path/to/daemon.pid',
],
- TXT);
+ TXT
+ );
}
$pidfile = $this->config->get('system', 'pidfile');
$daemonMode = $this->getArgument(0);
- $foreground = $this->getOption(['f', 'foreground']);
+ $foreground = $this->getOption(['f', 'foreground']) ?? false;
if (empty($daemonMode)) {
throw new RuntimeException("Please use either 'start', 'stop' or 'status'");
}
- $pid = null;
- if (is_readable($pidfile)) {
- $pid = intval(file_get_contents($pidfile));
- }
-
- if (empty($pid) && in_array($daemonMode, ['stop', 'status'])) {
- $this->keyValue->set('worker_daemon_mode', false);
- throw new RuntimeException("Pidfile wasn't found. Is the daemon running?");
- }
+ $this->daemon->init($pidfile);
if ($daemonMode == 'status') {
- if (posix_kill($pid, 0)) {
- $this->out("Daemon process $pid is running");
- return 0;
+ if ($this->daemon->isRunning()) {
+ $this->out(sprintf("Daemon process %s is running (%s)", $this->daemon->getPid(), $this->daemon->getPidfile()));
+ } else {
+ $this->out(sprintf("Daemon process %s isn't running (%s)", $this->daemon->getPid(), $this->daemon->getPidfile()));
}
-
- unlink($pidfile);
-
- $this->keyValue->set('worker_daemon_mode', false);
- $this->out("Daemon process $pid isn't running.");
return 0;
}
if ($daemonMode == 'stop') {
- posix_kill($pid, SIGTERM);
- unlink($pidfile);
-
- $this->logger->notice('Worker daemon process was killed', ['pid' => $pid]);
-
- $this->keyValue->set('worker_daemon_mode', false);
- $this->out("Daemon process $pid was killed.");
- return 0;
- }
-
- $this->logger->notice('Starting worker daemon', ['pid' => $pid]);
-
- if (!$foreground) {
- $this->out("Starting worker daemon");
- $this->dba->disconnect();
-
- // Fork a daemon process
- $pid = pcntl_fork();
- if ($pid == -1) {
- $this->logger->warning('Could not fork daemon');
- throw new RuntimeException("Daemon couldn't be forked");
- } elseif ($pid) {
- // The parent process continues here
- if (!file_put_contents($pidfile, $pid)) {
- posix_kill($pid, SIGTERM);
- $this->logger->warning('Could not store pid file');
- throw new RuntimeException("Pid file wasn't written");
- }
- $this->out("Child process started with pid $pid");
- $this->logger->notice('Child process started', ['pid' => $pid]);
+ if (!$this->daemon->isRunning()) {
+ $this->out(sprintf("Daemon process %s isn't running (%s)", $this->daemon->getPid(), $this->daemon->getPidfile()));
return 0;
}
- // We now are in the child process
- register_shutdown_function(function () {
- posix_kill(posix_getpid(), SIGTERM);
- posix_kill(posix_getpid(), SIGHUP);
- });
-
- // Make the child the main process, detach it from the terminal
- if (posix_setsid() < 0) {
+ if ($this->daemon->stop()) {
+ $this->keyValue->set('worker_daemon_mode', false);
+ $this->out(sprintf("Daemon process %s was killed (%s)", $this->daemon->getPid(), $this->daemon->getPidfile()));
return 0;
}
- // Closing all existing connections with the outside
- fclose(STDIN);
+ return 1;
+ }
- // And now connect the database again
- $this->dba->connect();
+ if ($this->daemon->isRunning()) {
+ $this->out(sprintf("Daemon process %s is already running (%s)", $this->daemon->getPid(), $this->daemon->getPidfile()));
+ return 1;
}
- $this->keyValue->set('worker_daemon_mode', true);
+ if ($daemonMode == "start") {
+ $this->out("Starting worker daemon");
- // Just to be sure that this script really runs endlessly
- set_time_limit(0);
+ $this->daemon->start(function () {
+ $wait_interval = intval($this->config->get('system', 'cron_interval', 5)) * 60;
- $wait_interval = intval($this->config->get('system', 'cron_interval', 5)) * 60;
+ $do_cron = true;
+ $last_cron = 0;
- $do_cron = true;
- $last_cron = 0;
+ $path = $this->basePath->getPath();
- $path = $this->basePath->getPath();
+ // Now running as a daemon.
+ while (true) {
+ // Check the database structure and possibly fixes it
+ Update::check($path, true);
- // Now running as a daemon.
- while (true) {
- // Check the database structure and possibly fixes it
- Update::check($path, true);
+ if (!$do_cron && ($last_cron + $wait_interval) < time()) {
+ $this->logger->info('Forcing cron worker call.', ['pid' => $this->daemon->getPid()]);
+ $do_cron = true;
+ }
- if (!$do_cron && ($last_cron + $wait_interval) < time()) {
- $this->logger->info('Forcing cron worker call.', ['pid' => $pid]);
- $do_cron = true;
- }
+ if ($do_cron || (!$this->system->isMaxLoadReached() && Worker::entriesExists() && Worker::isReady())) {
+ Worker::spawnWorker($do_cron);
+ } else {
+ $this->logger->info('Cool down for 5 seconds', ['pid' => $this->daemon->getPid()]);
+ sleep(5);
+ }
- if ($do_cron || (!$this->system->isMaxLoadReached() && Worker::entriesExists() && Worker::isReady())) {
- Worker::spawnWorker($do_cron);
- } else {
- $this->logger->info('Cool down for 5 seconds', ['pid' => $pid]);
- sleep(5);
- }
+ if ($do_cron) {
+ // We force a reconnect of the database connection.
+ // This is done to ensure that the connection don't get lost over time.
+ $this->dba->reconnect();
- if ($do_cron) {
- // We force a reconnect of the database connection.
- // This is done to ensure that the connection don't get lost over time.
- $this->dba->reconnect();
+ $last_cron = time();
+ }
- $last_cron = time();
- }
+ $start = time();
+ $this->logger->info('Sleeping', ['pid' => $this->daemon->getPid(), 'until' => gmdate(DateTimeFormat::MYSQL, $start + $wait_interval)]);
- $start = time();
- $this->logger->info('Sleeping', ['pid' => $pid, 'until' => gmdate(DateTimeFormat::MYSQL, $start + $wait_interval)]);
+ do {
+ $seconds = (time() - $start);
- do {
- $seconds = (time() - $start);
+ // logarithmic wait time calculation.
+ // Background: After jobs had been started, they often fork many workers.
+ // To not waste too much time, the sleep period increases.
+ $arg = (($seconds + 1) / ($wait_interval / 9)) + 1;
+ $sleep = min(1000000, round(log10($arg) * 1000000, 0));
- // logarithmic wait time calculation.
- // Background: After jobs had been started, they often fork many workers.
- // To not waste too much time, the sleep period increases.
- $arg = (($seconds + 1) / ($wait_interval / 9)) + 1;
- $sleep = min(1000000, round(log10($arg) * 1000000, 0));
- usleep((int)$sleep);
+ $this->daemon->sleep((int)$sleep);
- $pid = pcntl_waitpid(-1, $status, WNOHANG);
- if ($pid > 0) {
- $this->logger->info('Children quit via pcntl_waitpid', ['pid' => $pid, 'status' => $status]);
- }
+ $timeout = ($seconds >= $wait_interval);
+ } while (!$timeout && !Worker\IPC::JobsExists());
- $timeout = ($seconds >= $wait_interval);
- } while (!$timeout && !Worker\IPC::JobsExists());
+ if ($timeout) {
+ $do_cron = true;
+ $this->logger->info('Woke up after $wait_interval seconds.', ['pid' => $this->daemon->getPid(), 'sleep' => $wait_interval]);
+ } else {
+ $do_cron = false;
+ $this->logger->info('Worker jobs are calling to be forked.', ['pid' => $this->daemon->getPid()]);
+ }
+ }
+ }, $foreground);
- if ($timeout) {
- $do_cron = true;
- $this->logger->info('Woke up after $wait_interval seconds.', ['pid' => $pid, 'sleep' => $wait_interval]);
- } else {
- $do_cron = false;
- $this->logger->info('Worker jobs are calling to be forked.', ['pid' => $pid]);
- }
+ return 0;
}
+
+ $this->err('Invalid command');
+ $this->out($this->getHelp());
+ return 1;
}
}
--- /dev/null
+<?php
+
+namespace Friendica\System;
+
+use Friendica\Database\Database;
+use Psr\Log\LoggerInterface;
+
+final class Daemon
+{
+ private LoggerInterface $logger;
+ private Database $dba;
+ private ?string $pidfile = null;
+ private ?int $pid = null;
+
+ public function getPid(): ?int
+ {
+ return $this->pid;
+ }
+
+ public function getPidfile(): ?string
+ {
+ return $this->pidfile;
+ }
+
+ public function __construct(LoggerInterface $logger, Database $dba)
+ {
+ $this->logger = $logger;
+ $this->dba = $dba;
+ }
+
+ public function init($pidfile = null): void
+ {
+ if (!empty($pidfile)) {
+ $this->pid = null;
+ $this->pidfile = $pidfile;
+ }
+
+ if (!empty($this->pid)) {
+ return;
+ }
+
+ if (is_readable($this->pidfile)) {
+ $this->pid = intval(file_get_contents($this->pidfile));
+ }
+ }
+
+ public function start(callable $daemonLogic, bool $foreground = false): bool
+ {
+ $this->init();
+
+ if (!empty($this->pid)) {
+ $this->logger->notice('process is already running', ['pid' => $this->pid, 'pidfile' => $this->pidfile]);
+ return false;
+ }
+
+ $this->logger->notice('starting daemon', ['pid' => $this->pid, 'pidfile' => $this->pidfile]);
+
+ if (!$foreground) {
+ $this->dba->disconnect();
+
+ // fork a daemon process
+ $this->pid = pcntl_fork();
+ if ($this->pid < 0) {
+ $this->logger->warning('Could not fork daemon');
+ return false;
+ } elseif ($this->pid) {
+ // The parent process continues here
+ if (!file_put_contents($this->pidfile, $this->pid)) {
+ $this->logger->warning('Could not store pid file', ['pid' => $this->pid, 'pidfile' => $this->pidfile]);
+ posix_kill($this->pid, SIGTERM);
+ return false;
+ }
+ $this->logger->notice('Child process started', ['pid' => $this->pid, 'pidfile' => $this->pidfile]);
+ return true;
+ }
+
+ // We now are in the child process
+ register_shutdown_function(function (): void {
+ posix_kill(posix_getpid(), SIGTERM);
+ posix_kill(posix_getpid(), SIGHUP);
+ });
+
+ // Make the child the main process, detach it from the terminal
+ if (posix_setsid() < 0) {
+ return true;
+ }
+
+ // Closing all existing connections with the outside
+ fclose(STDIN);
+
+ // And now connect the database again
+ $this->dba->connect();
+ }
+
+ // Just to be sure that this script really runs endlessly
+ set_time_limit(0);
+
+ $daemonLogic();
+
+ return true;
+ }
+
+ public function isRunning(): bool
+ {
+ $this->init();
+
+ if (empty($this->pid)) {
+ $this->logger->notice("Pid wasn't found");
+
+ if (is_readable($this->pidfile)) {
+ unlink($this->pidfile);
+ $this->logger->notice("Pidfile removed", ['pidfile' => $this->pidfile]);
+ }
+ return false;
+ }
+
+ if (posix_kill($this->pid, 0)) {
+ $this->logger->notice("daemon process is running");
+ return true;
+ } else {
+ unlink($this->pidfile);
+ $this->logger->notice("daemon process isn't running");
+ return false;
+ }
+ }
+
+ public function stop(): bool
+ {
+ $this->init();
+
+ if (empty($this->pid)) {
+ $this->logger->notice("Pidfile wasn't found", ['pidfile' => $this->pidfile]);
+ return true;
+ }
+
+ posix_kill($this->pid, SIGTERM);
+ unlink($this->pidfile);
+
+ $this->logger->notice('daemon process was killed', ['pid' => $this->pid, 'pidfile' => $this->pidfile]);
+
+ return true;
+ }
+
+ public function sleep(int $duration)
+ {
+ usleep($duration);
+
+ $this->pid = pcntl_waitpid(-1, $status, WNOHANG);
+ if ($this->pid > 0) {
+ $this->logger->info('Children quit via pcntl_waitpid', ['pid' => $this->pid, 'status' => $status]);
+ }
+ }
+}