refactor(daemon): share service arg types across backends

This commit is contained in:
Peter Steinberger
2026-02-19 10:03:09 +00:00
parent be7462af1e
commit 70900feaa7
5 changed files with 109 additions and 144 deletions

View File

@@ -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, string | undefined> }): 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, string | undefined>): string {
export function resolveLaunchAgentPlistPath(env: GatewayServiceEnv): string {
const label = resolveLaunchAgentLabel({ env });
return resolveLaunchAgentPlistPathForLabel(env, label);
}
export function resolveGatewayLogPaths(env: Record<string, string | undefined>): {
export function resolveGatewayLogPaths(env: GatewayServiceEnv): {
logDir: string;
stdoutPath: string;
stderrPath: string;
@@ -53,13 +61,8 @@ export function resolveGatewayLogPaths(env: Record<string, string | undefined>):
}
export async function readLaunchAgentProgramArguments(
env: Record<string, string | undefined>,
): Promise<{
programArguments: string[];
workingDirectory?: string;
environment?: Record<string, string>;
sourcePath?: string;
} | null> {
env: GatewayServiceEnv,
): Promise<GatewayServiceCommandConfig | null> {
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<string, string | undefined>;
}): Promise<boolean> {
export async function isLaunchAgentLoaded(args: GatewayServiceEnvArgs): Promise<boolean> {
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<string, string | undefined>;
}): Promise<boolean> {
export async function isLaunchAgentListed(args: GatewayServiceEnvArgs): Promise<boolean> {
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<string, string | undefined>,
): Promise<boolean> {
export async function launchAgentPlistExists(env: GatewayServiceEnv): Promise<boolean> {
try {
const plistPath = resolveLaunchAgentPlistPath(env);
await fs.access(plistPath);
@@ -227,9 +224,7 @@ export type LegacyLaunchAgent = {
exists: boolean;
};
export async function findLegacyLaunchAgents(
env: Record<string, string | undefined>,
): Promise<LegacyLaunchAgent[]> {
export async function findLegacyLaunchAgents(env: GatewayServiceEnv): Promise<LegacyLaunchAgent[]> {
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<string, string | undefined>;
stdout: NodeJS.WritableStream;
}): Promise<LegacyLaunchAgent[]> {
}: GatewayServiceManageArgs): Promise<LegacyLaunchAgent[]> {
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<string, string | undefined>;
stdout: NodeJS.WritableStream;
}): Promise<void> {
}: GatewayServiceManageArgs): Promise<void> {
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<string, string | undefined>;
}): Promise<void> {
export async function stopLaunchAgent({ stdout, env }: GatewayServiceControlArgs): Promise<void> {
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<string, string | undefined>;
stdout: NodeJS.WritableStream;
programArguments: string[];
workingDirectory?: string;
environment?: Record<string, string | undefined>;
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<string, string | undefined>;
}): Promise<void> {
}: GatewayServiceControlArgs): Promise<void> {
const domain = resolveGuiDomain();
const label = resolveLaunchAgentLabel({ env });
const res = await execLaunchctl(["kickstart", "-k", `${domain}/${label}`]);

View File

@@ -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, string | undefined>): 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, string | undefined>): string {
return resolveGatewayWindowsTaskName(env.OPENCLAW_PROFILE);
}
export function resolveTaskScriptPath(env: Record<string, string | undefined>): 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, string | undefined>): 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<string, string | undefined>): Promise<{
programArguments: string[];
workingDirectory?: string;
environment?: Record<string, string>;
} | null> {
export async function readScheduledTaskCommand(
env: GatewayServiceEnv,
): Promise<GatewayServiceCommandConfig | null> {
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, string | undefined>;
}): 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<string, string | undefined>;
stdout: NodeJS.WritableStream;
programArguments: string[];
workingDirectory?: string;
environment?: Record<string, string | undefined>;
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<string, string | undefined>;
stdout: NodeJS.WritableStream;
}): Promise<void> {
}: GatewayServiceManageArgs): Promise<void> {
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<string, string | undefined>;
}): Promise<void> {
export async function stopScheduledTask({ stdout, env }: GatewayServiceControlArgs): Promise<void> {
await assertSchtasksAvailable();
const taskName = resolveTaskName(env ?? (process.env as Record<string, string | undefined>));
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<string, string | undefined>;
}): Promise<void> {
}: GatewayServiceControlArgs): Promise<void> {
await assertSchtasksAvailable();
const taskName = resolveTaskName(env ?? (process.env as Record<string, string | undefined>));
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<string, string | undefined>;
}): Promise<boolean> {
export async function isScheduledTaskInstalled(args: GatewayServiceEnvArgs): Promise<boolean> {
await assertSchtasksAvailable();
const taskName = resolveTaskName(args.env ?? (process.env as Record<string, string | undefined>));
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<string, string | undefined> = process.env as Record<string, string | undefined>,
env: GatewayServiceEnv = process.env as GatewayServiceEnv,
): Promise<GatewayServiceRuntime> {
try {
await assertSchtasksAvailable();

View File

@@ -0,0 +1,38 @@
export type GatewayServiceEnv = Record<string, string | undefined>;
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<string, string>;
sourcePath?: string;
};
export type GatewayServiceRenderArgs = {
description?: string;
programArguments: string[];
workingDirectory?: string;
environment?: GatewayServiceEnv;
};

View File

@@ -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, string | undefined>;
}): string {
}: GatewayServiceRenderArgs): string {
const execStart = programArguments.map(systemdEscapeArg).join(" ");
const descriptionLine = `Description=${description?.trim() || "OpenClaw Gateway"}`;
const workingDirLine = workingDirectory

View File

@@ -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<string, string | undefined>,
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, string | undefined>): 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<string, string | undefined>): str
return resolveGatewaySystemdServiceName(env.OPENCLAW_PROFILE);
}
function resolveSystemdUnitPath(env: Record<string, string | undefined>): string {
function resolveSystemdUnitPath(env: GatewayServiceEnv): string {
return resolveSystemdUnitPathForName(env, resolveSystemdServiceName(env));
}
export function resolveSystemdUserUnitPath(env: Record<string, string | undefined>): 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<string, string | undefined>,
): Promise<{
programArguments: string[];
workingDirectory?: string;
environment?: Record<string, string>;
sourcePath?: string;
} | null> {
env: GatewayServiceEnv,
): Promise<GatewayServiceCommandConfig | null> {
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<string, string | undefined>;
stdout: NodeJS.WritableStream;
programArguments: string[];
workingDirectory?: string;
environment?: Record<string, string | undefined>;
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<string, string | undefined>;
stdout: NodeJS.WritableStream;
}): Promise<void> {
}: GatewayServiceManageArgs): Promise<void> {
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<string, string | undefined>;
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<string, string | undefined>;
}): Promise<void> {
}: GatewayServiceControlArgs): Promise<void> {
await runSystemdServiceAction({
stdout,
env,
@@ -295,10 +282,7 @@ export async function stopSystemdService({
export async function restartSystemdService({
stdout,
env,
}: {
stdout: NodeJS.WritableStream;
env?: Record<string, string | undefined>;
}): Promise<void> {
}: GatewayServiceControlArgs): Promise<void> {
await runSystemdServiceAction({
stdout,
env,
@@ -307,9 +291,7 @@ export async function restartSystemdService({
});
}
export async function isSystemdServiceEnabled(args: {
env?: Record<string, string | undefined>;
}): Promise<boolean> {
export async function isSystemdServiceEnabled(args: GatewayServiceEnvArgs): Promise<boolean> {
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<string, string | undefined> = process.env as Record<string, string | undefined>,
env: GatewayServiceEnv = process.env as GatewayServiceEnv,
): Promise<GatewayServiceRuntime> {
try {
await assertSystemdAvailable();
@@ -375,9 +357,7 @@ async function isSystemctlAvailable(): Promise<boolean> {
return !detail.includes("not found");
}
export async function findLegacySystemdUnits(
env: Record<string, string | undefined>,
): Promise<LegacySystemdUnit[]> {
export async function findLegacySystemdUnits(env: GatewayServiceEnv): Promise<LegacySystemdUnit[]> {
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<string, string | undefined>;
stdout: NodeJS.WritableStream;
}): Promise<LegacySystemdUnit[]> {
}: GatewayServiceManageArgs): Promise<LegacySystemdUnit[]> {
const units = await findLegacySystemdUnits(env);
if (units.length === 0) {
return units;