UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

962 lines (961 loc) 36.1 kB
import { a as normalizeLowercaseStringOrEmpty } from "./string-coerce-mnp54Vah.js"; import { n as parseTcpPort } from "./tcp-port-DPgvEEt3.js"; import { _ as uniqueStrings } from "./string-normalization-WNUDCpXX.js"; import { _ as sleep } from "./utils-CCC-BEJH.js"; import { f as resolveGatewayServiceDescription, m as resolveGatewayWindowsTaskName, o as NODE_SERVICE_KIND } from "./constants-e049XCW0.js"; import { n as resolveGatewayTaskScriptPath } from "./paths-Qb6rRFUH.js"; import { t as getWindowsInstallRoots } from "./windows-install-roots-jk5k_ejK.js"; import { t as killProcessTree } from "./kill-tree-kSm0C74g.js"; import { o as inspectPortUsage } from "./ports-CoxQQbiY.js"; import { t as isGatewayArgv } from "./gateway-process-argv-BPpPk56q.js"; import { a as renderCmdSetAssignment, i as parseCmdSetAssignment, n as quoteCmdScriptArg, r as assertNoCmdLineBreak, t as parseCmdScriptCommandLine } from "./cmd-argv-DYSpLFnE.js"; import { i as writeFormattedLines, n as formatLine, t as parseKeyValueOutput } from "./runtime-parse-BllMHmcZ.js"; import { t as findVerifiedGatewayListenerPidsOnPortSync } from "./gateway-processes-cAJXq3M3.js"; import { t as execSchtasks } from "./schtasks-exec-CWvod29b.js"; import path from "node:path"; import fs from "node:fs/promises"; import os from "node:os"; import { spawn, spawnSync } from "node:child_process"; //#region src/daemon/schtasks.ts /** Windows Task Scheduler installer, startup fallback, and lifecycle controls. */ function resolveTaskName(env) { const override = env.OPENCLAW_WINDOWS_TASK_NAME?.trim(); if (override) return override; return resolveGatewayWindowsTaskName(env.OPENCLAW_PROFILE); } function shouldFallbackToStartupEntry(params) { return params.code === 1 || /(?:access is denied|acceso denegado)/i.test(params.detail) || params.code === 124 || /schtasks timed out/i.test(params.detail) || /schtasks produced no output/i.test(params.detail); } function resolveTaskScriptPath(env) { return resolveGatewayTaskScriptPath(env); } function resolveWindowsStartupDir(env) { const appData = env.APPDATA?.trim(); if (appData) return path.join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup"); const home = env.USERPROFILE?.trim() || env.HOME?.trim(); if (!home) throw new Error("Windows startup folder unavailable: APPDATA/USERPROFILE not set"); return path.join(home, "AppData", "Roaming", "Microsoft", "Windows", "Start Menu", "Programs", "Startup"); } function sanitizeWindowsFilename(value) { return value.replace(/[<>:"/\\|?*]/g, "_").replace(/\p{Cc}/gu, "_"); } function resolveStartupEntryPath(env, extension) { const taskName = resolveTaskName(env); const entryExtension = extension ?? (shouldUseHiddenWindowsTaskLauncher(env) ? "vbs" : "cmd"); return path.join(resolveWindowsStartupDir(env), `${sanitizeWindowsFilename(taskName)}.${entryExtension}`); } function resolveStartupEntryPaths(env) { return uniqueStrings([resolveStartupEntryPath(env), resolveStartupEntryPath(env, "cmd")]); } function quoteSchtasksArg(value) { if (!/[ \t"]/g.test(value)) return value; return `"${value.replace(/"/g, "\\\"")}"`; } function escapeXmlText(value) { return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;"); } function buildScheduledTaskXml(params) { const description = escapeXmlText(params.taskDescription); const command = escapeXmlText(params.launchPath); const principalLogon = params.taskUser ? `\n <UserId>${escapeXmlText(params.taskUser)}</UserId>\n <LogonType>InteractiveToken</LogonType>` : "\n <GroupId>S-1-5-32-545</GroupId>"; return `<?xml version="1.0" encoding="UTF-16"?> <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"> <RegistrationInfo> <Description>${description}</Description> </RegistrationInfo> <Triggers> <LogonTrigger> <Enabled>true</Enabled>${params.taskUser ? `\n <UserId>${escapeXmlText(params.taskUser)}</UserId>` : ""} </LogonTrigger> </Triggers> <Principals> <Principal id="Author">${principalLogon} <RunLevel>LeastPrivilege</RunLevel> </Principal> </Principals> <Settings> <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy> <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries> <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries> <AllowHardTerminate>true</AllowHardTerminate> <StartWhenAvailable>false</StartWhenAvailable> <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable> <IdleSettings> <StopOnIdleEnd>false</StopOnIdleEnd> <RestartOnIdle>false</RestartOnIdle> </IdleSettings> <AllowStartOnDemand>true</AllowStartOnDemand> <Enabled>true</Enabled> <Hidden>false</Hidden> <RunOnlyIfIdle>false</RunOnlyIfIdle> <WakeToRun>false</WakeToRun> <ExecutionTimeLimit>PT0S</ExecutionTimeLimit> <Priority>7</Priority> </Settings> <Actions Context="Author"> <Exec> <Command>${command}</Command> </Exec> </Actions> </Task>`; } async function writeTaskXmlTempFile(xml) { const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-task-xml-")); const xmlPath = path.join(tmpDir, "task.xml"); const bom = Buffer.from([255, 254]); const body = Buffer.from(xml, "utf16le"); await fs.writeFile(xmlPath, Buffer.concat([bom, body])); return xmlPath; } function resolveTaskUser(env) { const username = env.USERNAME || env.USER || env.LOGNAME; if (!username) return null; if (username.includes("\\")) return username; const domain = env.USERDOMAIN; if (normalizeLowercaseStringOrEmpty(domain) === "workgroup") return username; if (domain) return `${domain}\\${username}`; return username; } function resolveSchtasksCreateUser(env, taskUser) { if (normalizeLowercaseStringOrEmpty(env.USERDOMAIN) === "workgroup") return null; return taskUser; } function shouldUseHiddenWindowsTaskLauncher(env) { const value = normalizeLowercaseStringOrEmpty(env.OPENCLAW_WINDOWS_TASK_HIDDEN_LAUNCHER); return value === "1" || value === "true" || value === "yes"; } function resolveTaskLauncherScriptPath(env, scriptPath) { if (!shouldUseHiddenWindowsTaskLauncher(env)) return scriptPath; const parsed = path.parse(scriptPath); return path.join(parsed.dir, `${parsed.name}.vbs`); } async function readScheduledTaskCommand(env) { const scriptPath = resolveTaskScriptPath(env); try { const content = await fs.readFile(scriptPath, "utf8"); let workingDirectory = ""; let commandLine = ""; const environment = {}; for (const rawLine of content.split(/\r?\n/)) { const line = rawLine.trim(); if (!line) continue; const lower = normalizeLowercaseStringOrEmpty(line); if (line.startsWith("@echo")) continue; if (lower.startsWith("rem ")) continue; if (lower.startsWith("set ")) { const assignment = parseCmdSetAssignment(line.slice(4)); if (assignment) environment[assignment.key] = assignment.value; continue; } if (lower.startsWith("cd /d ")) { workingDirectory = line.slice(6).trim().replace(/^"|"$/g, ""); continue; } commandLine = line; break; } if (!commandLine) return null; return { programArguments: parseCmdScriptCommandLine(commandLine), ...workingDirectory ? { workingDirectory } : {}, ...Object.keys(environment).length > 0 ? { environment } : {}, ...Object.keys(environment).length > 0 ? { environmentValueSources: Object.fromEntries(Object.keys(environment).map((key) => [key, "inline"])) } : {}, sourcePath: scriptPath }; } catch { return null; } } function hasListenerPid(listener) { return typeof listener.pid === "number"; } function parseSchtasksQuery(output) { const entries = parseKeyValueOutput(output, ":"); const info = {}; const status = entries.status; if (status) info.status = status; const lastRunTime = entries["last run time"]; if (lastRunTime) info.lastRunTime = lastRunTime; const lastRunResult = entries["last run result"] ?? entries["last result"]; if (lastRunResult) info.lastRunResult = lastRunResult; return info; } function normalizeTaskResultCode(value) { if (!value) return null; const raw = normalizeLowercaseStringOrEmpty(value); if (!raw) return null; if (/^0x[0-9a-f]+$/.test(raw)) return `0x${raw.slice(2).replace(/^0+/, "") || "0"}`; if (/^\d+$/.test(raw)) { const numeric = Number.parseInt(raw, 10); if (Number.isFinite(numeric)) return `0x${numeric.toString(16)}`; } return null; } const RUNNING_RESULT_CODES = new Set(["0x41301"]); const NOT_YET_RUN_RESULT_CODES = new Set(["0x41303"]); const UNKNOWN_STATUS_DETAIL = "Task status is locale-dependent and no numeric Last Run Result was available."; const SCHEDULED_TASK_FALLBACK_POLL_MS = 250; const SCHEDULED_TASK_FALLBACK_TIMEOUT_MS = 15e3; function deriveScheduledTaskRuntimeStatus(parsed) { const normalizedResult = normalizeTaskResultCode(parsed.lastRunResult); if (normalizedResult != null) { if (RUNNING_RESULT_CODES.has(normalizedResult)) return { status: "running" }; return { status: "stopped", detail: `Task Last Run Result=${parsed.lastRunResult}; treating as not running.` }; } if (parsed.status?.trim()) return { status: "unknown", detail: UNKNOWN_STATUS_DETAIL }; return { status: "unknown" }; } function buildTaskScript({ description, programArguments, workingDirectory, environment }) { const lines = ["@echo off"]; const trimmedDescription = description?.trim(); if (trimmedDescription) { assertNoCmdLineBreak(trimmedDescription, "Task description"); lines.push(`rem ${trimmedDescription}`); } if (workingDirectory) lines.push(`cd /d ${quoteCmdScriptArg(workingDirectory)}`); if (environment) for (const [key, value] of Object.entries(environment)) { if (!value) continue; if (key.toUpperCase() === "PATH") continue; lines.push(renderCmdSetAssignment(key, value)); } const command = programArguments.map(quoteCmdScriptArg).join(" "); lines.push(command); return `${lines.join("\r\n")}\r\n`; } function renderStartupLaunchCommand(scriptPath) { return `start "" /min cmd.exe /d /c ${quoteCmdScriptArg(scriptPath)}`; } function buildStartupLauncherScript(params) { const lines = ["@echo off"]; const trimmedDescription = params.description?.trim(); if (trimmedDescription) { assertNoCmdLineBreak(trimmedDescription, "Startup launcher description"); lines.push(`rem ${trimmedDescription}`); } lines.push(renderStartupLaunchCommand(params.scriptPath)); return `${lines.join("\r\n")}\r\n`; } function quoteVbsString(value) { return `"${value.replace(/"/g, "\"\"")}"`; } function quoteVbsRunCommand(scriptPath) { return quoteVbsString(`"${scriptPath}"`); } function buildHiddenLauncherScript(params) { const lines = []; const trimmedDescription = params.description?.trim(); if (trimmedDescription) { assertNoCmdLineBreak(trimmedDescription, "Hidden launcher description"); lines.push(`' ${trimmedDescription}`); } lines.push(`CreateObject("WScript.Shell").Run ${quoteVbsRunCommand(params.scriptPath)}, 0, False`); return `${lines.join("\r\n")}\r\n`; } async function assertSchtasksAvailable() { const res = await execSchtasks(["/Query"]); if (res.code === 0) return; const detail = res.stderr || res.stdout; throw new Error(`schtasks unavailable: ${detail || "unknown error"}`.trim()); } async function isStartupEntryInstalled(env) { for (const startupEntryPath of resolveStartupEntryPaths(env)) try { await fs.access(startupEntryPath); return true; } catch {} return false; } async function isRegisteredScheduledTask(env) { return (await execSchtasks([ "/Query", "/TN", resolveTaskName(env) ]).catch(() => ({ code: 1, stdout: "", stderr: "" }))).code === 0; } async function launchFallbackTaskScript(env) { const scriptPath = resolveTaskScriptPath(env); const command = await readScheduledTaskCommand(env); if (command?.programArguments.length) { const [executable, ...args] = command.programArguments; spawn(executable, args, { cwd: command.workingDirectory || void 0, detached: true, env: { ...process.env, ...command.environment }, stdio: "ignore", windowsHide: true }).unref(); return; } spawn("cmd.exe", [ "/d", "/c", scriptPath ], { detached: true, stdio: "ignore", windowsHide: true }).unref(); } function resolveConfiguredGatewayPort(env) { return parseTcpPort(env.OPENCLAW_GATEWAY_PORT); } function parsePositivePort(raw) { return parseTcpPort(raw); } function parsePortFromProgramArguments(programArguments) { if (!programArguments?.length) return null; for (let i = 0; i < programArguments.length; i += 1) { const arg = programArguments[i]; if (!arg) continue; const inlineMatch = arg.match(/^--port=(\d+)$/); if (inlineMatch) return parsePositivePort(inlineMatch[1]); if (arg === "--port") return parsePositivePort(programArguments[i + 1]); } return null; } function isNodeHostArgv(programArguments) { const normalized = programArguments.map((arg) => normalizeLowercaseStringOrEmpty(arg.replaceAll("\\", "/"))); return normalized.some((arg, index) => arg === "node" && normalized[index + 1] === "run"); } function normalizeProgramArguments(programArguments) { return programArguments.map((arg) => normalizeLowercaseStringOrEmpty(arg.replaceAll("\\", "/"))); } function matchesInstalledProgramArguments(actualArguments, installedArguments) { const actual = normalizeProgramArguments(actualArguments); const installed = normalizeProgramArguments(installedArguments); return actual.length === installed.length && actual.every((arg, index) => arg === installed[index]); } function getSnapshotProcessId(entry) { const pid = entry.ProcessId; return typeof pid === "number" && Number.isFinite(pid) && pid > 0 ? pid : null; } function findNodeHostProcessPid(entries, port, installedArguments) { for (const entry of entries) { if (!normalizeLowercaseStringOrEmpty(entry.CommandLine ?? "")) continue; const argv = parseCmdScriptCommandLine(entry.CommandLine ?? ""); if (!isNodeHostArgv(argv) || parsePortFromProgramArguments(argv) !== port || !matchesInstalledProgramArguments(argv, installedArguments)) continue; const pid = getSnapshotProcessId(entry); if (pid) return pid; } return null; } async function resolveScheduledTaskNodeHostProcess(env) { const command = await readScheduledTaskCommand(env).catch(() => null); const installedArguments = command?.programArguments; if (!installedArguments?.length) return null; const port = parsePortFromProgramArguments(installedArguments) ?? parsePositivePort(command?.environment?.OPENCLAW_GATEWAY_PORT) ?? resolveConfiguredGatewayPort(env); if (!port) return null; const snapshot = readWindowsProcessSnapshot(); if (!snapshot) return null; const pid = findNodeHostProcessPid(snapshot, port, installedArguments); if (!pid) return null; return { pid, port }; } function shouldManageGatewayListenerPort(env) { return normalizeLowercaseStringOrEmpty(env.OPENCLAW_SERVICE_KIND) !== NODE_SERVICE_KIND; } async function resolveScheduledTaskPort(env) { const command = await readScheduledTaskCommand(env).catch(() => null); return parsePortFromProgramArguments(command?.programArguments) ?? parsePositivePort(command?.environment?.OPENCLAW_GATEWAY_PORT) ?? resolveConfiguredGatewayPort(env); } async function resolveScheduledTaskGatewayListenerPids(port) { const verified = findVerifiedGatewayListenerPidsOnPortSync(port); if (verified.length > 0) return verified; const diagnostics = await inspectPortUsage(port).catch(() => null); if (diagnostics?.status !== "busy") return []; const matchedGatewayPids = Array.from(new Set(diagnostics.listeners.filter((listener) => typeof listener.pid === "number" && listener.commandLine && isGatewayArgv(parseCmdScriptCommandLine(listener.commandLine), { allowGatewayBinary: true })).map((listener) => listener.pid))); if (matchedGatewayPids.length > 0) return matchedGatewayPids; return Array.from(new Set(diagnostics.listeners.map((listener) => listener.pid).filter((pid) => typeof pid === "number" && Number.isFinite(pid) && pid > 0))); } async function resolveListenerBackedScheduledTaskRuntime(env) { if (!shouldManageGatewayListenerPort(env)) { const matched = await resolveScheduledTaskNodeHostProcess(env); if (!matched) return null; return { status: "running", pid: matched.pid, detail: `Node host process detected for gateway port ${matched.port}.` }; } const port = await resolveScheduledTaskPort(env); if (!port) return null; const pids = findVerifiedGatewayListenerPidsOnPortSync(port); if (pids.length === 0) return null; return { status: "running", pid: pids[0], detail: `Verified gateway listener detected on port ${port} even though schtasks did not report a running task.` }; } async function terminateScheduledTaskNodeHost(env) { const matched = await resolveScheduledTaskNodeHostProcess(env); if (!matched) return []; await terminateGatewayProcessTree(matched.pid, 300); return [matched.pid]; } async function terminateScheduledTaskGatewayListeners(env) { if (!shouldManageGatewayListenerPort(env)) return []; const port = await resolveScheduledTaskPort(env); if (!port) return []; const pids = await resolveScheduledTaskGatewayListenerPids(port); for (const pid of pids) await terminateGatewayProcessTree(pid, 300); return pids; } function isProcessAlive(pid) { try { process.kill(pid, 0); return true; } catch { return false; } } async function waitForProcessExit(pid, timeoutMs) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { if (!isProcessAlive(pid)) return true; await sleep(100); } return !isProcessAlive(pid); } async function terminateGatewayProcessTree(pid, graceMs) { if (process.platform !== "win32") { killProcessTree(pid, { graceMs }); return; } const taskkillPath = path.join(getWindowsInstallRoots().systemRoot, "System32", "taskkill.exe"); spawnSync(taskkillPath, [ "/T", "/PID", String(pid) ], { stdio: "ignore", timeout: 5e3, windowsHide: true }); if (await waitForProcessExit(pid, graceMs)) return; spawnSync(taskkillPath, [ "/F", "/T", "/PID", String(pid) ], { stdio: "ignore", timeout: 5e3, windowsHide: true }); await waitForProcessExit(pid, 5e3); } async function waitForGatewayPortRelease(port, timeoutMs = 5e3) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { if ((await inspectPortUsage(port).catch(() => null))?.status === "free") return true; await sleep(250); } return false; } async function terminateBusyPortListeners(port) { const diagnostics = await inspectPortUsage(port).catch(() => null); if (diagnostics?.status !== "busy") return []; const pids = Array.from(new Set(diagnostics.listeners.map((listener) => listener.pid).filter((pid) => typeof pid === "number" && Number.isFinite(pid) && pid > 0))); for (const pid of pids) await terminateGatewayProcessTree(pid, 300); return pids; } function readWindowsProcessSnapshot() { if (process.platform !== "win32") return null; const processSnapshot = spawnSync("powershell", [ "-NoProfile", "-Command", "Get-CimInstance Win32_Process | Select-Object ProcessId,CommandLine | ConvertTo-Json -Compress" ], { encoding: "utf8", timeout: 1500, windowsHide: true }); if (processSnapshot.error || processSnapshot.status !== 0) return null; let parsedSnapshot; try { parsedSnapshot = JSON.parse(processSnapshot.stdout.trim() || "[]"); } catch { return null; } return (Array.isArray(parsedSnapshot) ? parsedSnapshot : [parsedSnapshot]).filter((entry) => typeof entry === "object" && entry !== null); } async function resolveFallbackRuntime(env) { if (!shouldManageGatewayListenerPort(env)) { const command = await readScheduledTaskCommand(env).catch(() => null); const installedArguments = command?.programArguments; const port = parsePortFromProgramArguments(installedArguments) ?? parsePositivePort(command?.environment?.OPENCLAW_GATEWAY_PORT) ?? resolveConfiguredGatewayPort(env); if (!port) return { status: "unknown", detail: "Startup-folder login item installed; node gateway port unknown." }; const snapshot = readWindowsProcessSnapshot(); if (!snapshot) return { status: "unknown", detail: `Startup-folder login item installed; could not inspect node host process for gateway port ${port}.` }; const pid = installedArguments?.length ? findNodeHostProcessPid(snapshot, port, installedArguments) : null; if (pid) return { status: "running", pid, detail: `Startup-folder login item installed; node host process detected for gateway port ${port}.` }; return { status: "stopped", detail: `Startup-folder login item installed; no node host process detected for gateway port ${port}.` }; } const port = await resolveScheduledTaskPort(env) ?? resolveConfiguredGatewayPort(env); if (!port) return { status: "unknown", detail: "Startup-folder login item installed; gateway port unknown." }; const diagnostics = await inspectPortUsage(port).catch(() => null); if (!diagnostics) return { status: "unknown", detail: `Startup-folder login item installed; could not inspect port ${port}.` }; const listener = diagnostics.listeners.find(hasListenerPid); return { status: diagnostics.status === "busy" ? "running" : "stopped", ...listener?.pid ? { pid: listener.pid } : {}, detail: diagnostics.status === "busy" ? `Startup-folder login item installed; listener detected on port ${port}.` : `Startup-folder login item installed; no listener detected on port ${port}.` }; } async function stopStartupEntry(env, stdout) { const runtime = await resolveFallbackRuntime(env); if (typeof runtime.pid === "number" && runtime.pid > 0) await terminateGatewayProcessTree(runtime.pid, 300); stdout.write(`${formatLine("Stopped Windows login item", resolveTaskName(env))}\n`); } async function terminateInstalledStartupRuntime(env) { if (!await isStartupEntryInstalled(env)) return; const runtime = await resolveFallbackRuntime(env); if (typeof runtime.pid === "number" && runtime.pid > 0) await terminateGatewayProcessTree(runtime.pid, 300); } async function restartStartupEntry(env, stdout) { const runtime = await resolveFallbackRuntime(env); if (typeof runtime.pid === "number" && runtime.pid > 0) await terminateGatewayProcessTree(runtime.pid, 300); await launchFallbackTaskScript(env); stdout.write(`${formatLine("Restarted Windows login item", resolveTaskName(env))}\n`); return { outcome: "completed" }; } async function writeScheduledTaskScript({ env, programArguments, workingDirectory, environment, description }) { await assertSchtasksAvailable().catch(() => void 0); const scriptPath = resolveTaskScriptPath(env); const taskLaunchPath = resolveTaskLauncherScriptPath(env, scriptPath); await fs.mkdir(path.dirname(scriptPath), { recursive: true }); const taskDescription = resolveGatewayServiceDescription({ env, environment, description }); const script = buildTaskScript({ description: taskDescription, programArguments, workingDirectory, environment }); await fs.writeFile(scriptPath, script, "utf8"); if (taskLaunchPath !== scriptPath) { const launcher = buildHiddenLauncherScript({ description: taskDescription, scriptPath }); await fs.writeFile(taskLaunchPath, launcher, "utf8"); } return { scriptPath, taskLaunchPath, taskDescription }; } async function stageScheduledTask({ stdout, ...args }) { const { scriptPath } = await writeScheduledTaskScript(args); writeFormattedLines(stdout, [{ label: "Staged task script", value: scriptPath }], { leadingBlankLine: true }); return { scriptPath }; } async function updateExistingScheduledTask(params) { if (!await isRegisteredScheduledTask(params.env)) return false; if ((await execSchtasks([ "/Change", "/TN", params.taskName, "/TR", params.quotedLaunchPath ])).code !== 0) return false; const upgradeXmlPath = await writeTaskXmlTempFile(buildScheduledTaskXml({ taskDescription: params.description ?? "OpenClaw Gateway", taskUser: resolveTaskUser(params.env), launchPath: params.taskLaunchPath })); try { await execSchtasks([ "/Create", "/F", "/TN", params.taskName, "/XML", upgradeXmlPath ]); } finally { await fs.rm(path.dirname(upgradeXmlPath), { recursive: true, force: true }).catch(() => {}); } await runScheduledTaskOrThrow({ taskName: params.taskName, env: params.env, scriptPath: params.scriptPath }); writeFormattedLines(params.stdout, [{ label: "Updated Scheduled Task", value: params.taskName }, { label: "Task script", value: params.scriptPath }], { leadingBlankLine: true }); return true; } async function shouldFallbackScheduledTaskLaunch(params) { const readLaunchObservation = async () => { const runtime = await readScheduledTaskRuntime(params.env).catch(() => null); if (runtime?.status === "running") return { state: "running", signature: [ runtime.state, runtime.lastRunTime, runtime.lastRunResult, runtime.detail ].filter(Boolean).join("|") }; const normalizedResult = normalizeTaskResultCode(runtime?.lastRunResult); if (normalizedResult && NOT_YET_RUN_RESULT_CODES.has(normalizedResult)) return { state: "not-yet-run", signature: [ runtime?.state, runtime?.lastRunTime, runtime?.lastRunResult, runtime?.detail ].filter(Boolean).join("|") }; return { state: "other", signature: [ runtime?.state, runtime?.lastRunTime, runtime?.lastRunResult, runtime?.detail ].filter(Boolean).join("|") }; }; const hasLaunchEvidence = async () => { const command = await readScheduledTaskCommand(params.env).catch(() => null); const installedArguments = command?.programArguments; const taskPort = parsePortFromProgramArguments(installedArguments) ?? parsePositivePort(command?.environment?.OPENCLAW_GATEWAY_PORT) ?? resolveConfiguredGatewayPort(params.env); const manageGatewayPort = shouldManageGatewayListenerPort(params.env); if (manageGatewayPort && taskPort) { if ((await resolveScheduledTaskGatewayListenerPids(taskPort)).length > 0) return true; } const scriptPathNeedle = normalizeLowercaseStringOrEmpty(params.scriptPath.replaceAll("/", "\\")); if (!scriptPathNeedle) return false; const entries = readWindowsProcessSnapshot(); if (!entries) return false; if (entries.some((entry) => normalizeLowercaseStringOrEmpty(entry.CommandLine ?? "").replaceAll("/", "\\").includes(scriptPathNeedle))) return true; if (!taskPort) return false; if (!manageGatewayPort) return installedArguments?.length ? findNodeHostProcessPid(entries, taskPort, installedArguments) != null : false; return entries.some((entry) => { if (!normalizeLowercaseStringOrEmpty(entry.CommandLine ?? "")) return false; const argv = parseCmdScriptCommandLine(entry.CommandLine ?? ""); return isGatewayArgv(argv, { allowGatewayBinary: true }) && parsePortFromProgramArguments(argv) === taskPort; }); }; const initial = await readLaunchObservation(); if (initial.state !== "not-yet-run") return false; const deadline = Date.now() + SCHEDULED_TASK_FALLBACK_TIMEOUT_MS; while (Date.now() < deadline) { await sleep(SCHEDULED_TASK_FALLBACK_POLL_MS); const current = await readLaunchObservation(); if (current.state !== "not-yet-run") return false; if (current.signature !== initial.signature) return false; } return !await hasLaunchEvidence(); } async function runScheduledTaskOrThrow(params) { const run = await execSchtasks([ "/Run", "/TN", params.taskName ]); if (run.code !== 0) throw new Error(`schtasks run failed: ${run.stderr || run.stdout}`.trim()); if (!await shouldFallbackScheduledTaskLaunch({ env: params.env, scriptPath: params.scriptPath })) return; await launchFallbackTaskScript(params.env); } async function activateScheduledTask(params) { const taskDescription = params.description ?? "OpenClaw Gateway"; const taskName = resolveTaskName(params.env); const quotedLaunchPath = quoteSchtasksArg(params.taskLaunchPath); if (await updateExistingScheduledTask({ ...params, taskName, quotedLaunchPath })) return; const taskUser = resolveTaskUser(params.env); const xmlPath = await writeTaskXmlTempFile(buildScheduledTaskXml({ taskDescription, taskUser, launchPath: params.taskLaunchPath })); let create; try { const xmlArgs = [ "/Create", "/F", "/TN", taskName, "/XML", xmlPath ]; const createUser = resolveSchtasksCreateUser(params.env, taskUser); create = await execSchtasks(createUser ? [ ...xmlArgs, "/RU", createUser, "/NP" ] : xmlArgs); if (create.code !== 0 && createUser) create = await execSchtasks(xmlArgs); } finally { await fs.rm(path.dirname(xmlPath), { recursive: true, force: true }).catch(() => {}); } if (create.code !== 0) { const detail = create.stderr || create.stdout; if (shouldFallbackToStartupEntry({ code: create.code, detail })) { const startupEntryPath = resolveStartupEntryPath(params.env); await fs.mkdir(path.dirname(startupEntryPath), { recursive: true }); const launcher = shouldUseHiddenWindowsTaskLauncher(params.env) ? buildHiddenLauncherScript({ description: taskDescription, scriptPath: params.scriptPath }) : buildStartupLauncherScript({ description: taskDescription, scriptPath: params.scriptPath }); await fs.writeFile(startupEntryPath, launcher, "utf8"); await launchFallbackTaskScript(params.env); writeFormattedLines(params.stdout, [{ label: "Installed Windows login item", value: startupEntryPath }, { label: "Task script", value: params.scriptPath }], { leadingBlankLine: true }); return; } throw new Error(`schtasks create failed: ${detail}`.trim()); } await runScheduledTaskOrThrow({ taskName, env: params.env, scriptPath: params.scriptPath }); writeFormattedLines(params.stdout, [{ label: "Installed Scheduled Task", value: taskName }, { label: "Task script", value: params.scriptPath }], { leadingBlankLine: true }); } async function installScheduledTask(args) { const staged = await writeScheduledTaskScript(args); await activateScheduledTask({ env: args.env, stdout: args.stdout, scriptPath: staged.scriptPath, taskLaunchPath: staged.taskLaunchPath, description: staged.taskDescription }); return { scriptPath: staged.scriptPath }; } async function uninstallScheduledTask({ env, stdout }) { await assertSchtasksAvailable(); const taskName = resolveTaskName(env); if (await isRegisteredScheduledTask(env).catch(() => false)) await execSchtasks([ "/Delete", "/F", "/TN", taskName ]); for (const startupEntryPath of resolveStartupEntryPaths(env)) try { await fs.unlink(startupEntryPath); stdout.write(`${formatLine("Removed Windows login item", startupEntryPath)}\n`); } catch {} const scriptPath = resolveTaskScriptPath(env); const launcherPath = resolveTaskLauncherScriptPath(env, scriptPath); if (launcherPath !== scriptPath) try { await fs.unlink(launcherPath); stdout.write(`${formatLine("Removed task launcher", launcherPath)}\n`); } catch {} try { await fs.unlink(scriptPath); stdout.write(`${formatLine("Removed task script", scriptPath)}\n`); } catch { stdout.write(`Task script not found at ${scriptPath}\n`); } } function isTaskNotRunning(res) { return normalizeLowercaseStringOrEmpty(res.stderr || res.stdout).includes("not running"); } async function stopScheduledTask({ stdout, env }) { const effectiveEnv = env ?? process.env; try { await assertSchtasksAvailable(); } catch (err) { if (await isStartupEntryInstalled(effectiveEnv)) { await stopStartupEntry(effectiveEnv, stdout); return; } throw err; } if (!await isRegisteredScheduledTask(effectiveEnv)) { if (await isStartupEntryInstalled(effectiveEnv)) { await stopStartupEntry(effectiveEnv, stdout); return; } } const taskName = resolveTaskName(effectiveEnv); const res = await execSchtasks([ "/End", "/TN", taskName ]); if (res.code !== 0 && !isTaskNotRunning(res)) throw new Error(`schtasks end failed: ${res.stderr || res.stdout}`.trim()); const manageGatewayPort = shouldManageGatewayListenerPort(effectiveEnv); const stopPort = manageGatewayPort ? await resolveScheduledTaskPort(effectiveEnv) : null; if (manageGatewayPort) await terminateScheduledTaskGatewayListeners(effectiveEnv); else await terminateScheduledTaskNodeHost(effectiveEnv); await terminateInstalledStartupRuntime(effectiveEnv); if (stopPort) { if (!await waitForGatewayPortRelease(stopPort)) { await terminateBusyPortListeners(stopPort); if (!await waitForGatewayPortRelease(stopPort, 2e3)) throw new Error(`gateway port ${stopPort} is still busy after stop`); } } stdout.write(`${formatLine("Stopped Scheduled Task", taskName)}\n`); } async function restartScheduledTask({ stdout, env }) { const effectiveEnv = env ?? process.env; try { await assertSchtasksAvailable(); } catch (err) { if (await isStartupEntryInstalled(effectiveEnv)) return await restartStartupEntry(effectiveEnv, stdout); throw err; } if (!await isRegisteredScheduledTask(effectiveEnv)) { if (await isStartupEntryInstalled(effectiveEnv)) return await restartStartupEntry(effectiveEnv, stdout); } const taskName = resolveTaskName(effectiveEnv); await execSchtasks([ "/End", "/TN", taskName ]); const manageGatewayPort = shouldManageGatewayListenerPort(effectiveEnv); const restartPort = manageGatewayPort ? await resolveScheduledTaskPort(effectiveEnv) : null; if (manageGatewayPort) await terminateScheduledTaskGatewayListeners(effectiveEnv); else await terminateScheduledTaskNodeHost(effectiveEnv); await terminateInstalledStartupRuntime(effectiveEnv); if (restartPort) { if (!await waitForGatewayPortRelease(restartPort)) { await terminateBusyPortListeners(restartPort); if (!await waitForGatewayPortRelease(restartPort, 2e3)) throw new Error(`gateway port ${restartPort} is still busy before restart`); } } await runScheduledTaskOrThrow({ taskName, env: effectiveEnv, scriptPath: resolveTaskScriptPath(effectiveEnv) }); stdout.write(`${formatLine("Restarted Scheduled Task", taskName)}\n`); return { outcome: "completed" }; } async function isScheduledTaskInstalled(args) { const effectiveEnv = args.env ?? process.env; if (await isRegisteredScheduledTask(effectiveEnv)) return true; return await isStartupEntryInstalled(effectiveEnv); } async function readScheduledTaskRuntime(env = process.env) { try { await assertSchtasksAvailable(); } catch (err) { if (await isStartupEntryInstalled(env)) return await resolveFallbackRuntime(env); return { status: "unknown", detail: String(err) }; } const res = await execSchtasks([ "/Query", "/TN", resolveTaskName(env), "/V", "/FO", "LIST" ]); if (res.code !== 0) { if (await isStartupEntryInstalled(env)) return await resolveFallbackRuntime(env); const detail = (res.stderr || res.stdout).trim(); const missing = normalizeLowercaseStringOrEmpty(detail).includes("cannot find the file"); return { status: missing ? "stopped" : "unknown", detail: detail || void 0, missingUnit: missing }; } const parsed = parseSchtasksQuery(res.stdout || ""); const derived = deriveScheduledTaskRuntimeStatus(parsed); if (derived.status !== "running") { const observedRuntime = await resolveListenerBackedScheduledTaskRuntime(env); if (observedRuntime) return { ...observedRuntime, state: parsed.status, lastRunTime: parsed.lastRunTime, lastRunResult: parsed.lastRunResult }; } return { status: derived.status, state: parsed.status, lastRunTime: parsed.lastRunTime, lastRunResult: parsed.lastRunResult, ...derived.detail ? { detail: derived.detail } : {} }; } //#endregion export { resolveTaskScriptPath as a, stopScheduledTask as c, readScheduledTaskRuntime as i, uninstallScheduledTask as l, isScheduledTaskInstalled as n, restartScheduledTask as o, readScheduledTaskCommand as r, stageScheduledTask as s, installScheduledTask as t };