2026-03-07 21:20:29 -05:00
import { spawnSync } from "node:child_process" ;
import fsSync from "node:fs" ;
import { isRestartEnabled } from "../../config/commands.js" ;
2026-03-07 20:48:13 -05:00
import { readBestEffortConfig , resolveGatewayPort } from "../../config/config.js" ;
2026-03-08 03:34:36 +00:00
import { parseCmdScriptCommandLine } from "../../daemon/cmd-argv.js" ;
2026-01-14 01:08:15 +00:00
import { resolveGatewayService } from "../../daemon/service.js" ;
2026-03-07 21:20:29 -05:00
import { probeGateway } from "../../gateway/probe.js" ;
2026-03-08 19:41:05 +00:00
import { isGatewayArgv , parseProcCmdline } from "../../infra/gateway-process-argv.js" ;
2026-03-07 21:20:29 -05:00
import { findGatewayPidsOnPortSync } from "../../infra/restart.js" ;
2026-02-21 18:02:05 +01:00
import { defaultRuntime } from "../../runtime.js" ;
import { theme } from "../../terminal/theme.js" ;
import { formatCliCommand } from "../command-format.js" ;
2026-02-14 14:00:34 +00:00
import {
runServiceRestart ,
runServiceStart ,
runServiceStop ,
runServiceUninstall ,
} from "./lifecycle-core.js" ;
2026-02-21 18:02:05 +01:00
import {
2026-02-23 01:49:54 +01:00
DEFAULT_RESTART_HEALTH_ATTEMPTS ,
DEFAULT_RESTART_HEALTH_DELAY_MS ,
2026-03-07 21:20:29 -05:00
renderGatewayPortHealthDiagnostics ,
2026-02-21 18:02:05 +01:00
renderRestartDiagnostics ,
terminateStaleGatewayPids ,
2026-03-07 21:20:29 -05:00
waitForGatewayHealthyListener ,
2026-02-21 18:02:05 +01:00
waitForGatewayHealthyRestart ,
} from "./restart-health.js" ;
import { parsePortFromArgs , renderGatewayServiceStartHints } from "./shared.js" ;
2026-02-18 01:34:35 +00:00
import type { DaemonLifecycleOptions } from "./types.js" ;
2026-01-14 01:08:15 +00:00
2026-02-23 01:49:54 +01:00
const POST_RESTART_HEALTH_ATTEMPTS = DEFAULT_RESTART_HEALTH_ATTEMPTS ;
const POST_RESTART_HEALTH_DELAY_MS = DEFAULT_RESTART_HEALTH_DELAY_MS ;
2026-02-21 18:02:05 +01:00
2026-03-07 21:20:29 -05:00
async function resolveGatewayLifecyclePort ( service = resolveGatewayService ( ) ) {
2026-02-21 18:02:05 +01:00
const command = await service . readCommand ( process . env ) . catch ( ( ) = > null ) ;
const serviceEnv = command ? . environment ? ? undefined ;
const mergedEnv = {
. . . ( process . env as Record < string , string | undefined > ) ,
. . . ( serviceEnv ? ? undefined ) ,
} as NodeJS . ProcessEnv ;
const portFromArgs = parsePortFromArgs ( command ? . programArguments ) ;
2026-03-07 20:48:13 -05:00
return portFromArgs ? ? resolveGatewayPort ( await readBestEffortConfig ( ) , mergedEnv ) ;
2026-02-21 18:02:05 +01:00
}
2026-03-08 03:34:36 +00:00
function extractWindowsCommandLine ( raw : string ) : string | null {
const lines = raw
. split ( /\r?\n/ )
. map ( ( line ) = > line . trim ( ) )
. filter ( Boolean ) ;
for ( const line of lines ) {
if ( ! line . toLowerCase ( ) . startsWith ( "commandline=" ) ) {
continue ;
}
const value = line . slice ( "commandline=" . length ) . trim ( ) ;
return value || null ;
}
return lines . find ( ( line ) = > line . toLowerCase ( ) !== "commandline" ) ? ? null ;
}
2026-03-07 21:20:29 -05:00
function readGatewayProcessArgsSync ( pid : number ) : string [ ] | null {
if ( process . platform === "linux" ) {
try {
return parseProcCmdline ( fsSync . readFileSync ( ` /proc/ ${ pid } /cmdline ` , "utf8" ) ) ;
} catch {
return null ;
}
}
if ( process . platform === "darwin" ) {
const ps = spawnSync ( "ps" , [ "-o" , "command=" , "-p" , String ( pid ) ] , {
encoding : "utf8" ,
timeout : 1000 ,
} ) ;
if ( ps . error || ps . status !== 0 ) {
return null ;
}
const command = ps . stdout . trim ( ) ;
return command ? command . split ( /\s+/ ) : null ;
}
2026-03-08 03:34:36 +00:00
if ( process . platform === "win32" ) {
const wmic = spawnSync (
"wmic" ,
[ "process" , "where" , ` ProcessId= ${ pid } ` , "get" , "CommandLine" , "/value" ] ,
{
encoding : "utf8" ,
timeout : 1000 ,
} ,
) ;
if ( wmic . error || wmic . status !== 0 ) {
return null ;
}
const command = extractWindowsCommandLine ( wmic . stdout ) ;
return command ? parseCmdScriptCommandLine ( command ) : null ;
}
2026-03-07 21:20:29 -05:00
return null ;
}
function resolveGatewayListenerPids ( port : number ) : number [ ] {
return Array . from ( new Set ( findGatewayPidsOnPortSync ( port ) ) )
. filter ( ( pid ) : pid is number = > Number . isFinite ( pid ) && pid > 0 )
. filter ( ( pid ) = > {
const args = readGatewayProcessArgsSync ( pid ) ;
2026-03-08 19:41:05 +00:00
return args != null && isGatewayArgv ( args , { allowGatewayBinary : true } ) ;
2026-03-07 21:20:29 -05:00
} ) ;
}
function resolveGatewayPortFallback ( ) : Promise < number > {
return readBestEffortConfig ( )
. then ( ( cfg ) = > resolveGatewayPort ( cfg , process . env ) )
. catch ( ( ) = > resolveGatewayPort ( undefined , process . env ) ) ;
}
function signalGatewayPid ( pid : number , signal : "SIGTERM" | "SIGUSR1" ) {
const args = readGatewayProcessArgsSync ( pid ) ;
2026-03-08 19:41:05 +00:00
if ( ! args || ! isGatewayArgv ( args , { allowGatewayBinary : true } ) ) {
2026-03-07 21:20:29 -05:00
throw new Error ( ` refusing to signal non-gateway process pid ${ pid } ` ) ;
}
process . kill ( pid , signal ) ;
}
function formatGatewayPidList ( pids : number [ ] ) : string {
return pids . join ( ", " ) ;
}
async function assertUnmanagedGatewayRestartEnabled ( port : number ) : Promise < void > {
const probe = await probeGateway ( {
url : ` ws://127.0.0.1: ${ port } ` ,
auth : {
token : process.env.OPENCLAW_GATEWAY_TOKEN?.trim ( ) || undefined ,
password : process.env.OPENCLAW_GATEWAY_PASSWORD?.trim ( ) || undefined ,
} ,
timeoutMs : 1_000 ,
} ) . catch ( ( ) = > null ) ;
if ( ! probe ? . ok ) {
return ;
}
if ( ! isRestartEnabled ( probe . configSnapshot as { commands? : unknown } | undefined ) ) {
throw new Error (
"Gateway restart is disabled in the running gateway config (commands.restart=false); unmanaged SIGUSR1 restart would be ignored" ,
) ;
}
}
function resolveVerifiedGatewayListenerPids ( port : number ) : number [ ] {
return resolveGatewayListenerPids ( port ) . filter (
( pid ) : pid is number = > Number . isFinite ( pid ) && pid > 0 ,
) ;
}
async function stopGatewayWithoutServiceManager ( port : number ) {
const pids = resolveVerifiedGatewayListenerPids ( port ) ;
if ( pids . length === 0 ) {
return null ;
}
for ( const pid of pids ) {
signalGatewayPid ( pid , "SIGTERM" ) ;
}
return {
result : "stopped" as const ,
message : ` Gateway stop signal sent to unmanaged process ${ pids . length === 1 ? "" : "es" } on port ${ port } : ${ formatGatewayPidList ( pids ) } . ` ,
} ;
}
async function restartGatewayWithoutServiceManager ( port : number ) {
await assertUnmanagedGatewayRestartEnabled ( port ) ;
const pids = resolveVerifiedGatewayListenerPids ( port ) ;
if ( pids . length === 0 ) {
return null ;
}
if ( pids . length > 1 ) {
throw new Error (
` multiple gateway processes are listening on port ${ port } : ${ formatGatewayPidList ( pids ) } ; use "openclaw gateway status --deep" before retrying restart ` ,
) ;
}
signalGatewayPid ( pids [ 0 ] , "SIGUSR1" ) ;
return {
result : "restarted" as const ,
message : ` Gateway restart signal sent to unmanaged process on port ${ port } : ${ pids [ 0 ] } . ` ,
} ;
}
2026-01-16 05:40:35 +00:00
export async function runDaemonUninstall ( opts : DaemonLifecycleOptions = { } ) {
2026-02-14 14:00:34 +00:00
return await runServiceUninstall ( {
serviceNoun : "Gateway" ,
service : resolveGatewayService ( ) ,
opts ,
stopBeforeUninstall : true ,
assertNotLoadedAfterUninstall : true ,
2026-01-16 05:40:35 +00:00
} ) ;
2026-01-14 01:08:15 +00:00
}
2026-01-16 05:40:35 +00:00
export async function runDaemonStart ( opts : DaemonLifecycleOptions = { } ) {
2026-02-14 14:00:34 +00:00
return await runServiceStart ( {
serviceNoun : "Gateway" ,
service : resolveGatewayService ( ) ,
renderStartHints : renderGatewayServiceStartHints ,
opts ,
2026-01-16 05:40:35 +00:00
} ) ;
2026-01-14 01:08:15 +00:00
}
2026-01-16 05:40:35 +00:00
export async function runDaemonStop ( opts : DaemonLifecycleOptions = { } ) {
2026-03-07 21:20:29 -05:00
const service = resolveGatewayService ( ) ;
const gatewayPort = await resolveGatewayLifecyclePort ( service ) . catch ( ( ) = >
resolveGatewayPortFallback ( ) ,
) ;
2026-02-14 14:00:34 +00:00
return await runServiceStop ( {
serviceNoun : "Gateway" ,
2026-03-07 21:20:29 -05:00
service ,
2026-02-14 14:00:34 +00:00
opts ,
2026-03-07 21:20:29 -05:00
onNotLoaded : async ( ) = > stopGatewayWithoutServiceManager ( gatewayPort ) ,
2026-01-16 05:40:35 +00:00
} ) ;
2026-01-14 01:08:15 +00:00
}
/ * *
2026-01-21 17:45:06 +00:00
* Restart the gateway service service .
2026-01-14 01:08:15 +00:00
* @returns ` true ` if restart succeeded , ` false ` if the service was not loaded .
* Throws / exits on check or restart failures .
* /
2026-01-16 05:40:35 +00:00
export async function runDaemonRestart ( opts : DaemonLifecycleOptions = { } ) : Promise < boolean > {
2026-02-21 18:02:05 +01:00
const json = Boolean ( opts . json ) ;
const service = resolveGatewayService ( ) ;
2026-03-07 21:20:29 -05:00
let restartedWithoutServiceManager = false ;
const restartPort = await resolveGatewayLifecyclePort ( service ) . catch ( ( ) = >
resolveGatewayPortFallback ( ) ,
2026-02-21 18:02:05 +01:00
) ;
2026-02-23 01:49:54 +01:00
const restartWaitMs = POST_RESTART_HEALTH_ATTEMPTS * POST_RESTART_HEALTH_DELAY_MS ;
const restartWaitSeconds = Math . round ( restartWaitMs / 1000 ) ;
2026-02-21 18:02:05 +01:00
2026-02-14 14:00:34 +00:00
return await runServiceRestart ( {
serviceNoun : "Gateway" ,
2026-02-21 18:02:05 +01:00
service ,
2026-02-14 14:00:34 +00:00
renderStartHints : renderGatewayServiceStartHints ,
opts ,
2026-02-17 08:44:07 -05:00
checkTokenDrift : true ,
2026-03-07 21:20:29 -05:00
onNotLoaded : async ( ) = > {
const handled = await restartGatewayWithoutServiceManager ( restartPort ) ;
if ( handled ) {
restartedWithoutServiceManager = true ;
}
return handled ;
} ,
2026-02-21 18:02:05 +01:00
postRestartCheck : async ( { warnings , fail , stdout } ) = > {
2026-03-07 21:20:29 -05:00
if ( restartedWithoutServiceManager ) {
const health = await waitForGatewayHealthyListener ( {
port : restartPort ,
attempts : POST_RESTART_HEALTH_ATTEMPTS ,
delayMs : POST_RESTART_HEALTH_DELAY_MS ,
} ) ;
if ( health . healthy ) {
return ;
}
const diagnostics = renderGatewayPortHealthDiagnostics ( health ) ;
const timeoutLine = ` Timed out after ${ restartWaitSeconds } s waiting for gateway port ${ restartPort } to become healthy. ` ;
if ( ! json ) {
defaultRuntime . log ( theme . warn ( timeoutLine ) ) ;
for ( const line of diagnostics ) {
defaultRuntime . log ( theme . muted ( line ) ) ;
}
} else {
warnings . push ( timeoutLine ) ;
warnings . push ( . . . diagnostics ) ;
}
fail ( ` Gateway restart timed out after ${ restartWaitSeconds } s waiting for health checks. ` , [
formatCliCommand ( "openclaw gateway status --deep" ) ,
formatCliCommand ( "openclaw doctor" ) ,
] ) ;
}
2026-02-21 18:02:05 +01:00
let health = await waitForGatewayHealthyRestart ( {
service ,
port : restartPort ,
attempts : POST_RESTART_HEALTH_ATTEMPTS ,
delayMs : POST_RESTART_HEALTH_DELAY_MS ,
2026-03-02 14:24:25 +00:00
includeUnknownListenersAsStale : process.platform === "win32" ,
2026-02-21 18:02:05 +01:00
} ) ;
if ( ! health . healthy && health . staleGatewayPids . length > 0 ) {
const staleMsg = ` Found stale gateway process(es): ${ health . staleGatewayPids . join ( ", " ) } . ` ;
warnings . push ( staleMsg ) ;
if ( ! json ) {
defaultRuntime . log ( theme . warn ( staleMsg ) ) ;
defaultRuntime . log ( theme . muted ( "Stopping stale process(es) and retrying restart..." ) ) ;
}
await terminateStaleGatewayPids ( health . staleGatewayPids ) ;
await service . restart ( { env : process.env , stdout } ) ;
health = await waitForGatewayHealthyRestart ( {
service ,
port : restartPort ,
attempts : POST_RESTART_HEALTH_ATTEMPTS ,
delayMs : POST_RESTART_HEALTH_DELAY_MS ,
2026-03-02 14:24:25 +00:00
includeUnknownListenersAsStale : process.platform === "win32" ,
2026-02-21 18:02:05 +01:00
} ) ;
}
if ( health . healthy ) {
return ;
}
const diagnostics = renderRestartDiagnostics ( health ) ;
2026-02-23 01:49:54 +01:00
const timeoutLine = ` Timed out after ${ restartWaitSeconds } s waiting for gateway port ${ restartPort } to become healthy. ` ;
const runningNoPortLine =
health . runtime . status === "running" && health . portUsage . status === "free"
? ` Gateway process is running but port ${ restartPort } is still free (startup hang/crash loop or very slow VM startup). `
: null ;
2026-02-21 18:02:05 +01:00
if ( ! json ) {
2026-02-23 01:49:54 +01:00
defaultRuntime . log ( theme . warn ( timeoutLine ) ) ;
if ( runningNoPortLine ) {
defaultRuntime . log ( theme . warn ( runningNoPortLine ) ) ;
}
2026-02-21 18:02:05 +01:00
for ( const line of diagnostics ) {
defaultRuntime . log ( theme . muted ( line ) ) ;
}
} else {
2026-02-23 01:49:54 +01:00
warnings . push ( timeoutLine ) ;
if ( runningNoPortLine ) {
warnings . push ( runningNoPortLine ) ;
}
2026-02-21 18:02:05 +01:00
warnings . push ( . . . diagnostics ) ;
}
2026-02-23 01:49:54 +01:00
fail ( ` Gateway restart timed out after ${ restartWaitSeconds } s waiting for health checks. ` , [
2026-02-24 09:19:59 +00:00
formatCliCommand ( "openclaw gateway status --deep" ) ,
2026-02-21 18:02:05 +01:00
formatCliCommand ( "openclaw doctor" ) ,
] ) ;
} ,
2026-02-14 14:00:34 +00:00
} ) ;
2026-01-14 01:08:15 +00:00
}