From 14f6e6bc4e8df074bced5646136371fd2c41c267 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 29 May 2026 10:39:17 -0400 Subject: [PATCH 1/4] fix(proxy): track built-in CODE cold-start state across requests --- proxy.php | 225 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 136 insertions(+), 89 deletions(-) diff --git a/proxy.php b/proxy.php index a54da56..2e55f81 100644 --- a/proxy.php +++ b/proxy.php @@ -35,7 +35,7 @@ function errorExit($msg) exit(); } -debug_log('Proxy v1'); +debug_log('Proxy'); // Let the webserver time us out in its own good time. set_time_limit(0); @@ -46,6 +46,9 @@ function errorExit($msg) $tmp_dir = ini_get('upload_tmp_dir') ? ini_get('upload_tmp_dir') : sys_get_temp_dir(); $lockfile = "$tmp_dir/coolwsd.lock"; $pidfile = "$tmp_dir/coolwsd.pid"; +$startingfile = "$tmp_dir/coolwsd.starting"; + +const COOLWSD_STARTING_TTL = 180; function getCoolwsdPid() { @@ -69,7 +72,61 @@ function isCoolwsdRunning() if ($pid === 0) return 0; - return posix_kill($pid,0); + return posix_kill($pid, 0); +} + +function markCoolwsdStarting() +{ + global $startingfile; + file_put_contents($startingfile, (string)time()); +} + +function clearCoolwsdStarting() +{ + global $startingfile; + if (file_exists($startingfile)) + unlink($startingfile); +} + +function getCoolwsdStartingSince() +{ + global $startingfile; + + clearstatcache(); + if (!file_exists($startingfile)) + return 0; + + $ts = (int)trim(@file_get_contents($startingfile)); + if ($ts <= 0) + $ts = (int)@filemtime($startingfile); + + return $ts ?: 0; +} + +function isCoolwsdStartupInProgress() +{ + $since = getCoolwsdStartingSince(); + if ($since === 0) + return false; + + return (time() - $since) < COOLWSD_STARTING_TTL; +} + +function isCoolwsdStartupStale() +{ + $since = getCoolwsdStartingSince(); + if ($since === 0) + return false; + + return (time() - $since) >= COOLWSD_STARTING_TTL; +} + +function clearStaleCoolwsdStartupState() +{ + if (isCoolwsdStartupStale() && !isCoolwsdRunning()) { + debug_log("Clearing stale coolwsd startup marker"); + clearCoolwsdStarting(); + } } function startCoolwsd() @@ -91,51 +148,64 @@ function startCoolwsd() $launchCmd = "ip -6 addr"; debug_log("Testing disabled IPv6: $launchCmd"); exec($launchCmd, $output, $return); - if (implode("",$output)=="") + if (implode("", $output) == "") { debug_log("IPv6 disabled. Will launch coolwsd with IPv4-only option."); $IPv4only = "--o:net.proto=IPv4"; } - // Extract the AppImage if FUSE is not available // net.lok_allow.host[14] is the next empty slot after the last element of the default list // when lok_allow does not contain the Nextcloud host, it is not possible to insert image from Nextcloud // we have to set explicitly, because storage.wopi.alias_groups[@mode] is 'first' in case of richdocumentscode $lok_allow = "--o:net.lok_allow.host[14]=" . escapeshellarg($_SERVER['HTTP_HOST']); - $launchCmd = "bash -c \"( $appImage $remoteFontConfig $IPv4only $lok_allow --pidfile=$pidfile || $appImage --appimage-extract-and-run $remoteFontConfig $IPv4only $lok_allow --pidfile=$pidfile) >/dev/null & disown\""; + + // Extract the AppImage if FUSE is not available + $launchCmd = "bash -c \"( $appImage $remoteFontConfig $IPv4only $lok_allow --pidfile=$pidfile || " . + "$appImage --appimage-extract-and-run $remoteFontConfig $IPv4only $lok_allow --pidfile=$pidfile" . + " ) >/dev/null 2>&1 &\""; // Remove stale lock file (just in case) - if (file_exists("$lockfile")) - if (time() - filectime("$lockfile") > 60 * 5) - unlink("$lockfile"); + if (file_exists($lockfile)) + if (time() - filectime($lockfile) > 60 * 5) + unlink($lockfile); // Prevent second start - $lock = @fopen("$lockfile", "x"); - if ($lock) + $lock = fopen($lockfile, "x"); + if (!$lock) { - // We start a new server, we don't need stale pidfile around - if (file_exists("$pidfile")) - unlink("$pidfile"); - - debug_log("Launch the coolwsd server: $launchCmd"); - exec($launchCmd, $output, $return); - if ($return) - debug_log("Failed to launch server at $appImage."); + debug_log("Someone else starts coolwsd"); + while (!isCoolwsdRunning()) + sleep(1); - fclose($lock); + return; } + // We start a new server, we don't need stale pidfile around + if (file_exists($pidfile)) + unlink($pidfile); + + markCoolwsdStarting(); + + debug_log("Launch the coolwsd server: $launchCmd"); + exec($launchCmd, $output, $return); + if ($return) + debug_log("Failed to launch server at $appImage."); + while (!isCoolwsdRunning()) sleep(1); - if (file_exists("$lockfile")) - unlink("$lockfile"); + // Startup complete enough to observe a running pid + clearCoolwsdStarting(); + + // Release the lock + fclose($lock); + unlink($lockfile); } function stopCoolwsd() { $pid = getCoolwsdPid(); - if (posix_kill($pid,0)) + if ($pid && posix_kill($pid, 0)) { debug_log("Stopping the coolwsd server with pid: $pid"); posix_kill($pid, 15 /*SIGTERM*/); @@ -246,59 +316,54 @@ function parseLastHeader(&$chunk, &$contentLength) exit(); } -// If we can't get a socket open in 3 seconds when that is backed by -// a dedicated thread, then we have a server missing in action. -$local = @fsockopen("localhost", 9983, $errno, $errstr, 3); +clearStaleCoolwsdStartupState(); // Return the status and exit if it is a ?status request if ($statusOnly) { header('Content-type: application/json'); header('Cache-Control: no-store'); - if (!$local) { + + if (!isCoolwsdRunning()) { $err = checkCoolwsdSetup(); - if (!empty($err)) + if (!empty($err)) { print '{"status":"error","error":"' . $err . '"}'; - else if (!isCoolwsdRunning()) { - startCoolwsd(); - print '{"status":"starting"}'; + exit(); } - } else if ($errno === 111) { - print '{"status":"starting"}'; - } else { - $response = file_get_contents("http://localhost:9983/hosting/capabilities", 0, stream_context_create(["http"=>["timeout"=>1]])); - if ($response) { - // Version check. - $obj = json_decode($response); - $expVer = '%COOLWSD_VERSION_HASH%'; - $actVer = substr($obj->{'productVersionHash'}, 0, strlen($expVer)); - if ($actVer !== $expVer && $expVer !== '%' . 'COOLWSD_VERSION_HASH' . '%') { // deliberately split so that sed does not touch this during build-time - // Old/unexpected server version; restart. - error_log("Old server found, restarting. Expected hash $expVer but found $actVer."); - stopCoolwsd(); - // wait 10 seconds max - for ($i = 0; isCoolwsdRunning() && ($i < 10); $i++) - sleep(1); - - // somebody else might have restarted it in the meantime - if (!isCoolwsdRunning()) - startCoolwsd(); - - print '{"status":"restarting"}'; - } - else - print '{"status":"OK"}'; + + startCoolwsd(); + } + + $response = file_get_contents( + "http://localhost:9983/hosting/capabilities", + 0, + stream_context_create(["http" => ["timeout" => 1]]) + ); + + if ($response) { + // Version check. + $obj = json_decode($response); + $expVer = '%COOLWSD_VERSION_HASH%'; + $actVer = substr($obj->{'productVersionHash'}, 0, strlen($expVer)); + if ($actVer !== $expVer && $expVer !== '%' . 'COOLWSD_VERSION_HASH' . '%') { // deliberately split so that sed does not touch this during build-time + // Old server found, restart. + error_log("Old server found, restarting. Expected hash $expVer but found $actVer."); + stopCoolwsd(); + clearCoolwsdStarting(); + startCoolwsd(); + print '{"status":"restarting"}'; } else - print '{"status":"starting"}'; - fclose($local); - } + print '{"status":"OK"}'; + } else + print '{"status":"starting"}'; exit(); } + // URL into this server of the proxy script. if ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') - || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https' ) - || (isset($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on') + || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https' ) + || (isset($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on') ) { $proxyURL = "https://"; } else { @@ -306,40 +371,21 @@ function parseLastHeader(&$chunk, &$contentLength) } // Start the appimage if necessary -if (!$local) +$local = @fsockopen("localhost", 9983, $errno, $errstr, 15); +while (!$local) { $err = checkCoolwsdSetup(); if (!empty($err)) errorExit($err); - else if (!isCoolwsdRunning()) - startCoolwsd(); - - $logonce = true; - while (true) { - $local = @fsockopen("localhost", 9983, $errno, $errstr, 15); - if ($errno === 111) { - if($logonce) { - debug_log("Can't yet connect to socket so sleep"); - $logonce = false; - } - usleep(50 * 1000); // 50ms. - } else { - debug_log("connected?"); - break; - } - } -} -if (!$local) { - errorExit("Timed out opening local socket: $errno - $errstr"); + startCoolwsd(); + $local = @fsockopen("localhost", 9983, $errno, $errstr, 15); } -// Fetch our headers for later -$headers = getallheaders(); - $proxyURL .= $_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'] . '?req='; debug_log("ProxyPrefix: '$proxyURL'"); +$headers = getallheaders(); $realRequest = $_SERVER['REQUEST_METHOD'] . " " . $request . " " . $_SERVER['SERVER_PROTOCOL']; debug_log("Onward request is: '$realRequest'"); @@ -356,11 +402,11 @@ function isMultipartRequest($headers) $multiBody = ''; if ($body === '' && isMultipartRequest($headers)) { debug_log("Oh dear - PHP's rfc1867 handling doesn't give any php://input to work with"); - debug_log("Reconstructing multipart body - Files: " . count($_FILES) . ", Form fields: " . count($_POST)); + debug_log("Reconstructing body - Files: " . count($_FILES) . ", Form fields: " . count($_POST)); $type = isset($headers['Content-Type']) ? $headers['Content-Type'] : $headers['content-type']; $boundary = trim(explode('boundary=', $type)[1]); - foreach ($_REQUEST as $key=>$value) { + foreach ($_REQUEST as $key => $value) { if ($key === 'req') { continue; } @@ -405,15 +451,16 @@ function isMultipartRequest($headers) $contentLength = -1; $contentWritten = 0; $parsingHeaders = true; +$extOut = null; do { $chunk = fread($local, 65536); - if($chunk === false) { + if ($chunk === false) { $error = error_get_last(); $errorMessage = $error ? implode(' ', $error) : 'No error'; echo "ERROR ! $errorMessage\n"; debug_log("error on chunk: $errorMessage"); break; - } elseif($chunk === '') { + } elseif ($chunk === '') { debug_log("empty chunk last data"); if ($parsingHeaders) errorExit("No content in reply from coolwsd. Is SSL enabled in error ?"); @@ -442,7 +489,7 @@ function isMultipartRequest($headers) break; } -} while(true); +} while (true); debug_log("closing local socket"); fclose($local); From aa8041f265645ae9b24856e6cf49434de6e8a1a0 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 29 May 2026 10:43:26 -0400 Subject: [PATCH 2/4] fix(proxy): decouple CODE startup from request lifetime --- proxy.php | 94 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 60 insertions(+), 34 deletions(-) diff --git a/proxy.php b/proxy.php index 2e55f81..df97c4d 100644 --- a/proxy.php +++ b/proxy.php @@ -49,6 +49,8 @@ function errorExit($msg) $startingfile = "$tmp_dir/coolwsd.starting"; const COOLWSD_STARTING_TTL = 180; +const COOLWSD_CONNECT_WAIT = 3; +const COOLWSD_RETRY_DELAY_US = 100000; function getCoolwsdPid() { @@ -75,6 +77,17 @@ function isCoolwsdRunning() return posix_kill($pid, 0); } +function isCoolwsdReachable() +{ + $local = @fsockopen("localhost", 9983, $errno, $errstr, 1); + if ($local) { + fclose($local); + return true; + } + + return false; +} + function markCoolwsdStarting() { global $startingfile; @@ -123,12 +136,28 @@ function isCoolwsdStartupStale() function clearStaleCoolwsdStartupState() { - if (isCoolwsdStartupStale() && !isCoolwsdRunning()) { + if (isCoolwsdStartupStale() && !isCoolwsdRunning() && !isCoolwsdReachable()) { debug_log("Clearing stale coolwsd startup marker"); clearCoolwsdStarting(); } } +function waitForCoolwsdReady($timeoutSec) +{ + $deadline = microtime(true) + $timeoutSec; + + while (microtime(true) < $deadline) { + if (isCoolwsdReachable()) { + clearCoolwsdStarting(); + return true; + } + + usleep(COOLWSD_RETRY_DELAY_US); + } + + return false; +} + function startCoolwsd() { global $appImage; @@ -146,6 +175,8 @@ function startCoolwsd() // Check if IPv6 has been disabled $IPv4only = ""; $launchCmd = "ip -6 addr"; + $output = []; + $return = 0; debug_log("Testing disabled IPv6: $launchCmd"); exec($launchCmd, $output, $return); if (implode("", $output) == "") @@ -159,10 +190,11 @@ function startCoolwsd() // we have to set explicitly, because storage.wopi.alias_groups[@mode] is 'first' in case of richdocumentscode $lok_allow = "--o:net.lok_allow.host[14]=" . escapeshellarg($_SERVER['HTTP_HOST']); - // Extract the AppImage if FUSE is not available - $launchCmd = "bash -c \"( $appImage $remoteFontConfig $IPv4only $lok_allow --pidfile=$pidfile || " . + // Launch detached so startup survives the PHP request ending. + // Extracts the AppImage if FUSE is not available + $launchCmd = "bash -c '( $appImage $remoteFontConfig $IPv4only $lok_allow --pidfile=$pidfile || " . "$appImage --appimage-extract-and-run $remoteFontConfig $IPv4only $lok_allow --pidfile=$pidfile" . - " ) >/dev/null 2>&1 &\""; + " ) >/dev/null 2>&1 < /dev/null &'"; // Remove stale lock file (just in case) if (file_exists($lockfile)) @@ -170,36 +202,30 @@ function startCoolwsd() unlink($lockfile); // Prevent second start - $lock = fopen($lockfile, "x"); + $lock = @fopen($lockfile, "x"); if (!$lock) { - debug_log("Someone else starts coolwsd"); - while (!isCoolwsdRunning()) - sleep(1); - + debug_log("coolwsd startup already in progress"); return; } - // We start a new server, we don't need stale pidfile around - if (file_exists($pidfile)) - unlink($pidfile); - - markCoolwsdStarting(); - - debug_log("Launch the coolwsd server: $launchCmd"); - exec($launchCmd, $output, $return); - if ($return) - debug_log("Failed to launch server at $appImage."); - - while (!isCoolwsdRunning()) - sleep(1); - - // Startup complete enough to observe a running pid - clearCoolwsdStarting(); - - // Release the lock - fclose($lock); - unlink($lockfile); + try { + if (file_exists($pidfile)) + unlink($pidfile); + + markCoolwsdStarting(); + + debug_log("Launch the coolwsd server: $launchCmd"); + $output = []; + $return = 0; + exec($launchCmd, $output, $return); + if ($return) + debug_log("Failed to launch server at $appImage."); + } finally { + fclose($lock); + if (file_exists($lockfile)) + unlink($lockfile); + } } function stopCoolwsd() @@ -237,10 +263,14 @@ function checkCoolwsdSetup() if (in_array('exec', $disabledFunctions) || @exec('echo EXEC') !== "EXEC") return 'exec_disabled'; + $output = []; + $return = 0; exec("LD_TRACE_LOADED_OBJECTS=1 $appImage", $output, $return); if ($return) return 'no_glibc'; + $output = []; + $return = 0; exec('( /sbin/ldconfig -p || scanelf -l ) | grep fontconfig > /dev/null 2>&1', $output, $return); if ($return) return 'no_fontconfig'; @@ -340,7 +370,6 @@ function parseLastHeader(&$chunk, &$contentLength) ); if ($response) { - // Version check. $obj = json_decode($response); $expVer = '%COOLWSD_VERSION_HASH%'; $actVer = substr($obj->{'productVersionHash'}, 0, strlen($expVer)); @@ -351,8 +380,7 @@ function parseLastHeader(&$chunk, &$contentLength) clearCoolwsdStarting(); startCoolwsd(); print '{"status":"restarting"}'; - } - else + } else print '{"status":"OK"}'; } else print '{"status":"starting"}'; @@ -398,7 +426,6 @@ function isMultipartRequest($headers) return strpos(strtolower($contentType), 'multipart/form-data') !== false; } -// Oh dear - PHP's rfc1867 handling doesn't give any php://input to work with in this case. $multiBody = ''; if ($body === '' && isMultipartRequest($headers)) { debug_log("Oh dear - PHP's rfc1867 handling doesn't give any php://input to work with"); @@ -430,7 +457,6 @@ function isMultipartRequest($headers) } fwrite($local, $realRequest . "\r\n"); -// Send the headers on ... foreach ($headers as $header => $value) { debug_log("$header: $value\n"); if ($multiBody !== '' && $header === 'Content-Length') From 5f0db99529dbab3d75470c14c4521d3569341fd4 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 29 May 2026 10:52:57 -0400 Subject: [PATCH 3/4] fix(proxy): make status endpoint reflect cold-start progress reliably --- proxy.php | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/proxy.php b/proxy.php index df97c4d..c7050c8 100644 --- a/proxy.php +++ b/proxy.php @@ -353,19 +353,28 @@ function parseLastHeader(&$chunk, &$contentLength) header('Content-type: application/json'); header('Cache-Control: no-store'); - if (!isCoolwsdRunning()) { + if (!isCoolwsdReachable()) { $err = checkCoolwsdSetup(); if (!empty($err)) { print '{"status":"error","error":"' . $err . '"}'; exit(); } - startCoolwsd(); + if (!isCoolwsdStartupInProgress()) + startCoolwsd(); + + if (!waitForCoolwsdReady(COOLWSD_CONNECT_WAIT)) { + $elapsed = getCoolwsdStartingSince(); + $elapsed = $elapsed ? max(0, time() - $elapsed) : 0; + http_response_code(202); + print '{"status":"starting","elapsed":' . $elapsed . '}'; + exit(); + } } - $response = file_get_contents( + $response = @file_get_contents( "http://localhost:9983/hosting/capabilities", - 0, + false, stream_context_create(["http" => ["timeout" => 1]]) ); @@ -379,11 +388,23 @@ function parseLastHeader(&$chunk, &$contentLength) stopCoolwsd(); clearCoolwsdStarting(); startCoolwsd(); - print '{"status":"restarting"}'; - } else + + $elapsed = getCoolwsdStartingSince(); + $elapsed = $elapsed ? max(0, time() - $elapsed) : 0; + http_response_code(202); + print '{"status":"restarting","elapsed":' . $elapsed . '}'; + } + else { + clearCoolwsdStarting(); print '{"status":"OK"}'; - } else - print '{"status":"starting"}'; + } + } + else { + $elapsed = getCoolwsdStartingSince(); + $elapsed = $elapsed ? max(0, time() - $elapsed) : 0; + http_response_code(202); + print '{"status":"starting","elapsed":' . $elapsed . '}'; + } exit(); } From 5237eda3d4e73031dfd19cf63c2d5414c830488e Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 29 May 2026 11:01:29 -0400 Subject: [PATCH 4/4] fix(proxy): bound normal request waits during CODE cold start --- proxy.php | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/proxy.php b/proxy.php index c7050c8..34e1eb2 100644 --- a/proxy.php +++ b/proxy.php @@ -234,7 +234,7 @@ function stopCoolwsd() if ($pid && posix_kill($pid, 0)) { debug_log("Stopping the coolwsd server with pid: $pid"); - posix_kill($pid, 15 /*SIGTERM*/); + posix_kill($pid, 15); } } @@ -331,7 +331,7 @@ function parseLastHeader(&$chunk, &$contentLength) if ($request === '' && !$statusOnly) errorExit("Missing, required req= parameter"); -if (startsWith($request, '/hosting/capabilities') && !isCoolwsdRunning()) { +if (startsWith($request, '/hosting/capabilities') && !isCoolwsdReachable()) { header('Content-type: application/json'); header('Cache-Control: no-store'); @@ -409,6 +409,27 @@ function parseLastHeader(&$chunk, &$contentLength) exit(); } +// Start the appimage if necessary +$local = @fsockopen("localhost", 9983, $errno, $errstr, 1); +if (!$local) +{ + $err = checkCoolwsdSetup(); + if (!empty($err)) + errorExit($err); + + if (!isCoolwsdStartupInProgress()) + startCoolwsd(); + + if (!waitForCoolwsdReady(COOLWSD_CONNECT_WAIT)) + errorExit("coolwsd is starting, please retry shortly"); + + $local = @fsockopen("localhost", 9983, $errno, $errstr, 3); +} + +if (!$local) { + errorExit("Timed out opening local socket: $errno - $errstr"); +} + // URL into this server of the proxy script. if ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https' ) @@ -419,22 +440,11 @@ function parseLastHeader(&$chunk, &$contentLength) $proxyURL = "http://"; } -// Start the appimage if necessary -$local = @fsockopen("localhost", 9983, $errno, $errstr, 15); -while (!$local) -{ - $err = checkCoolwsdSetup(); - if (!empty($err)) - errorExit($err); - - startCoolwsd(); - $local = @fsockopen("localhost", 9983, $errno, $errstr, 15); -} +$headers = getallheaders(); $proxyURL .= $_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'] . '?req='; debug_log("ProxyPrefix: '$proxyURL'"); -$headers = getallheaders(); $realRequest = $_SERVER['REQUEST_METHOD'] . " " . $request . " " . $_SERVER['SERVER_PROTOCOL']; debug_log("Onward request is: '$realRequest'");