diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml
index df8b4d89e8bec..08de4aaff0bbf 100644
--- a/build/psalm-baseline.xml
+++ b/build/psalm-baseline.xml
@@ -3073,13 +3073,6 @@
-
-
- ['message' => 'Background jobs disabled!']])]]>
- ['message' => 'Backgroundjobs are using system cron!']])]]>
-
-
-
diff --git a/core/Service/CronService.php b/core/Service/CronService.php
new file mode 100644
index 0000000000000..d5b4034a3387c
--- /dev/null
+++ b/core/Service/CronService.php
@@ -0,0 +1,246 @@
+verboseCallback = $callback;
+ }
+
+ /**
+ * @throws \RuntimeException
+ */
+ public function run(?array $jobClasses): void {
+ if (Util::needUpgrade()) {
+ $this->logger->debug('Update required, skipping cron', ['app' => 'core']);
+ return;
+ }
+
+ if ($this->config->getSystemValueBool('maintenance', false)) {
+ $this->logger->debug('We are in maintenance mode, skipping cron', ['app' => 'core']);
+ return;
+ }
+
+ // Don't do anything if Nextcloud has not been installed
+ if (!$this->config->getSystemValueBool('installed', false)) {
+ return;
+ }
+
+ // load all apps to get all api routes properly setup
+ $this->appManager->loadApps();
+ $this->session->close();
+
+ // initialize a dummy memory session
+ $session = new Memory();
+ $session = $this->cryptoWrapper->wrapSession($session);
+ $this->sessionStorage->setSession($session);
+ $this->userSession->setSession($session);
+ $this->store->setSession($session);
+
+ $this->tempManager->cleanOld();
+
+ // Exit if background jobs are disabled!
+ $appMode = $this->appConfig->getValueString('core', 'backgroundjobs_mode', 'ajax');
+ if ($appMode === 'none') {
+ throw new \RuntimeException('Background Jobs are disabled!');
+ }
+
+ if ($this->isCLI) {
+ $this->runCli($appMode, $jobClasses);
+ } else {
+ $this->runWeb($appMode);
+ }
+
+ // Log the successful cron execution
+ $this->appConfig->setValueInt('core', 'lastcron', time());
+ }
+
+ /**
+ * @throws \RuntimeException
+ */
+ private function runCli(string $appMode, ?array $jobClasses): void {
+ // set to run indefinitely if needed
+ if (!str_contains(@ini_get('disable_functions'), 'set_time_limit')) {
+ @set_time_limit(0);
+ }
+
+ // the cron job must be executed with the right user
+ if (!function_exists('posix_getuid')) {
+ throw new \RuntimeException('The posix extensions are required - see https://www.php.net/manual/en/book.posix.php');
+ }
+
+ $user = posix_getuid();
+ $configUser = fileowner(OC::$configDir . 'config.php');
+ if ($user !== $configUser) {
+ throw new \RuntimeException('Console has to be executed with the user that owns the file config/config.php.' . PHP_EOL . 'Current user id: ' . $user . PHP_EOL . 'Owner id of config.php: ' . $configUser . PHP_EOL);
+ }
+
+ // We call Nextcloud from the CLI (aka cron)
+ if ($appMode !== 'cron') {
+ $this->appConfig->setValueString('core', 'backgroundjobs_mode', 'cron');
+ }
+
+ // Low-load hours
+ $onlyTimeSensitive = false;
+ $startHour = $this->config->getSystemValueInt('maintenance_window_start', 100);
+ if ($jobClasses === null && $startHour <= 23) {
+ $date = new \DateTime('now', new \DateTimeZone('UTC'));
+ $currentHour = (int)$date->format('G');
+ $endHour = $startHour + 4;
+
+ if ($startHour <= 20) {
+ // Start time: 01:00
+ // End time: 05:00
+ // Only run sensitive tasks when it's before the start or after the end
+ $onlyTimeSensitive = $currentHour < $startHour || $currentHour > $endHour;
+ } else {
+ // Start time: 23:00
+ // End time: 03:00
+ $endHour -= 24; // Correct the end time from 27:00 to 03:00
+ // Only run sensitive tasks when it's after the end and before the start
+ $onlyTimeSensitive = $currentHour > $endHour && $currentHour < $startHour;
+ }
+ }
+
+ // We only ask for jobs for 14 minutes, because after 5 minutes the next
+ // system cron task should spawn and we want to have at most three
+ // cron jobs running in parallel.
+ $endTime = time() + 14 * 60;
+
+ $executedJobs = [];
+
+ while ($job = $this->jobList->getNext($onlyTimeSensitive, $jobClasses)) {
+ if (isset($executedJobs[$job->getId()])) {
+ $this->jobList->unlockJob($job);
+ break;
+ }
+
+ $jobDetails = get_class($job) . ' (id: ' . $job->getId() . ', arguments: ' . json_encode($job->getArgument()) . ')';
+ $this->logger->debug('CLI cron call has selected job ' . $jobDetails, ['app' => 'cron']);
+
+ $timeBefore = time();
+ $memoryBefore = memory_get_usage();
+ $memoryPeakBefore = memory_get_peak_usage();
+
+ $this->verboseOutput('Starting job ' . $jobDetails);
+
+ $job->start($this->jobList);
+
+ $timeAfter = time();
+ $memoryAfter = memory_get_usage();
+ $memoryPeakAfter = memory_get_peak_usage();
+
+ $cronInterval = 5 * 60;
+ $timeSpent = $timeAfter - $timeBefore;
+ if ($timeSpent > $cronInterval) {
+ $logLevel = match (true) {
+ $timeSpent > $cronInterval * 128 => ILogger::FATAL,
+ $timeSpent > $cronInterval * 64 => ILogger::ERROR,
+ $timeSpent > $cronInterval * 16 => ILogger::WARN,
+ $timeSpent > $cronInterval * 8 => ILogger::INFO,
+ default => ILogger::DEBUG,
+ };
+ $this->logger->log(
+ $logLevel,
+ 'Background job ' . $jobDetails . ' ran for ' . $timeSpent . ' seconds',
+ ['app' => 'cron']
+ );
+ }
+
+ if ($memoryAfter - $memoryBefore > 50_000_000) {
+ $message = 'Used memory grew by more than 50 MB when executing job ' . $jobDetails . ': ' . Util::humanFileSize($memoryAfter) . ' (before: ' . Util::humanFileSize($memoryBefore) . ')';
+ $this->logger->warning($message, ['app' => 'cron']);
+ $this->verboseOutput($message);
+ }
+ if ($memoryPeakAfter > 300_000_000 && $memoryPeakBefore <= 300_000_000) {
+ $message = 'Cron job used more than 300 MB of ram after executing job ' . $jobDetails . ': ' . Util::humanFileSize($memoryPeakAfter) . ' (before: ' . Util::humanFileSize($memoryPeakBefore) . ')';
+ $this->logger->warning($message, ['app' => 'cron']);
+ $this->verboseOutput($message);
+ }
+
+ // clean up after unclean jobs
+ $this->setupManager->tearDown();
+ $this->tempManager->clean();
+
+ $this->verboseOutput('Job ' . $jobDetails . ' done in ' . ($timeAfter - $timeBefore) . ' seconds');
+
+ $this->jobList->setLastJob($job);
+ $executedJobs[$job->getId()] = true;
+ unset($job);
+
+ if ($timeAfter > $endTime) {
+ break;
+ }
+ }
+ }
+
+ private function runWeb(string $appMode): void {
+ if ($appMode === 'cron') {
+ // Cron is cron :-P
+ throw new \RuntimeException('Backgroundjobs are using system cron!');
+ } else {
+ // Work and success :-)
+ $job = $this->jobList->getNext();
+ if ($job != null) {
+ $this->logger->debug('WebCron call has selected job with ID ' . strval($job->getId()), ['app' => 'cron']);
+ $job->start($this->jobList);
+ $this->jobList->setLastJob($job);
+ }
+ }
+ }
+
+ private function verboseOutput(string $message): void {
+ if ($this->verboseCallback !== null) {
+ call_user_func($this->verboseCallback, $message);
+ }
+ }
+}
diff --git a/cron.php b/cron.php
index 445177a4501e1..cb858ab09b15a 100644
--- a/cron.php
+++ b/cron.php
@@ -2,10 +2,9 @@
declare(strict_types=1);
-use OC\Files\SetupManager;
-use OC\Session\CryptoWrapper;
-use OC\Session\Memory;
-use OCP\ILogger;
+use OC\Core\Service\CronService;
+use OCP\Server;
+use Psr\Log\LoggerInterface;
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
@@ -15,16 +14,6 @@
require_once __DIR__ . '/lib/versioncheck.php';
-use OCP\App\IAppManager;
-use OCP\BackgroundJob\IJobList;
-use OCP\IAppConfig;
-use OCP\IConfig;
-use OCP\ISession;
-use OCP\ITempManager;
-use OCP\Server;
-use OCP\Util;
-use Psr\Log\LoggerInterface;
-
try {
require_once __DIR__ . '/lib/base.php';
@@ -45,219 +34,44 @@
exit(0);
}
- if (Util::needUpgrade()) {
- Server::get(LoggerInterface::class)->debug('Update required, skipping cron', ['app' => 'cron']);
- exit;
- }
-
- $config = Server::get(IConfig::class);
-
- if ($config->getSystemValueBool('maintenance', false)) {
- Server::get(LoggerInterface::class)->debug('We are in maintenance mode, skipping cron', ['app' => 'cron']);
- exit;
- }
-
- // Don't do anything if Nextcloud has not been installed
- if (!$config->getSystemValueBool('installed', false)) {
- exit(0);
- }
-
- // load all apps to get all api routes properly setup
- Server::get(IAppManager::class)->loadApps();
- Server::get(ISession::class)->close();
-
- $verbose = isset($argv[1]) && ($argv[1] === '-v' || $argv[1] === '--verbose');
-
- // initialize a dummy memory session
- $session = new Memory();
- $cryptoWrapper = Server::get(CryptoWrapper::class);
- $session = $cryptoWrapper->wrapSession($session);
- \OC::$server->setSession($session);
-
- $logger = Server::get(LoggerInterface::class);
- $appConfig = Server::get(IAppConfig::class);
- $tempManager = Server::get(ITempManager::class);
-
- $tempManager->cleanOld();
-
- // Exit if background jobs are disabled!
- $appMode = $appConfig->getValueString('core', 'backgroundjobs_mode', 'ajax');
- if ($appMode === 'none') {
- if (OC::$CLI) {
- echo 'Background Jobs are disabled!' . PHP_EOL;
- } else {
- OC_JSON::error(['data' => ['message' => 'Background jobs disabled!']]);
- }
- exit(1);
- }
-
- if (OC::$CLI) {
- // set to run indefinitely if needed
- if (strpos(@ini_get('disable_functions'), 'set_time_limit') === false) {
- @set_time_limit(0);
- }
-
- // the cron job must be executed with the right user
- if (!function_exists('posix_getuid')) {
- echo 'The posix extensions are required - see https://www.php.net/manual/en/book.posix.php' . PHP_EOL;
- exit(1);
- }
-
- $user = posix_getuid();
- $configUser = fileowner(OC::$configDir . 'config.php');
- if ($user !== $configUser) {
- echo 'Console has to be executed with the user that owns the file config/config.php' . PHP_EOL;
- echo 'Current user id: ' . $user . PHP_EOL;
- echo 'Owner id of config.php: ' . $configUser . PHP_EOL;
- exit(1);
- }
-
-
- // We call Nextcloud from the CLI (aka cron)
- if ($appMode !== 'cron') {
- $appConfig->setValueString('core', 'backgroundjobs_mode', 'cron');
- }
-
- // a specific job class list can optionally be given as argument
+ $cronService = Server::get(CronService::class);
+ if (isset($argv[1])) {
+ $verbose = $argv[1] === '-v' || $argv[1] === '--verbose';
$jobClasses = array_slice($argv, $verbose ? 2 : 1);
$jobClasses = empty($jobClasses) ? null : $jobClasses;
- // Low-load hours
- $onlyTimeSensitive = false;
- $startHour = $config->getSystemValueInt('maintenance_window_start', 100);
- if ($jobClasses === null && $startHour <= 23) {
- $date = new \DateTime('now', new \DateTimeZone('UTC'));
- $currentHour = (int)$date->format('G');
- $endHour = $startHour + 4;
-
- if ($startHour <= 20) {
- // Start time: 01:00
- // End time: 05:00
- // Only run sensitive tasks when it's before the start or after the end
- $onlyTimeSensitive = $currentHour < $startHour || $currentHour > $endHour;
- } else {
- // Start time: 23:00
- // End time: 03:00
- $endHour -= 24; // Correct the end time from 27:00 to 03:00
- // Only run sensitive tasks when it's after the end and before the start
- $onlyTimeSensitive = $currentHour > $endHour && $currentHour < $startHour;
- }
- }
-
- // Work
- $jobList = Server::get(IJobList::class);
-
- // We only ask for jobs for 14 minutes, because after 5 minutes the next
- // system cron task should spawn and we want to have at most three
- // cron jobs running in parallel.
- $endTime = time() + 14 * 60;
-
- $executedJobs = [];
-
- while ($job = $jobList->getNext($onlyTimeSensitive, $jobClasses)) {
- if (isset($executedJobs[$job->getId()])) {
- $jobList->unlockJob($job);
- break;
- }
-
- $jobDetails = get_class($job) . ' (id: ' . $job->getId() . ', arguments: ' . json_encode($job->getArgument()) . ')';
- $logger->debug('CLI cron call has selected job ' . $jobDetails, ['app' => 'cron']);
-
- $timeBefore = time();
- $memoryBefore = memory_get_usage();
- $memoryPeakBefore = memory_get_peak_usage();
-
- if ($verbose) {
- echo 'Starting job ' . $jobDetails . PHP_EOL;
- }
-
- $job->start($jobList);
-
- $timeAfter = time();
- $memoryAfter = memory_get_usage();
- $memoryPeakAfter = memory_get_peak_usage();
-
- $cronInterval = 5 * 60;
- $timeSpent = $timeAfter - $timeBefore;
- if ($timeSpent > $cronInterval) {
- $logLevel = match (true) {
- $timeSpent > $cronInterval * 128 => ILogger::FATAL,
- $timeSpent > $cronInterval * 64 => ILogger::ERROR,
- $timeSpent > $cronInterval * 16 => ILogger::WARN,
- $timeSpent > $cronInterval * 8 => ILogger::INFO,
- default => ILogger::DEBUG,
- };
- $logger->log(
- $logLevel,
- 'Background job ' . $jobDetails . ' ran for ' . $timeSpent . ' seconds',
- ['app' => 'cron']
- );
- }
-
- if ($memoryAfter - $memoryBefore > 50_000_000) {
- $message = 'Used memory grew by more than 50 MB when executing job ' . $jobDetails . ': ' . Util::humanFileSize($memoryAfter) . ' (before: ' . Util::humanFileSize($memoryBefore) . ')';
- $logger->warning($message, ['app' => 'cron']);
- if ($verbose) {
- echo $message . PHP_EOL;
- }
- }
- if ($memoryPeakAfter > 300_000_000 && $memoryPeakBefore <= 300_000_000) {
- $message = 'Cron job used more than 300 MB of ram after executing job ' . $jobDetails . ': ' . Util::humanFileSize($memoryPeakAfter) . ' (before: ' . Util::humanFileSize($memoryPeakBefore) . ')';
- $logger->warning($message, ['app' => 'cron']);
- if ($verbose) {
- echo $message . PHP_EOL;
- }
- }
-
- // clean up after unclean jobs
- Server::get(SetupManager::class)->tearDown();
- $tempManager->clean();
-
- if ($verbose) {
- echo 'Job ' . $jobDetails . ' done in ' . ($timeAfter - $timeBefore) . ' seconds' . PHP_EOL;
- }
-
- $jobList->setLastJob($job);
- $executedJobs[$job->getId()] = true;
- unset($job);
-
- if ($timeAfter > $endTime) {
- break;
- }
+ if ($verbose) {
+ $cronService->registerVerboseCallback(function (string $message) {
+ echo $message . PHP_EOL;
+ });
}
} else {
- // We call cron.php from some website
- if ($appMode === 'cron') {
- // Cron is cron :-P
- OC_JSON::error(['data' => ['message' => 'Backgroundjobs are using system cron!']]);
- } else {
- // Work and success :-)
- $jobList = Server::get(IJobList::class);
- $job = $jobList->getNext();
- if ($job != null) {
- $logger->debug('WebCron call has selected job with ID ' . strval($job->getId()), ['app' => 'cron']);
- $job->start($jobList);
- $jobList->setLastJob($job);
- }
- OC_JSON::success();
- }
+ $jobClasses = null;
}
- // Log the successful cron execution
- $appConfig->setValueInt('core', 'lastcron', time());
- exit();
-} catch (Exception $ex) {
- Server::get(LoggerInterface::class)->error(
- $ex->getMessage(),
- ['app' => 'cron', 'exception' => $ex]
- );
- echo $ex . PHP_EOL;
- exit(1);
-} catch (Error $ex) {
+ $cronService->run($jobClasses);
+ if (!OC::$CLI) {
+ $data = [
+ 'status' => 'success',
+ ];
+ header('Content-Type: application/json; charset=utf-8');
+ echo json_encode($data, JSON_HEX_TAG);
+ }
+ exit(0);
+} catch (Throwable $e) {
Server::get(LoggerInterface::class)->error(
- $ex->getMessage(),
- ['app' => 'cron', 'exception' => $ex]
+ $e->getMessage(),
+ ['app' => 'cron', 'exception' => $e]
);
- echo $ex . PHP_EOL;
+ if (OC::$CLI) {
+ echo $e->getMessage() . PHP_EOL;
+ } else {
+ $data = [
+ 'status' => 'error',
+ 'message' => $e->getMessage(),
+ ];
+ header('Content-Type: application/json; charset=utf-8');
+ echo json_encode($data, JSON_HEX_TAG);
+ }
exit(1);
}
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index ee77fbd4cda82..df87efcef308d 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -1530,6 +1530,7 @@
'OC\\Core\\Migrations\\Version32000Date20250806110519' => $baseDir . '/core/Migrations/Version32000Date20250806110519.php',
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
'OC\\Core\\ResponseDefinitions' => $baseDir . '/core/ResponseDefinitions.php',
+ 'OC\\Core\\Service\\CronService' => $baseDir . '/core/Service/CronService.php',
'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php',
'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php',
'OC\\DB\\AdapterMySQL' => $baseDir . '/lib/private/DB/AdapterMySQL.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index 3b18f00da9697..d85113291a8bd 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -1571,6 +1571,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\Migrations\\Version32000Date20250806110519' => __DIR__ . '/../../..' . '/core/Migrations/Version32000Date20250806110519.php',
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
'OC\\Core\\ResponseDefinitions' => __DIR__ . '/../../..' . '/core/ResponseDefinitions.php',
+ 'OC\\Core\\Service\\CronService' => __DIR__ . '/../../..' . '/core/Service/CronService.php',
'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php',
'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php',
'OC\\DB\\AdapterMySQL' => __DIR__ . '/../../..' . '/lib/private/DB/AdapterMySQL.php',