| import { spawn } from "node:child_process"; |
| import fs from "node:fs/promises"; |
| import os from "node:os"; |
| import path from "node:path"; |
| import { resolveDefaultGatewayPort } from "../../config/port-defaults.js"; |
| import { quoteCmdScriptArg } from "../../daemon/cmd-argv.js"; |
| import { |
| resolveGatewayLaunchAgentLabel, |
| resolveGatewaySystemdServiceName, |
| resolveGatewayWindowsTaskName, |
| } from "../../daemon/constants.js"; |
|
|
| |
| |
| |
| |
| |
| function shellEscape(value: string): string { |
| return value.replace(/'/g, "'\\''"); |
| } |
|
|
| |
| function isBatchSafe(value: string): boolean { |
| |
| return /^[A-Za-z0-9 _\-().]+$/.test(value); |
| } |
|
|
| function resolveSystemdUnit(env: NodeJS.ProcessEnv): string { |
| const override = env.OPENCLAW_SYSTEMD_UNIT?.trim(); |
| if (override) { |
| return override.endsWith(".service") ? override : `${override}.service`; |
| } |
| return `${resolveGatewaySystemdServiceName(env.OPENCLAW_PROFILE)}.service`; |
| } |
|
|
| function resolveLaunchdLabel(env: NodeJS.ProcessEnv): string { |
| const override = env.OPENCLAW_LAUNCHD_LABEL?.trim(); |
| if (override) { |
| return override; |
| } |
| return resolveGatewayLaunchAgentLabel(env.OPENCLAW_PROFILE); |
| } |
|
|
| function resolveWindowsTaskName(env: NodeJS.ProcessEnv): string { |
| const override = env.OPENCLAW_WINDOWS_TASK_NAME?.trim(); |
| if (override) { |
| return override; |
| } |
| return resolveGatewayWindowsTaskName(env.OPENCLAW_PROFILE); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export async function prepareRestartScript( |
| env: NodeJS.ProcessEnv = process.env, |
| gatewayPort: number = resolveDefaultGatewayPort(), |
| ): Promise<string | null> { |
| const tmpDir = os.tmpdir(); |
| const timestamp = Date.now(); |
| const platform = process.platform; |
|
|
| let scriptContent = ""; |
| let filename = ""; |
|
|
| try { |
| if (platform === "linux") { |
| const unitName = resolveSystemdUnit(env); |
| const escaped = shellEscape(unitName); |
| filename = `openclaw-restart-${timestamp}.sh`; |
| scriptContent = `#!/bin/sh |
| # Standalone restart script — survives parent process termination. |
| # Wait briefly to ensure file locks are released after update. |
| sleep 1 |
| systemctl --user restart '${escaped}' |
| # Self-cleanup |
| rm -f "$0" |
| `; |
| } else if (platform === "darwin") { |
| const label = resolveLaunchdLabel(env); |
| const escaped = shellEscape(label); |
| |
| const uid = process.getuid ? process.getuid() : 501; |
| |
| |
| const home = env.HOME?.trim() || process.env.HOME || os.homedir(); |
| const plistPath = path.join(home, "Library", "LaunchAgents", `${label}.plist`); |
| const escapedPlistPath = shellEscape(plistPath); |
| filename = `openclaw-restart-${timestamp}.sh`; |
| scriptContent = `#!/bin/sh |
| # Standalone restart script — survives parent process termination. |
| # Wait briefly to ensure file locks are released after update. |
| sleep 1 |
| # Try kickstart first (works when the service is still registered). |
| # If it fails (e.g. after bootout), clear any persisted disabled state, |
| # then re-register via bootstrap and kickstart. |
| if ! launchctl kickstart -k 'gui/${uid}/${escaped}' 2>/dev/null; then |
| launchctl enable 'gui/${uid}/${escaped}' 2>/dev/null |
| launchctl bootstrap 'gui/${uid}' '${escapedPlistPath}' 2>/dev/null |
| launchctl kickstart -k 'gui/${uid}/${escaped}' 2>/dev/null || true |
| fi |
| # Self-cleanup |
| rm -f "$0" |
| `; |
| } else if (platform === "win32") { |
| const taskName = resolveWindowsTaskName(env); |
| if (!isBatchSafe(taskName)) { |
| return null; |
| } |
| const port = |
| Number.isFinite(gatewayPort) && gatewayPort > 0 ? gatewayPort : resolveDefaultGatewayPort(); |
| filename = `openclaw-restart-${timestamp}.bat`; |
| scriptContent = `@echo off |
| REM Standalone restart script — survives parent process termination. |
| REM Wait briefly to ensure file locks are released after update. |
| timeout /t 2 /nobreak >nul |
| schtasks /End /TN "${taskName}" |
| REM Poll for gateway port release before rerun; force-kill listener if stuck. |
| set /a attempts=0 |
| :wait_for_port_release |
| set /a attempts+=1 |
| netstat -ano | findstr /R /C:":${port} .*LISTENING" >nul |
| if errorlevel 1 goto port_released |
| if %attempts% GEQ 10 goto force_kill_listener |
| timeout /t 1 /nobreak >nul |
| goto wait_for_port_release |
| :force_kill_listener |
| for /f "tokens=5" %%P in ('netstat -ano ^| findstr /R /C:":${port} .*LISTENING"') do ( |
| taskkill /F /PID %%P >nul 2>&1 |
| goto port_released |
| ) |
| :port_released |
| schtasks /Run /TN "${taskName}" |
| REM Self-cleanup |
| del "%~f0" |
| `; |
| } else { |
| return null; |
| } |
|
|
| const scriptPath = path.join(tmpDir, filename); |
| await fs.writeFile(scriptPath, scriptContent, { mode: 0o755 }); |
| return scriptPath; |
| } catch { |
| |
| return null; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function runRestartScript(scriptPath: string): Promise<void> { |
| const isWindows = process.platform === "win32"; |
| const file = isWindows ? "cmd.exe" : "/bin/sh"; |
| const args = isWindows ? ["/d", "/s", "/c", quoteCmdScriptArg(scriptPath)] : [scriptPath]; |
|
|
| const child = spawn(file, args, { |
| detached: true, |
| stdio: "ignore", |
| windowsHide: true, |
| }); |
| child.unref(); |
| } |
|
|