refactor(daemon): share runtime and service probe helpers

This commit is contained in:
Peter Steinberger
2026-02-22 21:18:30 +00:00
parent e029f78447
commit 06b0a60bef
12 changed files with 241 additions and 107 deletions

View File

@@ -0,0 +1,87 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { findExtraGatewayServices } from "./inspect.js";
const { execSchtasksMock } = vi.hoisted(() => ({
execSchtasksMock: vi.fn(),
}));
vi.mock("./schtasks-exec.js", () => ({
execSchtasks: (...args: unknown[]) => execSchtasksMock(...args),
}));
describe("findExtraGatewayServices (win32)", () => {
const originalPlatform = process.platform;
beforeEach(() => {
Object.defineProperty(process, "platform", {
configurable: true,
value: "win32",
});
execSchtasksMock.mockReset();
});
afterEach(() => {
Object.defineProperty(process, "platform", {
configurable: true,
value: originalPlatform,
});
});
it("skips schtasks queries unless deep mode is enabled", async () => {
const result = await findExtraGatewayServices({});
expect(result).toEqual([]);
expect(execSchtasksMock).not.toHaveBeenCalled();
});
it("returns empty results when schtasks query fails", async () => {
execSchtasksMock.mockResolvedValueOnce({
code: 1,
stdout: "",
stderr: "error",
});
const result = await findExtraGatewayServices({}, { deep: true });
expect(result).toEqual([]);
});
it("collects only non-openclaw marker tasks from schtasks output", async () => {
execSchtasksMock.mockResolvedValueOnce({
code: 0,
stdout: [
"TaskName: OpenClaw Gateway",
"Task To Run: C:\\Program Files\\OpenClaw\\openclaw.exe gateway run",
"",
"TaskName: Clawdbot Legacy",
"Task To Run: C:\\clawdbot\\clawdbot.exe run",
"",
"TaskName: Other Task",
"Task To Run: C:\\tools\\helper.exe",
"",
"TaskName: MoltBot Legacy",
"Task To Run: C:\\moltbot\\moltbot.exe run",
"",
].join("\n"),
stderr: "",
});
const result = await findExtraGatewayServices({}, { deep: true });
expect(result).toEqual([
{
platform: "win32",
label: "Clawdbot Legacy",
detail: "task: Clawdbot Legacy, run: C:\\clawdbot\\clawdbot.exe run",
scope: "system",
marker: "clawdbot",
legacy: true,
},
{
platform: "win32",
label: "MoltBot Legacy",
detail: "task: MoltBot Legacy, run: C:\\moltbot\\moltbot.exe run",
scope: "system",
marker: "moltbot",
legacy: true,
},
]);
});
});

View File

@@ -152,19 +152,26 @@ async function readUtf8File(filePath: string): Promise<string | null> {
}
}
async function scanLaunchdDir(params: {
dir: string;
scope: "user" | "system";
}): Promise<ExtraGatewayService[]> {
const results: ExtraGatewayService[] = [];
const entries = await readDirEntries(params.dir);
type ServiceFileEntry = {
entry: string;
name: string;
fullPath: string;
contents: string;
};
async function collectServiceFiles(params: {
dir: string;
extension: string;
isIgnoredName: (name: string) => boolean;
}): Promise<ServiceFileEntry[]> {
const out: ServiceFileEntry[] = [];
const entries = await readDirEntries(params.dir);
for (const entry of entries) {
if (!entry.endsWith(".plist")) {
if (!entry.endsWith(params.extension)) {
continue;
}
const labelFromName = entry.replace(/\.plist$/, "");
if (isIgnoredLaunchdLabel(labelFromName)) {
const name = entry.slice(0, -params.extension.length);
if (params.isIgnoredName(name)) {
continue;
}
const fullPath = path.join(params.dir, entry);
@@ -172,6 +179,23 @@ async function scanLaunchdDir(params: {
if (contents === null) {
continue;
}
out.push({ entry, name, fullPath, contents });
}
return out;
}
async function scanLaunchdDir(params: {
dir: string;
scope: "user" | "system";
}): Promise<ExtraGatewayService[]> {
const results: ExtraGatewayService[] = [];
const candidates = await collectServiceFiles({
dir: params.dir,
extension: ".plist",
isIgnoredName: isIgnoredLaunchdLabel,
});
for (const { name: labelFromName, fullPath, contents } of candidates) {
const marker = detectMarker(contents);
const label = tryExtractPlistLabel(contents) ?? labelFromName;
if (!marker) {
@@ -213,21 +237,13 @@ async function scanSystemdDir(params: {
scope: "user" | "system";
}): Promise<ExtraGatewayService[]> {
const results: ExtraGatewayService[] = [];
const entries = await readDirEntries(params.dir);
const candidates = await collectServiceFiles({
dir: params.dir,
extension: ".service",
isIgnoredName: isIgnoredSystemdName,
});
for (const entry of entries) {
if (!entry.endsWith(".service")) {
continue;
}
const name = entry.replace(/\.service$/, "");
if (isIgnoredSystemdName(name)) {
continue;
}
const fullPath = path.join(params.dir, entry);
const contents = await readUtf8File(fullPath);
if (contents === null) {
continue;
}
for (const { entry, name, fullPath, contents } of candidates) {
const marker = detectMarker(contents);
if (!marker) {
continue;

View File

@@ -1,5 +1,6 @@
import fs from "node:fs/promises";
import path from "node:path";
import { isBunRuntime, isNodeRuntime } from "./runtime-binary.js";
type GatewayProgramArgs = {
programArguments: string[];
@@ -8,16 +9,6 @@ type GatewayProgramArgs = {
type GatewayRuntimePreference = "auto" | "node" | "bun";
function isNodeRuntime(execPath: string): boolean {
const base = path.basename(execPath).toLowerCase();
return base === "node" || base === "node.exe";
}
function isBunRuntime(execPath: string): boolean {
const base = path.basename(execPath).toLowerCase();
return base === "bun" || base === "bun.exe";
}
async function resolveCliEntrypointPathForService(): Promise<string> {
const argv1 = process.argv[1];
if (!argv1) {

View File

@@ -0,0 +1,11 @@
import path from "node:path";
export function isNodeRuntime(execPath: string): boolean {
const base = path.basename(execPath).toLowerCase();
return base === "node" || base === "node.exe";
}
export function isBunRuntime(execPath: string): boolean {
const base = path.basename(execPath).toLowerCase();
return base === "bun" || base === "bun.exe";
}

View File

@@ -1,6 +1,7 @@
import fs from "node:fs/promises";
import path from "node:path";
import { resolveLaunchAgentPlistPath } from "./launchd.js";
import { isBunRuntime, isNodeRuntime } from "./runtime-binary.js";
import {
isSystemNodePath,
isVersionManagedNodePath,
@@ -224,16 +225,6 @@ function auditGatewayToken(
});
}
function isNodeRuntime(execPath: string): boolean {
const base = path.basename(execPath).toLowerCase();
return base === "node" || base === "node.exe";
}
function isBunRuntime(execPath: string): boolean {
const base = path.basename(execPath).toLowerCase();
return base === "bun" || base === "bun.exe";
}
function getPathModule(platform: NodeJS.Platform) {
return platform === "win32" ? path.win32 : path.posix;
}