feat: enabled frankenphp worker mode

Signed-off-by: Carl Schwan <carlschwan@kde.org>
This commit is contained in:
Carl Schwan 2026-02-24 16:40:33 +01:00
parent 689196b1d3
commit 6fc03681bd
No known key found for this signature in database
GPG key ID: 02325448204E452A
9 changed files with 148 additions and 105 deletions

View file

@ -1,9 +1,10 @@
localhost {
php_server {
worker index.php
}
log {
level ERROR
output stderr
}

View file

@ -30,6 +30,7 @@ function exceptionHandler($exception) {
}
try {
require_once __DIR__ . '/lib/base.php';
OC::init();
// set to run indefinitely if needed
if (strpos(@ini_get('disable_functions'), 'set_time_limit') === false) {

View file

@ -17,6 +17,8 @@ require_once __DIR__ . '/lib/versioncheck.php';
try {
require_once __DIR__ . '/lib/base.php';
OC::init();
if (isset($argv[1]) && ($argv[1] === '-h' || $argv[1] === '--help')) {
echo 'Description:
Run the background job routine

169
index.php
View file

@ -19,90 +19,113 @@ use OCP\Server;
use OCP\Template\ITemplateManager;
use Psr\Log\LoggerInterface;
try {
require_once __DIR__ . '/lib/base.php';
require_once __DIR__ . '/lib/base.php';
OC::handleRequest();
} catch (ServiceUnavailableException $ex) {
Server::get(LoggerInterface::class)->error($ex->getMessage(), [
'app' => 'index',
'exception' => $ex,
]);
//show the user a detailed error page
Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 503);
} catch (HintException $ex) {
$handler = static function () {
try {
Server::get(ITemplateManager::class)->printErrorPage($ex->getMessage(), $ex->getHint(), 503);
} catch (Exception $ex2) {
// In worker mode, script name is empty in FrankenPHP
if ($_SERVER['SCRIPT_NAME'] === '') {
$_SERVER['SCRIPT_NAME'] = $_SERVER['PHP_SELF'];
}
OC::init();
OC::handleRequest();
} catch (ServiceUnavailableException $ex) {
Server::get(LoggerInterface::class)->error($ex->getMessage(), [
'app' => 'index',
'exception' => $ex,
]);
//show the user a detailed error page
Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 503);
} catch (HintException $ex) {
try {
Server::get(ITemplateManager::class)->printErrorPage($ex->getMessage(), $ex->getHint(), 503);
} catch (Exception $ex2) {
try {
Server::get(LoggerInterface::class)->error($ex->getMessage(), [
'app' => 'index',
'exception' => $ex,
]);
Server::get(LoggerInterface::class)->error($ex2->getMessage(), [
'app' => 'index',
'exception' => $ex2,
]);
} catch (Throwable $e) {
// no way to log it properly - but to avoid a white page of death we try harder and ignore this one here
}
//show the user a detailed error page
Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500);
}
} catch (LoginException $ex) {
$request = Server::get(IRequest::class);
/**
* Routes with the @CORS annotation and other API endpoints should
* not return a webpage, so we only print the error page when html is accepted,
* otherwise we reply with a JSON array like the SecurityMiddleware would do.
*/
if (stripos($request->getHeader('Accept'), 'html') === false) {
http_response_code(401);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['message' => $ex->getMessage()]);
exit();
}
Server::get(ITemplateManager::class)->printErrorPage($ex->getMessage(), $ex->getMessage(), 401);
} catch (MaxDelayReached $ex) {
$request = Server::get(IRequest::class);
/**
* Routes with the @CORS annotation and other API endpoints should
* not return a webpage, so we only print the error page when html is accepted,
* otherwise we reply with a JSON array like the BruteForceMiddleware would do.
*/
if (stripos($request->getHeader('Accept'), 'html') === false) {
http_response_code(429);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['message' => $ex->getMessage()]);
exit();
}
http_response_code(429);
Server::get(ITemplateManager::class)->printGuestPage('core', '429');
} catch (Exception $ex) {
Server::get(LoggerInterface::class)->error($ex->getMessage(), [
'app' => 'index',
'exception' => $ex,
]);
//show the user a detailed error page
Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500);
} catch (Error $ex) {
try {
Server::get(LoggerInterface::class)->error($ex->getMessage(), [
'app' => 'index',
'exception' => $ex,
]);
Server::get(LoggerInterface::class)->error($ex2->getMessage(), [
'app' => 'index',
'exception' => $ex2,
]);
} catch (Throwable $e) {
// no way to log it properly - but to avoid a white page of death we try harder and ignore this one here
}
} catch (Error $e) {
http_response_code(500);
header('Content-Type: text/plain; charset=utf-8');
print("Internal Server Error\n\n");
print("The server encountered an internal error and was unable to complete your request.\n");
print("Please contact the server administrator if this error reappears multiple times, please include the technical details below in your report.\n");
print("More details can be found in the webserver log.\n");
//show the user a detailed error page
throw $ex;
}
Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500);
}
} catch (LoginException $ex) {
$request = Server::get(IRequest::class);
/**
* Routes with the @CORS annotation and other API endpoints should
* not return a webpage, so we only print the error page when html is accepted,
* otherwise we reply with a JSON array like the SecurityMiddleware would do.
*/
if (stripos($request->getHeader('Accept'), 'html') === false) {
http_response_code(401);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['message' => $ex->getMessage()]);
exit();
}
Server::get(ITemplateManager::class)->printErrorPage($ex->getMessage(), $ex->getMessage(), 401);
} catch (MaxDelayReached $ex) {
$request = Server::get(IRequest::class);
/**
* Routes with the @CORS annotation and other API endpoints should
* not return a webpage, so we only print the error page when html is accepted,
* otherwise we reply with a JSON array like the BruteForceMiddleware would do.
*/
if (stripos($request->getHeader('Accept'), 'html') === false) {
http_response_code(429);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['message' => $ex->getMessage()]);
exit();
}
http_response_code(429);
Server::get(ITemplateManager::class)->printGuestPage('core', '429');
} catch (Exception $ex) {
Server::get(LoggerInterface::class)->error($ex->getMessage(), [
'app' => 'index',
'exception' => $ex,
]);
};
//show the user a detailed error page
Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500);
} catch (Error $ex) {
try {
Server::get(LoggerInterface::class)->error($ex->getMessage(), [
'app' => 'index',
'exception' => $ex,
]);
} catch (Error $e) {
http_response_code(500);
header('Content-Type: text/plain; charset=utf-8');
print("Internal Server Error\n\n");
print("The server encountered an internal error and was unable to complete your request.\n");
print("Please contact the server administrator if this error reappears multiple times, please include the technical details below in your report.\n");
print("More details can be found in the webserver log.\n");
if (function_exists('frankenphp_handle_request')) {
$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);
for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {
$keepRunning = \frankenphp_handle_request($handler);
throw $ex;
// Call the garbage collector to reduce the chances of it being triggered in the middle of a page generation
gc_collect_cycles();
if (!$keepRunning) {
break;
}
}
Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500);
} else {
$handler();
}

View file

@ -104,17 +104,6 @@ class OC {
* the app path list is empty or contains an invalid path
*/
public static function initPaths(): void {
if (defined('PHPUNIT_CONFIG_DIR')) {
self::$configDir = OC::$SERVERROOT . '/' . PHPUNIT_CONFIG_DIR . '/';
} elseif (defined('PHPUNIT_RUN') && PHPUNIT_RUN && is_dir(OC::$SERVERROOT . '/tests/config/')) {
self::$configDir = OC::$SERVERROOT . '/tests/config/';
} elseif ($dir = getenv('NEXTCLOUD_CONFIG_DIR')) {
self::$configDir = rtrim($dir, '/') . '/';
} else {
self::$configDir = OC::$SERVERROOT . '/config/';
}
self::$config = new \OC\Config(self::$configDir);
OC::$SUBURI = str_replace('\\', '/', substr(realpath($_SERVER['SCRIPT_FILENAME'] ?? ''), strlen(OC::$SERVERROOT)));
/**
* FIXME: The following lines are required because we can't yet instantiate
@ -166,7 +155,7 @@ class OC {
// Resolve /nextcloud to /nextcloud/ to ensure to always have a trailing
// slash which is required by URL generation.
if (isset($_SERVER['REQUEST_URI']) && $_SERVER['REQUEST_URI'] === \OC::$WEBROOT
&& substr($_SERVER['REQUEST_URI'], -1) !== '/') {
&& substr($_SERVER['REQUEST_URI'], -1) !== '/') {
header('Location: ' . \OC::$WEBROOT . '/');
exit();
}
@ -186,6 +175,7 @@ class OC {
OC::$APPSROOTS[] = ['path' => OC::$SERVERROOT . '/apps', 'url' => '/apps', 'writable' => true];
}
if (empty(OC::$APPSROOTS)) {
throw new \RuntimeException('apps directory not found! Please put the Nextcloud apps folder in the Nextcloud folder'
. '. You can also configure the location in the config.php file.');
@ -664,12 +654,7 @@ class OC {
}
}
public static function init(): void {
// First handle PHP configuration and copy auth headers to the expected
// $_SERVER variable before doing anything Server object related
self::setRequiredIniValues();
self::handleAuthHeaders();
public static function boot(): void {
// prevent any XML processing from loading external entities
libxml_set_external_entity_loader(static function () {
return null;
@ -692,14 +677,32 @@ class OC {
self::$composerAutoloader = require_once OC::$SERVERROOT . '/lib/composer/autoload.php';
self::$composerAutoloader->setApcuPrefix(null);
// setup 3rdparty autoloader
$vendorAutoLoad = OC::$SERVERROOT . '/3rdparty/autoload.php';
if (!file_exists($vendorAutoLoad)) {
throw new \RuntimeException('Composer autoloader not found, unable to continue. Check the folder "3rdparty". Running "git submodule update --init" will initialize the git submodule that handles the subfolder "3rdparty".');
}
require_once $vendorAutoLoad;
$loaderEnd = microtime(true);
// load configs
if (defined('PHPUNIT_CONFIG_DIR')) {
self::$configDir = OC::$SERVERROOT . '/' . PHPUNIT_CONFIG_DIR . '/';
} elseif (defined('PHPUNIT_RUN') && PHPUNIT_RUN && is_dir(OC::$SERVERROOT . '/tests/config/')) {
self::$configDir = OC::$SERVERROOT . '/tests/config/';
} elseif ($dir = getenv('NEXTCLOUD_CONFIG_DIR')) {
self::$configDir = rtrim($dir, '/') . '/';
} else {
self::$configDir = OC::$SERVERROOT . '/config/';
}
self::$config = new \OC\Config(self::$configDir);
// Enable lazy loading if activated
\OC\AppFramework\Utility\SimpleContainer::$useLazyObjects = (bool)self::$config->getValue('enable_lazy_objects', true);
try {
self::initPaths();
// setup 3rdparty autoloader
$vendorAutoLoad = OC::$SERVERROOT . '/3rdparty/autoload.php';
if (!file_exists($vendorAutoLoad)) {
throw new \RuntimeException('Composer autoloader not found, unable to continue. Check the folder "3rdparty". Running "git submodule update --init" will initialize the git submodule that handles the subfolder "3rdparty".');
}
require_once $vendorAutoLoad;
} catch (\RuntimeException $e) {
if (!self::$CLI) {
http_response_code(503);
@ -709,15 +712,24 @@ class OC {
print($e->getMessage());
exit();
}
$loaderEnd = microtime(true);
// Enable lazy loading if activated
\OC\AppFramework\Utility\SimpleContainer::$useLazyObjects = (bool)self::$config->getValue('enable_lazy_objects', true);
//$eventLogger = Server::get(\OCP\Diagnostics\IEventLogger::class);
//$eventLogger->log('autoloader', 'Autoloader', $loaderStart, $loaderEnd);
//$eventLogger->start('init', 'Initialize');
}
// setup the basic server
public static function init(): void {
// First handle PHP configuration and copy auth headers to the expected
// $_SERVER variable before doing anything Server object related
self::setRequiredIniValues();
self::handleAuthHeaders();
// set up the basic server
self::$server = new \OC\Server(\OC::$WEBROOT, self::$config);
self::$server->boot();
$loaderStart = microtime(true);
try {
$profiler = new BuiltInProfiler(
Server::get(IConfig::class),
@ -733,8 +745,7 @@ class OC {
}
$eventLogger = Server::get(\OCP\Diagnostics\IEventLogger::class);
$eventLogger->log('autoloader', 'Autoloader', $loaderStart, $loaderEnd);
$eventLogger->start('boot', 'Initialize');
$eventLogger->start('init', 'Initialize');
// Override php.ini and log everything if we're troubleshooting
if (self::$config->getValue('loglevel') === ILogger::DEBUG) {
@ -1307,4 +1318,4 @@ class OC {
}
}
OC::init();
OC::boot();

View file

@ -280,6 +280,7 @@ class Router implements IRouter {
$this->loadRoutes();
}
$this->eventLogger->start('route:url:match', 'Symfony url matcher call');
$matcher = new UrlMatcher($this->root, $this->context);
try {

View file

@ -11,6 +11,7 @@ use OCP\IRequest;
use OCP\Server;
require_once __DIR__ . '/../lib/base.php';
OC::init();
header('Content-Type: application/json');

View file

@ -28,6 +28,7 @@ use Psr\Log\LoggerInterface;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
OC::init();
$request = Server::get(IRequest::class);
if ((Util::needUpgrade() || Server::get(IConfig::class)->getSystemValueBool('maintenance')) && $request->getPathInfo() !== '/core/update') {

View file

@ -97,6 +97,8 @@ function resolveService($service) {
try {
require_once __DIR__ . '/lib/base.php';
OC::init();
// All resources served via the DAV endpoint should have the strictest possible
// policy. Exempted from this is the SabreDAV browser plugin which overwrites
// this policy with a softer one if debug mode is enabled.