diff --git a/apps/cli/php-server-child.ts b/apps/cli/php-server-child.ts index 8744f51832..efdafd84bf 100644 --- a/apps/cli/php-server-child.ts +++ b/apps/cli/php-server-child.ts @@ -8,6 +8,7 @@ import { ChildProcess, spawn } from 'node:child_process'; import fs from 'node:fs'; +import os from 'node:os'; import path from 'node:path'; import { DEFAULT_PHP_VERSION } from '@studio/common/constants'; import { DEFAULT_LOCALE } from '@studio/common/lib/locale'; @@ -346,9 +347,6 @@ async function startServer( config: ServerConfig, signal: AbortSignal ): Promise } ); } ); - stopSignal.throwIfAborted(); - await waitForServerReady( `http://localhost:${ config.port }/`, stopSignal ); - serverChild.once( 'exit', ( code, signalName ) => { errorToConsole( `PHP child process exited unexpectedly (code: ${ code }, signal: ${ signalName })` @@ -356,6 +354,9 @@ async function startServer( config: ServerConfig, signal: AbortSignal ): Promise process.exit( code ?? 1 ); } ); + stopSignal.throwIfAborted(); + await waitForServerReady( `http://localhost:${ config.port }/`, stopSignal ); + phpProcess = serverChild; } catch ( error ) { if ( spawnedChild && ! spawnedChild.killed ) { @@ -642,6 +643,8 @@ async function ipcMessageHandler( packet: unknown ) { function killPhpProcess(): void { if ( phpProcess && ! phpProcess.killed ) { try { + // Detach the unexpected-exit listener so the imminent SIGKILL is not logged as a crash. + phpProcess.removeAllListeners( 'exit' ); phpProcess.kill( 'SIGKILL' ); } catch { // Best effort — nothing useful to do if this fails. @@ -649,12 +652,25 @@ function killPhpProcess(): void { } } +function shutdownOnSignal( signal: NodeJS.Signals ): void { + logToConsole( `Received ${ signal }, shutting down` ); + killPhpProcess(); + // Follow the Unix convention of `128 + signum` so the exit code reflects the signal. + const signum = os.constants.signals[ signal ] ?? 0; + process.exit( 128 + signum ); +} + // If this node process is going down (normal exit or IPC disconnect), make sure PHP goes with it. process.on( 'exit', killPhpProcess ); process.on( 'disconnect', () => { killPhpProcess(); } ); +// Without explicit signal handlers, the process is terminated abruptly and the 'exit' event +// does not fire — leaving the PHP child orphaned. These handlers ensure cleanup runs. +process.on( 'SIGTERM', shutdownOnSignal ); +process.on( 'SIGINT', shutdownOnSignal ); + if ( process.send ) { process.on( 'message', ipcMessageHandler ); process.send( { topic: 'ready' } ); diff --git a/apps/cli/process-manager-daemon.ts b/apps/cli/process-manager-daemon.ts index b41274d821..6ac0d9624e 100644 --- a/apps/cli/process-manager-daemon.ts +++ b/apps/cli/process-manager-daemon.ts @@ -217,6 +217,7 @@ export class ProcessManagerDaemon { env, stdio: [ 'ignore', 'pipe', 'pipe', 'ipc' ], windowsHide: true, + detached: process.platform !== 'win32', } ); const managedProcess: ManagedProcessRunning = { @@ -293,7 +294,7 @@ export class ProcessManagerDaemon { await new Promise< void >( ( resolve ) => { const timeoutId = setTimeout( () => { - managedProcess.child.kill( 'SIGKILL' ); + this.forceCleanupChild( managedProcess ); }, STOP_TIMEOUT_MS ); managedProcess.child.once( 'exit', () => { @@ -308,7 +309,7 @@ export class ProcessManagerDaemon { resolve(); } ); - managedProcess.child.kill( 'SIGTERM' ); + this.signalProcessGroup( managedProcess, 'SIGTERM' ); } ); } @@ -418,8 +419,35 @@ export class ProcessManagerDaemon { if ( managedProcess.settled ) { continue; } + this.forceCleanupChild( managedProcess ); + } + } + + private forceCleanupChild( managedProcess: ManagedProcess ) { + // On Windows, child.kill() maps any signal to TerminateProcess, so SIGKILL and SIGTERM + // are equivalent there. On non-Windows the helper sends SIGKILL to the whole group. + this.signalProcessGroup( managedProcess, 'SIGKILL' ); + } + + private signalProcessGroup( managedProcess: ManagedProcess, signal: NodeJS.Signals ): void { + if ( process.platform === 'win32' || ! managedProcess.child.pid ) { + try { + managedProcess.child.kill( signal ); + } catch { + // Do nothing + } + return; + } + + // Children are spawned with `detached: true` on non-Windows, so each lives in its own + // process group. Signalling the negative PID delivers to every member of that group, + // including grandchildren (e.g. the PHP server spawned by the wrapper). + try { + process.kill( -managedProcess.child.pid, signal ); + } catch { + // Group send can fail if the leader has already exited but children remain. try { - managedProcess.child.kill( 'SIGKILL' ); + managedProcess.child.kill( signal ); } catch { // Do nothing } @@ -432,10 +460,6 @@ export class ProcessManagerDaemon { } this.shuttingDown = true; - await this.broadcastEvent( { - type: 'daemon-kill', - payload: { reason }, - } ); await Promise.allSettled( Array.from( this.managedProcesses.values() ).map( ( managedProcess ) => @@ -443,6 +467,11 @@ export class ProcessManagerDaemon { ) ); + await this.broadcastEvent( { + type: 'daemon-kill', + payload: { reason }, + } ); + await new Promise< void >( ( resolve ) => { void this.controlServer.close().then( () => resolve() ); } ); diff --git a/apps/studio/src/ipc-handlers.ts b/apps/studio/src/ipc-handlers.ts index 1fcf117e57..d1a3d81375 100644 --- a/apps/studio/src/ipc-handlers.ts +++ b/apps/studio/src/ipc-handlers.ts @@ -85,6 +85,7 @@ import { setSentryWpcomUserIdMain } from 'src/lib/main-sentry-utils'; import * as oauthClient from 'src/lib/oauth'; import { getAiInstructionsPath } from 'src/lib/server-files-paths'; import { shellOpenExternalWrapper } from 'src/lib/shell-open-external-wrapper'; +import { updateSiteUrl } from 'src/lib/update-site-url'; import * as windowsHelpers from 'src/lib/windows-helpers'; import { getLogsFilePath, writeLogToFile, type LogLevel } from 'src/logging'; import { getMainWindow } from 'src/main-window'; @@ -1053,6 +1054,10 @@ export async function copySite( noStart: true, } ); + // Playground sets the correct siteurl internally, but for the native-php runtime, we need to + // explicitly update that option + await updateSiteUrl( server, `http://localhost:${ details.port }` ); + // Persist themeDetails to appdata (Studio-only data) if ( sourceSite.themeDetails ) { server.details.themeDetails = sourceSite.themeDetails; diff --git a/apps/studio/src/modules/cli/lib/execute-export-command.ts b/apps/studio/src/modules/cli/lib/execute-export-command.ts index fd034aba53..9cc03306cd 100644 --- a/apps/studio/src/modules/cli/lib/execute-export-command.ts +++ b/apps/studio/src/modules/cli/lib/execute-export-command.ts @@ -24,7 +24,7 @@ export function executeExportCliCommand( const { abortSignal } = options; function abortExportProcess() { - childProcess.kill( 'SIGKILL' ); + childProcess.kill( 'SIGTERM' ); emitFailure( new Error( 'Export aborted' ) ); } diff --git a/tools/common/lib/php-binary-metadata.ts b/tools/common/lib/php-binary-metadata.ts index 13068023ec..837e01eebc 100644 --- a/tools/common/lib/php-binary-metadata.ts +++ b/tools/common/lib/php-binary-metadata.ts @@ -39,13 +39,13 @@ export const PHP_PATCH_VERSIONS: Record< NativePhpSupportedVersion, string > = { // Windows ARM64 falls back to x64, so there is no separate win32-arm64 entry. export const PHP_BINARY_HASHES: Record< string, string > = { // PHP 8.5 (8.5.5) - '8.5-darwin-arm64': 'd66ef557189baa9215cec65dba80eb5dcfa84ac87bfce565059263bce5c9cccb', + '8.5-darwin-arm64': '025d07dc55e806f98fc36871cf8b342228379b9d5ac3d739d66744a2b550de2a', '8.5-darwin-x64': 'f206193e86c6ca188fbb46532c432ff3f216f203be9fe67bfc839973de64f9b8', '8.5-linux-x64': '72a877af1cb93d7c14f79344bdbd78dc2a57a2e915a76b13e9a3141700df3f21', '8.5-linux-arm64': 'a45fc1f818497586bfd8f749c808f1f63c9d10bf48f439985e77bee607a2c76b', '8.5-win32-x64': 'ddd8098a1e71dfe53c147c392eeaa50eb61259a1c430e3cbe016b0edbdc1cf91', // PHP 8.4 (8.4.20) - '8.4-darwin-arm64': '63b676b3e638aae10055a795ad25a820451fdfc79afb9f99b1605bad61560679', + '8.4-darwin-arm64': 'e1ce00874e398bcef5884f2b9067a984480e7dd32d256747f89a75e5f183113c', '8.4-darwin-x64': '6fc09f87d9676bf8f22b05cf77a91b99ee120414e9a028c6f357c18df531fa83', '8.4-linux-x64': '3624293e0556625e19f4483d74eac21d41b70d21bdbb7e8ea3e1247303886148', '8.4-linux-arm64': '1be37b0cc533edc691632828ecdaddd84eaa0f71bb9c06a3458347aa13da2987',