From 70900feaa724a849129472af741a674d1721b60b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 19 Feb 2026 10:03:09 +0000 Subject: [PATCH] refactor(daemon): share service arg types across backends --- src/daemon/launchd.ts | 69 +++++++++++-------------------------- src/daemon/schtasks.ts | 69 ++++++++++++++----------------------- src/daemon/service-types.ts | 38 ++++++++++++++++++++ src/daemon/systemd-unit.ts | 8 ++--- src/daemon/systemd.ts | 69 +++++++++++++------------------------ 5 files changed, 109 insertions(+), 144 deletions(-) create mode 100644 src/daemon/service-types.ts diff --git a/src/daemon/launchd.ts b/src/daemon/launchd.ts index ebe2397189..dded364858 100644 --- a/src/daemon/launchd.ts +++ b/src/daemon/launchd.ts @@ -15,6 +15,14 @@ import { formatLine, toPosixPath, writeFormattedLines } from "./output.js"; import { resolveGatewayStateDir, resolveHomeDir } from "./paths.js"; import { parseKeyValueOutput } from "./runtime-parse.js"; import type { GatewayServiceRuntime } from "./service-runtime.js"; +import type { + GatewayServiceCommandConfig, + GatewayServiceControlArgs, + GatewayServiceEnv, + GatewayServiceEnvArgs, + GatewayServiceInstallArgs, + GatewayServiceManageArgs, +} from "./service-types.js"; function resolveLaunchAgentLabel(args?: { env?: Record }): string { const envLabel = args?.env?.OPENCLAW_LAUNCHD_LABEL?.trim(); @@ -32,12 +40,12 @@ function resolveLaunchAgentPlistPathForLabel( return path.posix.join(home, "Library", "LaunchAgents", `${label}.plist`); } -export function resolveLaunchAgentPlistPath(env: Record): string { +export function resolveLaunchAgentPlistPath(env: GatewayServiceEnv): string { const label = resolveLaunchAgentLabel({ env }); return resolveLaunchAgentPlistPathForLabel(env, label); } -export function resolveGatewayLogPaths(env: Record): { +export function resolveGatewayLogPaths(env: GatewayServiceEnv): { logDir: string; stdoutPath: string; stderrPath: string; @@ -53,13 +61,8 @@ export function resolveGatewayLogPaths(env: Record): } export async function readLaunchAgentProgramArguments( - env: Record, -): Promise<{ - programArguments: string[]; - workingDirectory?: string; - environment?: Record; - sourcePath?: string; -} | null> { + env: GatewayServiceEnv, +): Promise { const plistPath = resolveLaunchAgentPlistPath(env); return readLaunchAgentProgramArgumentsFromFile(plistPath); } @@ -143,18 +146,14 @@ export function parseLaunchctlPrint(output: string): LaunchctlPrintInfo { return info; } -export async function isLaunchAgentLoaded(args: { - env?: Record; -}): Promise { +export async function isLaunchAgentLoaded(args: GatewayServiceEnvArgs): Promise { const domain = resolveGuiDomain(); const label = resolveLaunchAgentLabel({ env: args.env }); const res = await execLaunchctl(["print", `${domain}/${label}`]); return res.code === 0; } -export async function isLaunchAgentListed(args: { - env?: Record; -}): Promise { +export async function isLaunchAgentListed(args: GatewayServiceEnvArgs): Promise { const label = resolveLaunchAgentLabel({ env: args.env }); const res = await execLaunchctl(["list"]); if (res.code !== 0) { @@ -163,9 +162,7 @@ export async function isLaunchAgentListed(args: { return res.stdout.split(/\r?\n/).some((line) => line.trim().split(/\s+/).at(-1) === label); } -export async function launchAgentPlistExists( - env: Record, -): Promise { +export async function launchAgentPlistExists(env: GatewayServiceEnv): Promise { try { const plistPath = resolveLaunchAgentPlistPath(env); await fs.access(plistPath); @@ -227,9 +224,7 @@ export type LegacyLaunchAgent = { exists: boolean; }; -export async function findLegacyLaunchAgents( - env: Record, -): Promise { +export async function findLegacyLaunchAgents(env: GatewayServiceEnv): Promise { const domain = resolveGuiDomain(); const results: LegacyLaunchAgent[] = []; for (const label of resolveLegacyGatewayLaunchAgentLabels(env.OPENCLAW_PROFILE)) { @@ -253,10 +248,7 @@ export async function findLegacyLaunchAgents( export async function uninstallLegacyLaunchAgents({ env, stdout, -}: { - env: Record; - stdout: NodeJS.WritableStream; -}): Promise { +}: GatewayServiceManageArgs): Promise { const domain = resolveGuiDomain(); const agents = await findLegacyLaunchAgents(env); if (agents.length === 0) { @@ -296,10 +288,7 @@ export async function uninstallLegacyLaunchAgents({ export async function uninstallLaunchAgent({ env, stdout, -}: { - env: Record; - stdout: NodeJS.WritableStream; -}): Promise { +}: GatewayServiceManageArgs): Promise { const domain = resolveGuiDomain(); const label = resolveLaunchAgentLabel({ env }); const plistPath = resolveLaunchAgentPlistPath(env); @@ -342,13 +331,7 @@ function isUnsupportedGuiDomain(detail: string): boolean { ); } -export async function stopLaunchAgent({ - stdout, - env, -}: { - stdout: NodeJS.WritableStream; - env?: Record; -}): Promise { +export async function stopLaunchAgent({ stdout, env }: GatewayServiceControlArgs): Promise { const domain = resolveGuiDomain(); const label = resolveLaunchAgentLabel({ env }); const res = await execLaunchctl(["bootout", `${domain}/${label}`]); @@ -365,14 +348,7 @@ export async function installLaunchAgent({ workingDirectory, environment, description, -}: { - env: Record; - stdout: NodeJS.WritableStream; - programArguments: string[]; - workingDirectory?: string; - environment?: Record; - description?: string; -}): Promise<{ plistPath: string }> { +}: GatewayServiceInstallArgs): Promise<{ plistPath: string }> { const { logDir, stdoutPath, stderrPath } = resolveGatewayLogPaths(env); await fs.mkdir(logDir, { recursive: true }); @@ -441,10 +417,7 @@ export async function installLaunchAgent({ export async function restartLaunchAgent({ stdout, env, -}: { - stdout: NodeJS.WritableStream; - env?: Record; -}): Promise { +}: GatewayServiceControlArgs): Promise { const domain = resolveGuiDomain(); const label = resolveLaunchAgentLabel({ env }); const res = await execLaunchctl(["kickstart", "-k", `${domain}/${label}`]); diff --git a/src/daemon/schtasks.ts b/src/daemon/schtasks.ts index b1c325276e..1b58d9704e 100644 --- a/src/daemon/schtasks.ts +++ b/src/daemon/schtasks.ts @@ -7,8 +7,17 @@ import { resolveGatewayStateDir } from "./paths.js"; import { parseKeyValueOutput } from "./runtime-parse.js"; import { execSchtasks } from "./schtasks-exec.js"; import type { GatewayServiceRuntime } from "./service-runtime.js"; +import type { + GatewayServiceCommandConfig, + GatewayServiceControlArgs, + GatewayServiceEnv, + GatewayServiceEnvArgs, + GatewayServiceInstallArgs, + GatewayServiceManageArgs, + GatewayServiceRenderArgs, +} from "./service-types.js"; -function resolveTaskName(env: Record): string { +function resolveTaskName(env: GatewayServiceEnv): string { const override = env.OPENCLAW_WINDOWS_TASK_NAME?.trim(); if (override) { return override; @@ -16,7 +25,7 @@ function resolveTaskName(env: Record): string { return resolveGatewayWindowsTaskName(env.OPENCLAW_PROFILE); } -export function resolveTaskScriptPath(env: Record): string { +export function resolveTaskScriptPath(env: GatewayServiceEnv): string { const override = env.OPENCLAW_TASK_SCRIPT?.trim(); if (override) { return override; @@ -33,7 +42,7 @@ function quoteCmdArg(value: string): string { return `"${value.replace(/"/g, '\\"')}"`; } -function resolveTaskUser(env: Record): string | null { +function resolveTaskUser(env: GatewayServiceEnv): string | null { const username = env.USERNAME || env.USER || env.LOGNAME; if (!username) { return null; @@ -54,11 +63,9 @@ function parseCommandLine(value: string): string[] { return splitArgsPreservingQuotes(value, { escapeMode: "backslash-quote-only" }); } -export async function readScheduledTaskCommand(env: Record): Promise<{ - programArguments: string[]; - workingDirectory?: string; - environment?: Record; -} | null> { +export async function readScheduledTaskCommand( + env: GatewayServiceEnv, +): Promise { const scriptPath = resolveTaskScriptPath(env); try { const content = await fs.readFile(scriptPath, "utf8"); @@ -137,12 +144,7 @@ function buildTaskScript({ programArguments, workingDirectory, environment, -}: { - description?: string; - programArguments: string[]; - workingDirectory?: string; - environment?: Record; -}): string { +}: GatewayServiceRenderArgs): string { const lines: string[] = ["@echo off"]; if (description?.trim()) { lines.push(`rem ${description.trim()}`); @@ -179,14 +181,7 @@ export async function installScheduledTask({ workingDirectory, environment, description, -}: { - env: Record; - stdout: NodeJS.WritableStream; - programArguments: string[]; - workingDirectory?: string; - environment?: Record; - description?: string; -}): Promise<{ scriptPath: string }> { +}: GatewayServiceInstallArgs): Promise<{ scriptPath: string }> { await assertSchtasksAvailable(); const scriptPath = resolveTaskScriptPath(env); await fs.mkdir(path.dirname(scriptPath), { recursive: true }); @@ -244,10 +239,7 @@ export async function installScheduledTask({ export async function uninstallScheduledTask({ env, stdout, -}: { - env: Record; - stdout: NodeJS.WritableStream; -}): Promise { +}: GatewayServiceManageArgs): Promise { await assertSchtasksAvailable(); const taskName = resolveTaskName(env); await execSchtasks(["/Delete", "/F", "/TN", taskName]); @@ -266,15 +258,9 @@ function isTaskNotRunning(res: { stdout: string; stderr: string; code: number }) return detail.includes("not running"); } -export async function stopScheduledTask({ - stdout, - env, -}: { - stdout: NodeJS.WritableStream; - env?: Record; -}): Promise { +export async function stopScheduledTask({ stdout, env }: GatewayServiceControlArgs): Promise { await assertSchtasksAvailable(); - const taskName = resolveTaskName(env ?? (process.env as Record)); + const taskName = resolveTaskName(env ?? (process.env as GatewayServiceEnv)); const res = await execSchtasks(["/End", "/TN", taskName]); if (res.code !== 0 && !isTaskNotRunning(res)) { throw new Error(`schtasks end failed: ${res.stderr || res.stdout}`.trim()); @@ -285,12 +271,9 @@ export async function stopScheduledTask({ export async function restartScheduledTask({ stdout, env, -}: { - stdout: NodeJS.WritableStream; - env?: Record; -}): Promise { +}: GatewayServiceControlArgs): Promise { await assertSchtasksAvailable(); - const taskName = resolveTaskName(env ?? (process.env as Record)); + const taskName = resolveTaskName(env ?? (process.env as GatewayServiceEnv)); await execSchtasks(["/End", "/TN", taskName]); const res = await execSchtasks(["/Run", "/TN", taskName]); if (res.code !== 0) { @@ -299,17 +282,15 @@ export async function restartScheduledTask({ stdout.write(`${formatLine("Restarted Scheduled Task", taskName)}\n`); } -export async function isScheduledTaskInstalled(args: { - env?: Record; -}): Promise { +export async function isScheduledTaskInstalled(args: GatewayServiceEnvArgs): Promise { await assertSchtasksAvailable(); - const taskName = resolveTaskName(args.env ?? (process.env as Record)); + const taskName = resolveTaskName(args.env ?? (process.env as GatewayServiceEnv)); const res = await execSchtasks(["/Query", "/TN", taskName]); return res.code === 0; } export async function readScheduledTaskRuntime( - env: Record = process.env as Record, + env: GatewayServiceEnv = process.env as GatewayServiceEnv, ): Promise { try { await assertSchtasksAvailable(); diff --git a/src/daemon/service-types.ts b/src/daemon/service-types.ts new file mode 100644 index 0000000000..38f3efaee1 --- /dev/null +++ b/src/daemon/service-types.ts @@ -0,0 +1,38 @@ +export type GatewayServiceEnv = Record; + +export type GatewayServiceInstallArgs = { + env: GatewayServiceEnv; + stdout: NodeJS.WritableStream; + programArguments: string[]; + workingDirectory?: string; + environment?: GatewayServiceEnv; + description?: string; +}; + +export type GatewayServiceManageArgs = { + env: GatewayServiceEnv; + stdout: NodeJS.WritableStream; +}; + +export type GatewayServiceControlArgs = { + stdout: NodeJS.WritableStream; + env?: GatewayServiceEnv; +}; + +export type GatewayServiceEnvArgs = { + env?: GatewayServiceEnv; +}; + +export type GatewayServiceCommandConfig = { + programArguments: string[]; + workingDirectory?: string; + environment?: Record; + sourcePath?: string; +}; + +export type GatewayServiceRenderArgs = { + description?: string; + programArguments: string[]; + workingDirectory?: string; + environment?: GatewayServiceEnv; +}; diff --git a/src/daemon/systemd-unit.ts b/src/daemon/systemd-unit.ts index f947d4e2be..e76883c779 100644 --- a/src/daemon/systemd-unit.ts +++ b/src/daemon/systemd-unit.ts @@ -1,4 +1,5 @@ import { splitArgsPreservingQuotes } from "./arg-split.js"; +import type { GatewayServiceRenderArgs } from "./service-types.js"; function systemdEscapeArg(value: string): string { if (!/[\\s"\\\\]/.test(value)) { @@ -27,12 +28,7 @@ export function buildSystemdUnit({ programArguments, workingDirectory, environment, -}: { - description?: string; - programArguments: string[]; - workingDirectory?: string; - environment?: Record; -}): string { +}: GatewayServiceRenderArgs): string { const execStart = programArguments.map(systemdEscapeArg).join(" "); const descriptionLine = `Description=${description?.trim() || "OpenClaw Gateway"}`; const workingDirLine = workingDirectory diff --git a/src/daemon/systemd.ts b/src/daemon/systemd.ts index 6fad14034a..0a295436df 100644 --- a/src/daemon/systemd.ts +++ b/src/daemon/systemd.ts @@ -10,6 +10,14 @@ import { formatLine, toPosixPath, writeFormattedLines } from "./output.js"; import { resolveHomeDir } from "./paths.js"; import { parseKeyValueOutput } from "./runtime-parse.js"; import type { GatewayServiceRuntime } from "./service-runtime.js"; +import type { + GatewayServiceCommandConfig, + GatewayServiceControlArgs, + GatewayServiceEnv, + GatewayServiceEnvArgs, + GatewayServiceInstallArgs, + GatewayServiceManageArgs, +} from "./service-types.js"; import { enableSystemdUserLinger, readSystemdUserLingerStatus, @@ -21,15 +29,12 @@ import { parseSystemdExecStart, } from "./systemd-unit.js"; -function resolveSystemdUnitPathForName( - env: Record, - name: string, -): string { +function resolveSystemdUnitPathForName(env: GatewayServiceEnv, name: string): string { const home = toPosixPath(resolveHomeDir(env)); return path.posix.join(home, ".config", "systemd", "user", `${name}.service`); } -function resolveSystemdServiceName(env: Record): string { +function resolveSystemdServiceName(env: GatewayServiceEnv): string { const override = env.OPENCLAW_SYSTEMD_UNIT?.trim(); if (override) { return override.endsWith(".service") ? override.slice(0, -".service".length) : override; @@ -37,11 +42,11 @@ function resolveSystemdServiceName(env: Record): str return resolveGatewaySystemdServiceName(env.OPENCLAW_PROFILE); } -function resolveSystemdUnitPath(env: Record): string { +function resolveSystemdUnitPath(env: GatewayServiceEnv): string { return resolveSystemdUnitPathForName(env, resolveSystemdServiceName(env)); } -export function resolveSystemdUserUnitPath(env: Record): string { +export function resolveSystemdUserUnitPath(env: GatewayServiceEnv): string { return resolveSystemdUnitPath(env); } @@ -51,13 +56,8 @@ export type { SystemdUserLingerStatus }; // Unit file parsing/rendering: see systemd-unit.ts export async function readSystemdServiceExecStart( - env: Record, -): Promise<{ - programArguments: string[]; - workingDirectory?: string; - environment?: Record; - sourcePath?: string; -} | null> { + env: GatewayServiceEnv, +): Promise { const unitPath = resolveSystemdUnitPath(env); try { const content = await fs.readFile(unitPath, "utf8"); @@ -188,14 +188,7 @@ export async function installSystemdService({ workingDirectory, environment, description, -}: { - env: Record; - stdout: NodeJS.WritableStream; - programArguments: string[]; - workingDirectory?: string; - environment?: Record; - description?: string; -}): Promise<{ unitPath: string }> { +}: GatewayServiceInstallArgs): Promise<{ unitPath: string }> { await assertSystemdAvailable(); const unitPath = resolveSystemdUnitPath(env); @@ -243,10 +236,7 @@ export async function installSystemdService({ export async function uninstallSystemdService({ env, stdout, -}: { - env: Record; - stdout: NodeJS.WritableStream; -}): Promise { +}: GatewayServiceManageArgs): Promise { await assertSystemdAvailable(); const serviceName = resolveGatewaySystemdServiceName(env.OPENCLAW_PROFILE); const unitName = `${serviceName}.service`; @@ -263,7 +253,7 @@ export async function uninstallSystemdService({ async function runSystemdServiceAction(params: { stdout: NodeJS.WritableStream; - env?: Record; + env?: GatewayServiceEnv; action: "stop" | "restart"; label: string; }) { @@ -280,10 +270,7 @@ async function runSystemdServiceAction(params: { export async function stopSystemdService({ stdout, env, -}: { - stdout: NodeJS.WritableStream; - env?: Record; -}): Promise { +}: GatewayServiceControlArgs): Promise { await runSystemdServiceAction({ stdout, env, @@ -295,10 +282,7 @@ export async function stopSystemdService({ export async function restartSystemdService({ stdout, env, -}: { - stdout: NodeJS.WritableStream; - env?: Record; -}): Promise { +}: GatewayServiceControlArgs): Promise { await runSystemdServiceAction({ stdout, env, @@ -307,9 +291,7 @@ export async function restartSystemdService({ }); } -export async function isSystemdServiceEnabled(args: { - env?: Record; -}): Promise { +export async function isSystemdServiceEnabled(args: GatewayServiceEnvArgs): Promise { await assertSystemdAvailable(); const serviceName = resolveSystemdServiceName(args.env ?? {}); const unitName = `${serviceName}.service`; @@ -318,7 +300,7 @@ export async function isSystemdServiceEnabled(args: { } export async function readSystemdServiceRuntime( - env: Record = process.env as Record, + env: GatewayServiceEnv = process.env as GatewayServiceEnv, ): Promise { try { await assertSystemdAvailable(); @@ -375,9 +357,7 @@ async function isSystemctlAvailable(): Promise { return !detail.includes("not found"); } -export async function findLegacySystemdUnits( - env: Record, -): Promise { +export async function findLegacySystemdUnits(env: GatewayServiceEnv): Promise { const results: LegacySystemdUnit[] = []; const systemctlAvailable = await isSystemctlAvailable(); for (const name of LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES) { @@ -404,10 +384,7 @@ export async function findLegacySystemdUnits( export async function uninstallLegacySystemdUnits({ env, stdout, -}: { - env: Record; - stdout: NodeJS.WritableStream; -}): Promise { +}: GatewayServiceManageArgs): Promise { const units = await findLegacySystemdUnits(env); if (units.length === 0) { return units;