From b0ec3ad0c96eae35d8d889c947ab5f69ee74d978 Mon Sep 17 00:00:00 2001 From: Philipp Date: Mon, 30 Dec 2024 12:40:42 +0100 Subject: [PATCH] Move daemon to Console Command --- bin/daemon.php | 8 +- src/App.php | 218 ----------------------------------- src/Console/Daemon.php | 256 +++++++++++++++++++++++++++++++++++++++++ src/Core/Console.php | 2 + 4 files changed, 262 insertions(+), 222 deletions(-) create mode 100644 src/Console/Daemon.php diff --git a/bin/daemon.php b/bin/daemon.php index 06e8574cae..8f567edfb7 100755 --- a/bin/daemon.php +++ b/bin/daemon.php @@ -19,9 +19,6 @@ if (php_sapi_name() !== 'cli') { use Dice\Dice; -// Get options -$options = getopt('f', ['foreground']); - // Ensure that daemon.php is executed from the base path of the installation chdir(dirname(__DIR__)); @@ -31,4 +28,7 @@ $dice = (new Dice())->addRules(require(dirname(__DIR__) . '/static/dependencies. $app = \Friendica\App::fromDice($dice); -$app->processDaemon($_SERVER['argv'] ?? [], $options ?: []); +$argv = $_SERVER['argv'] ?? []; +array_splice($argv, 1, 0, "daemon"); + +$app->processConsole($argv); diff --git a/src/App.php b/src/App.php index 15d2cd9540..c24215e37d 100644 --- a/src/App.php +++ b/src/App.php @@ -20,14 +20,12 @@ use Friendica\Content\Nav; use Friendica\Core\Addon; use Friendica\Core\Config\Factory\Config; use Friendica\Core\Hook; -use Friendica\Core\KeyValueStorage\Capability\IManageKeyValuePairs; use Friendica\Core\Renderer; use Friendica\Core\Session\Capability\IHandleUserSessions; use Friendica\Core\Worker\Repository\Process as ProcessRepository; use Friendica\Database\DBA; use Friendica\Database\Definition\DbaDefinition; use Friendica\Database\Definition\ViewDefinition; -use Friendica\DI; use Friendica\Module\Maintenance; use Friendica\Protocol\ATProtocol\Jetstream; use Friendica\Security\Authentication; @@ -222,222 +220,6 @@ class App (new \Friendica\Core\Console($this->container, $argv))->execute(); } - public function processDaemon(array $argv, array $options): void - { - $this->setupContainerForAddons(); - - $this->setupContainerForLogger(LogChannel::DAEMON); - - $this->setupLegacyServiceLocator(); - - $this->registerErrorHandler(); - - /** @var Mode */ - $mode = $this->container->create(Mode::class); - - if ($mode->isInstall()) { - die("Friendica isn't properly installed yet.\n"); - } - - $mode->setExecutor(Mode::DAEMON); - - /** @var IManageConfigValues */ - $config = $this->container->create(IManageConfigValues::class); - - $config->reload(); - - if (empty($config->get('system', 'pidfile'))) { - die(<<< TXT - Please set system.pidfile in config/local.config.php. For example: - - 'system' => [ - 'pidfile' => '/path/to/daemon.pid', - ], - TXT - ); - } - - $pidfile = $config->get('system', 'pidfile'); - - if (in_array('start', $argv)) { - $daemonMode = 'start'; - } - - if (in_array('stop', $argv)) { - $daemonMode = 'stop'; - } - - if (in_array('status', $argv)) { - $daemonMode = 'status'; - } - - $foreground = array_key_exists('f', $options) || array_key_exists('foreground', $options); - - if (!isset($daemonMode)) { - die("Please use either 'start', 'stop' or 'status'.\n"); - } - - if (empty($argv[0])) { - die("Unexpected script behaviour. This message should never occur.\n"); - } - - $pid = null; - - if (is_readable($pidfile)) { - $pid = intval(file_get_contents($pidfile)); - } - - /** @var IManageKeyValuePairs */ - $keyValue = $this->container->create(IManageKeyValuePairs::class); - - if (empty($pid) && in_array($daemonMode, ['stop', 'status'])) { - $keyValue->set('worker_daemon_mode', false); - die("Pidfile wasn't found. Is the daemon running?\n"); - } - - if ($daemonMode == 'status') { - if (posix_kill($pid, 0)) { - die("Daemon process $pid is running.\n"); - } - - unlink($pidfile); - - $keyValue->set('worker_daemon_mode', false); - die("Daemon process $pid isn't running.\n"); - } - - if ($daemonMode == 'stop') { - posix_kill($pid, SIGTERM); - - unlink($pidfile); - - Logger::notice('Worker daemon process was killed', ['pid' => $pid]); - - $keyValue->set('worker_daemon_mode', false); - die("Worker daemon process $pid was killed.\n"); - } - - if (!empty($pid) && posix_kill($pid, 0)) { - die("Daemon process $pid is already running.\n"); - } - - Logger::notice('Starting worker daemon.', ['pid' => $pid]); - - if (!$foreground) { - echo "Starting worker daemon.\n"; - - DBA::disconnect(); - - // Fork a daemon process - $pid = pcntl_fork(); - if ($pid == -1) { - echo "Daemon couldn't be forked.\n"; - Logger::warning('Could not fork daemon'); - exit(1); - } elseif ($pid) { - // The parent process continues here - if (!file_put_contents($pidfile, $pid)) { - echo "Pid file wasn't written.\n"; - Logger::warning('Could not store pid file'); - posix_kill($pid, SIGTERM); - exit(1); - } - echo 'Child process started with pid ' . $pid . ".\n"; - Logger::notice('Child process started', ['pid' => $pid]); - exit(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) { - return; - } - - // Closing all existing connections with the outside - fclose(STDIN); - - // And now connect the database again - DBA::connect(); - } - - $keyValue->set('worker_daemon_mode', true); - - // Just to be sure that this script really runs endlessly - set_time_limit(0); - - $wait_interval = intval($config->get('system', 'cron_interval', 5)) * 60; - - $do_cron = true; - $last_cron = 0; - - /** @var BasePath */ - $basePath = $this->container->create(BasePath::class); - $path = $basePath->getPath(); - - /** @var System */ - $system = $this->container->create(System::class); - - // 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()) { - Logger::info('Forcing cron worker call.', ['pid' => $pid]); - $do_cron = true; - } - - if ($do_cron || (!$system->isMaxLoadReached() && Worker::entriesExists() && Worker::isReady())) { - Worker::spawnWorker($do_cron); - } else { - 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. - DBA::reconnect(); - - $last_cron = time(); - } - - $start = time(); - Logger::info('Sleeping', ['pid' => $pid, 'until' => gmdate(DateTimeFormat::MYSQL, $start + $wait_interval)]); - - 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)); - usleep($sleep); - - $pid = pcntl_waitpid(-1, $status, WNOHANG); - if ($pid > 0) { - Logger::info('Children quit via pcntl_waitpid', ['pid' => $pid, 'status' => $status]); - } - - $timeout = ($seconds >= $wait_interval); - } while (!$timeout && !Worker\IPC::JobsExists()); - - if ($timeout) { - $do_cron = true; - Logger::info('Woke up after $wait_interval seconds.', ['pid' => $pid, 'sleep' => $wait_interval]); - } else { - $do_cron = false; - Logger::info('Worker jobs are calling to be forked.', ['pid' => $pid]); - } - } - } - public function processJetstream(): void { $this->setupContainerForAddons(); diff --git a/src/Console/Daemon.php b/src/Console/Daemon.php new file mode 100644 index 0000000000..5f45f81fad --- /dev/null +++ b/src/Console/Daemon.php @@ -0,0 +1,256 @@ +mode = $mode; + $this->config = $config; + $this->keyValue = $keyValue; + $this->basePath = $basePath; + $this->system = $system; + $this->logger = $logger; + $this->dba = $dba; + } + + protected function getHelp(): string + { + return <<mode->isInstall()) { + throw new RuntimeException("Friendica isn't properly installed yet"); + } + + $this->mode->setExecutor(Mode::DAEMON); + + $this->config->reload(); + + if (empty($this->config->get('system', 'pidfile'))) { + throw new RuntimeException(<<< TXT + Please set system.pidfile in config/local.config.php. For example: + + 'system' => [ + 'pidfile' => '/path/to/daemon.pid', + ], + TXT); + } + + $pidfile = $this->config->get('system', 'pidfile'); + + $daemonMode = $this->getArgument(0); + $foreground = $this->getOption(['f', 'foreground']); + + 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?"); + } + + if ($daemonMode == 'status') { + if (posix_kill($pid, 0)) { + $this->out("Daemon process $pid is running"); + return 0; + } + + 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]); + 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) { + return 0; + } + + // Closing all existing connections with the outside + fclose(STDIN); + + // And now connect the database again + $this->dba->connect(); + } + + $this->keyValue->set('worker_daemon_mode', true); + + // Just to be sure that this script really runs endlessly + set_time_limit(0); + + $wait_interval = intval($this->config->get('system', 'cron_interval', 5)) * 60; + + $do_cron = true; + $last_cron = 0; + + $path = $this->basePath->getPath(); + + // 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' => $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' => $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(); + + $last_cron = time(); + } + + $start = time(); + $this->logger->info('Sleeping', ['pid' => $pid, 'until' => gmdate(DateTimeFormat::MYSQL, $start + $wait_interval)]); + + 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)); + usleep((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()); + + 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]); + } + } + } +} diff --git a/src/Core/Console.php b/src/Core/Console.php index e957e9c1a1..75ff6453b2 100644 --- a/src/Core/Console.php +++ b/src/Core/Console.php @@ -37,6 +37,7 @@ Commands: config Edit site config contact Contact management createdoxygen Generate Doxygen headers + daemon Interacting with the Friendica daemons dbstructure Do database updates docbloxerrorchecker Check the file tree for DocBlox errors extract Generate translation string file for the Friendica project (deprecated) @@ -75,6 +76,7 @@ HELP; 'config' => Friendica\Console\Config::class, 'contact' => Friendica\Console\Contact::class, 'createdoxygen' => Friendica\Console\CreateDoxygen::class, + 'daemon' => Friendica\Console\Daemon::class, 'docbloxerrorchecker' => Friendica\Console\DocBloxErrorChecker::class, 'dbstructure' => Friendica\Console\DatabaseStructure::class, 'extract' => Friendica\Console\Extract::class,