refactor: dedupe daemon exec wrappers

This commit is contained in:
Peter Steinberger
2026-02-15 03:44:05 +00:00
parent 4ce9b35f75
commit f33031bc9e
4 changed files with 41 additions and 83 deletions

32
src/daemon/exec-file.ts Normal file
View File

@@ -0,0 +1,32 @@
import { execFile, type ExecFileOptionsWithStringEncoding } from "node:child_process";
export type ExecResult = { stdout: string; stderr: string; code: number };
export async function execFileUtf8(
command: string,
args: string[],
options: Omit<ExecFileOptionsWithStringEncoding, "encoding"> = {},
): Promise<ExecResult> {
return await new Promise<ExecResult>((resolve) => {
execFile(command, args, { ...options, encoding: "utf8" }, (error, stdout, stderr) => {
if (!error) {
resolve({
stdout: String(stdout ?? ""),
stderr: String(stderr ?? ""),
code: 0,
});
return;
}
const e = error as { code?: unknown; message?: unknown };
const stderrText = String(stderr ?? "");
resolve({
stdout: String(stdout ?? ""),
stderr:
stderrText ||
(typeof e.message === "string" ? e.message : typeof error === "string" ? error : ""),
code: typeof e.code === "number" ? e.code : 1,
});
});
});
}

View File

@@ -1,7 +1,5 @@
import { execFile } from "node:child_process";
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { promisify } from "node:util";
import type { GatewayServiceRuntime } from "./service-runtime.js"; import type { GatewayServiceRuntime } from "./service-runtime.js";
import { import {
formatGatewayServiceDescription, formatGatewayServiceDescription,
@@ -9,6 +7,7 @@ import {
resolveGatewayLaunchAgentLabel, resolveGatewayLaunchAgentLabel,
resolveLegacyGatewayLaunchAgentLabels, resolveLegacyGatewayLaunchAgentLabels,
} from "./constants.js"; } from "./constants.js";
import { execFileUtf8 } from "./exec-file.js";
import { import {
buildLaunchAgentPlist as buildLaunchAgentPlistImpl, buildLaunchAgentPlist as buildLaunchAgentPlistImpl,
readLaunchAgentProgramArgumentsFromFile, readLaunchAgentProgramArgumentsFromFile,
@@ -17,8 +16,6 @@ import { formatLine, toPosixPath } from "./output.js";
import { resolveGatewayStateDir, resolveHomeDir } from "./paths.js"; import { resolveGatewayStateDir, resolveHomeDir } from "./paths.js";
import { parseKeyValueOutput } from "./runtime-parse.js"; import { parseKeyValueOutput } from "./runtime-parse.js";
const execFileAsync = promisify(execFile);
function resolveLaunchAgentLabel(args?: { env?: Record<string, string | undefined> }): string { function resolveLaunchAgentLabel(args?: { env?: Record<string, string | undefined> }): string {
const envLabel = args?.env?.OPENCLAW_LAUNCHD_LABEL?.trim(); const envLabel = args?.env?.OPENCLAW_LAUNCHD_LABEL?.trim();
if (envLabel) { if (envLabel) {
@@ -98,30 +95,10 @@ export function buildLaunchAgentPlist({
async function execLaunchctl( async function execLaunchctl(
args: string[], args: string[],
): Promise<{ stdout: string; stderr: string; code: number }> { ): Promise<{ stdout: string; stderr: string; code: number }> {
try {
const isWindows = process.platform === "win32"; const isWindows = process.platform === "win32";
const file = isWindows ? (process.env.ComSpec ?? "cmd.exe") : "launchctl"; const file = isWindows ? (process.env.ComSpec ?? "cmd.exe") : "launchctl";
const fileArgs = isWindows ? ["/d", "/s", "/c", "launchctl", ...args] : args; const fileArgs = isWindows ? ["/d", "/s", "/c", "launchctl", ...args] : args;
const { stdout, stderr } = await execFileAsync(file, fileArgs, { encoding: "utf8" }); return await execFileUtf8(file, fileArgs, isWindows ? { windowsHide: true } : {});
return {
stdout: String(stdout ?? ""),
stderr: String(stderr ?? ""),
code: 0,
};
} catch (error) {
const e = error as {
stdout?: unknown;
stderr?: unknown;
code?: unknown;
message?: unknown;
};
return {
stdout: typeof e.stdout === "string" ? e.stdout : "",
stderr:
typeof e.stderr === "string" ? e.stderr : typeof e.message === "string" ? e.message : "",
code: typeof e.code === "number" ? e.code : 1,
};
}
} }
function resolveGuiDomain(): string { function resolveGuiDomain(): string {

View File

@@ -1,33 +1,7 @@
import { execFile } from "node:child_process"; import { execFileUtf8 } from "./exec-file.js";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
export async function execSchtasks( export async function execSchtasks(
args: string[], args: string[],
): Promise<{ stdout: string; stderr: string; code: number }> { ): Promise<{ stdout: string; stderr: string; code: number }> {
try { return await execFileUtf8("schtasks", args, { windowsHide: true });
const { stdout, stderr } = await execFileAsync("schtasks", args, {
encoding: "utf8",
windowsHide: true,
});
return {
stdout: String(stdout ?? ""),
stderr: String(stderr ?? ""),
code: 0,
};
} catch (error) {
const e = error as {
stdout?: unknown;
stderr?: unknown;
code?: unknown;
message?: unknown;
};
return {
stdout: typeof e.stdout === "string" ? e.stdout : "",
stderr:
typeof e.stderr === "string" ? e.stderr : typeof e.message === "string" ? e.message : "",
code: typeof e.code === "number" ? e.code : 1,
};
}
} }

View File

@@ -1,13 +1,12 @@
import { execFile } from "node:child_process";
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { promisify } from "node:util";
import type { GatewayServiceRuntime } from "./service-runtime.js"; import type { GatewayServiceRuntime } from "./service-runtime.js";
import { import {
formatGatewayServiceDescription, formatGatewayServiceDescription,
LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES, LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES,
resolveGatewaySystemdServiceName, resolveGatewaySystemdServiceName,
} from "./constants.js"; } from "./constants.js";
import { execFileUtf8 } from "./exec-file.js";
import { formatLine, toPosixPath } from "./output.js"; import { formatLine, toPosixPath } from "./output.js";
import { resolveHomeDir } from "./paths.js"; import { resolveHomeDir } from "./paths.js";
import { parseKeyValueOutput } from "./runtime-parse.js"; import { parseKeyValueOutput } from "./runtime-parse.js";
@@ -22,8 +21,6 @@ import {
parseSystemdExecStart, parseSystemdExecStart,
} from "./systemd-unit.js"; } from "./systemd-unit.js";
const execFileAsync = promisify(execFile);
function resolveSystemdUnitPathForName( function resolveSystemdUnitPathForName(
env: Record<string, string | undefined>, env: Record<string, string | undefined>,
name: string, name: string,
@@ -142,29 +139,7 @@ export function parseSystemdShow(output: string): SystemdServiceInfo {
async function execSystemctl( async function execSystemctl(
args: string[], args: string[],
): Promise<{ stdout: string; stderr: string; code: number }> { ): Promise<{ stdout: string; stderr: string; code: number }> {
try { return await execFileUtf8("systemctl", args);
const { stdout, stderr } = await execFileAsync("systemctl", args, {
encoding: "utf8",
});
return {
stdout: String(stdout ?? ""),
stderr: String(stderr ?? ""),
code: 0,
};
} catch (error) {
const e = error as {
stdout?: unknown;
stderr?: unknown;
code?: unknown;
message?: unknown;
};
return {
stdout: typeof e.stdout === "string" ? e.stdout : "",
stderr:
typeof e.stderr === "string" ? e.stderr : typeof e.message === "string" ? e.message : "",
code: typeof e.code === "number" ? e.code : 1,
};
}
} }
export async function isSystemdUserServiceAvailable(): Promise<boolean> { export async function isSystemdUserServiceAvailable(): Promise<boolean> {