2026-03-07 21:20:29 -05:00
import { isRestartEnabled } from "../../config/commands.js" ;
2026-03-07 20:48:13 -05:00
import { readBestEffortConfig , resolveGatewayPort } from "../../config/config.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-13 18:33:59 +00:00
import {
findVerifiedGatewayListenerPidsOnPortSync ,
formatGatewayPidList ,
signalVerifiedGatewayPidSync ,
} from "../../infra/gateway-processes.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-07 21:20:29 -05:00
function resolveGatewayPortFallback ( ) : Promise < number > {
return readBestEffortConfig ( )
. then ( ( cfg ) = > resolveGatewayPort ( cfg , process . env ) )
. catch ( ( ) = > resolveGatewayPort ( undefined , process . env ) ) ;
}
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 [ ] {
2026-03-13 18:33:59 +00:00
return findVerifiedGatewayListenerPidsOnPortSync ( port ) . filter (
2026-03-07 21:20:29 -05:00
( 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 ) {
2026-03-13 18:33:59 +00:00
signalVerifiedGatewayPidSync ( pid , "SIGTERM" ) ;
2026-03-07 21:20:29 -05:00
}
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 ` ,
) ;
}
2026-03-13 18:33:59 +00:00
signalVerifiedGatewayPidSync ( pids [ 0 ] , "SIGUSR1" ) ;
2026-03-07 21:20:29 -05:00
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 ) ;
2026-03-12 01:38:39 +00:00
const retryRestart = await service . restart ( { env : process.env , stdout } ) ;
if ( retryRestart . outcome === "scheduled" ) {
return retryRestart ;
}
2026-02-21 18:02:05 +01:00
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
}