fix(cli): diagnose native service plans in manager context

This commit is contained in:
Federico Jaramillo Martinez
2026-07-13 00:41:51 +02:00
parent 3ac6679952
commit dde48b3b11
11 changed files with 1021 additions and 164 deletions
+238
View File
@@ -0,0 +1,238 @@
import { describe, expect, it } from "vitest";
import {
formatNativeServiceDoctorResult,
inferInstalledNativeServiceMode,
inspectInstalledDevelopmentServiceInput,
inspectInstalledProductionServiceContext,
runNativeServiceDoctor,
type InstalledNativeServiceDefinition,
type NativeServiceDoctorTarget,
} from "./serviceDoctor.js";
import {
createDevelopmentNativeServicePlan,
type NativeServiceAuthoritativeProbe,
type NativeServicePlan,
type ProductionNativeServicePlanInput,
} from "./servicePlan.js";
import { renderLaunchdPlist, renderSystemdUnit } from "./serviceRendering.js";
const shell = {
name: "zsh",
executable: "/bin/zsh",
source: "detected",
detectedExecutable: "/bin/zsh",
} as const;
function productionInput(configured = false): ProductionNativeServicePlanInput {
return {
backend: { kind: "systemd", label: "systemd user services" },
shell,
environment: { PI_WEB_CONFIG: "/home/user/config.json" },
executables: {
sessiond: {
configuredCommand: configured ? "custom sessiond --flag" : undefined,
namedCommand: "pi-web-sessiond",
bundledEntrypointPath: "/package/sessiond.js",
},
web: {
configuredCommand: configured ? "custom web --flag" : undefined,
namedCommand: "pi-web-server",
bundledEntrypointPath: "/package/server.js",
},
},
};
}
function probeWithStatus(status: "satisfied" | "unsatisfied"): NativeServiceAuthoritativeProbe {
return {
run: (request) => Promise.resolve({
kind: "completed",
outcomes: request.prerequisites.map((prerequisite) => ({
prerequisiteId: prerequisite.id,
status,
detail: status === "satisfied" ? null : `${prerequisite.id} missing in manager context`,
})),
}),
};
}
function developmentPlan(kind: "systemd" | "launchd"): NativeServicePlan {
return createDevelopmentNativeServicePlan({
backend: { kind, label: kind },
shell,
environment: { PI_WEB_CONFIG: "/home/user/config & dev.json" },
workingDirectory: "/checkout with space",
packageJsonPath: "/checkout with space/package.json",
});
}
function renderedDefinitions(plan: NativeServicePlan): InstalledNativeServiceDefinition[] {
return plan.services.map((service) => ({
id: service.id,
contents: plan.backend.kind === "systemd"
? renderSystemdUnit(plan, service)
: renderLaunchdPlist(plan, service, "/tmp/logs"),
}));
}
describe("installed native-service mode and definition inspection", () => {
it("infers production, development, absent, and ambiguous service sets", () => {
expect(inferInstalledNativeServiceMode(new Set())).toBe("none");
expect(inferInstalledNativeServiceMode(new Set(["sessiond", "web"]))).toBe("production");
expect(inferInstalledNativeServiceMode(new Set(["sessiond", "uiDev"]))).toBe("development");
expect(inferInstalledNativeServiceMode(new Set(["web", "uiDev"]))).toBe("ambiguous");
expect(inferInstalledNativeServiceMode(new Set(["sessiond"]))).toBe("ambiguous");
});
it.each(["systemd", "launchd"] as const)("reconstructs an exact installed development input from %s definitions", (kind) => {
const plan = developmentPlan(kind);
expect(inspectInstalledDevelopmentServiceInput(plan.backend, renderedDefinitions(plan))).toEqual({
ok: true,
value: {
backend: plan.backend,
shell,
environment: { PI_WEB_CONFIG: "/home/user/config & dev.json" },
workingDirectory: "/checkout with space",
packageJsonPath: "/checkout with space/package.json",
},
});
});
it("inspects legacy systemd definitions without /usr/bin/env or quoted working directories", () => {
const plan = developmentPlan("systemd");
const definitions = renderedDefinitions(plan).map((definition) => ({
...definition,
contents: definition.contents
.replace("ExecStart=/usr/bin/env ", "ExecStart=")
.replace('WorkingDirectory="/checkout with space"', "WorkingDirectory=/checkout with space"),
}));
expect(inspectInstalledDevelopmentServiceInput(plan.backend, definitions)).toMatchObject({
ok: true,
value: { workingDirectory: "/checkout with space" },
});
});
it("rejects a modified development command rather than claiming to check the installed plan", () => {
const plan = developmentPlan("systemd");
const definitions = renderedDefinitions(plan);
const firstDefinition = definitions[0];
if (firstDefinition === undefined) throw new Error("Expected a rendered service definition");
definitions[0] = {
...firstDefinition,
contents: firstDefinition.contents.replace("exec npm run start:sessiond", "exec npm run something-else"),
};
const inspection = inspectInstalledDevelopmentServiceInput(plan.backend, definitions);
expect(inspection.ok).toBe(false);
if (inspection.ok) throw new Error("Expected development inspection to fail");
expect(inspection.message).toContain("does not match the canonical development plan");
});
it("recovers production shell and environment while leaving executable strategy prospective", () => {
const plan = developmentPlan("launchd");
const firstService = plan.services[0];
if (firstService === undefined) throw new Error("Expected a development service");
const productionService = { ...firstService, workingDirectory: null };
const productionPlan: NativeServicePlan = { ...plan, mode: "production", services: [productionService] };
const productionLike = [{
id: "sessiond" as const,
contents: renderLaunchdPlist(productionPlan, productionService, "/tmp/logs"),
}];
expect(inspectInstalledProductionServiceContext(plan.backend, productionLike)).toEqual({
ok: true,
value: {
shell,
environment: { PI_WEB_CONFIG: "/home/user/config & dev.json" },
},
});
});
});
describe("native-service doctor planning and reporting", () => {
it("validates installed development requirements without production binary checks", async () => {
const plan = developmentPlan("launchd");
const inspected = inspectInstalledDevelopmentServiceInput(plan.backend, renderedDefinitions(plan));
if (!inspected.ok) throw new Error(inspected.message);
const result = await runNativeServiceDoctor(
{ kind: "installed-development", input: inspected.value },
{ probe: probeWithStatus("satisfied"), fileExists: () => false },
);
const report = formatNativeServiceDoctorResult(result);
expect(report.ok).toBe(true);
expect(report.lines).toContain("Installed development native-service plan:");
expect(report.plan?.services.flatMap((service) => service.prerequisites)).not.toEqual(
expect.arrayContaining([expect.objectContaining({ command: "pi-web-server" })]),
);
});
it("labels a production check as prospective and reports manager-context requirements", async () => {
const target: NativeServiceDoctorTarget = {
kind: "prospective-production",
input: productionInput(),
reason: "installed executable strategy is not recorded",
};
const result = await runNativeServiceDoctor(target, {
probe: probeWithStatus("unsatisfied"),
fileExists: () => true,
});
const report = formatNativeServiceDoctorResult(result);
expect(report.ok).toBe(false);
expect(report.failureKind).toBe("requirements");
expect(report.lines[0]).toContain("Prospective production native-service plan");
expect(report.lines.join("\n")).toContain("Native service requirement failed");
expect(report.failedPrerequisites).toEqual(expect.arrayContaining([
expect.objectContaining({ kind: "node-version" }),
expect.objectContaining({ kind: "readable-file" }),
]));
});
it("preserves configured overrides as unverified and does not probe arbitrary commands", async () => {
let calls = 0;
const result = await runNativeServiceDoctor(
{ kind: "prospective-production", input: productionInput(true), reason: "current configured overrides" },
{
probe: { run: () => { calls += 1; return Promise.resolve({ kind: "completed", outcomes: [] }); } },
fileExists: () => false,
},
);
const report = formatNativeServiceDoctorResult(result);
expect(calls).toBe(0);
expect(report.ok).toBe(true);
expect(report.lines.join("\n")).toContain("does not execute arbitrary configured commands");
});
it.each(["manager", "timeout", "malformed-output", "cleanup"] as const)(
"distinguishes %s infrastructure failures from PATH requirement drift",
async (reason) => {
const result = await runNativeServiceDoctor(
{ kind: "prospective-production", input: productionInput(), reason: "no installed services" },
{
probe: { run: () => Promise.resolve({ kind: "infrastructure-failure", reason, message: `${reason} failure` }) },
fileExists: () => true,
},
);
const report = formatNativeServiceDoctorResult(result);
expect(report.ok).toBe(false);
expect(report.failureKind).toBe("infrastructure");
expect(report.lines.join("\n")).toContain(`infrastructure failure (${reason})`);
expect(report.lines.join("\n")).toContain("not proof of a PATH mismatch");
},
);
it("makes mixed or malformed installed definitions a failing inspection result", async () => {
const result = await runNativeServiceDoctor(
{ kind: "inspection-failure", message: "production and development service IDs are both installed" },
{ probe: probeWithStatus("satisfied"), fileExists: () => true },
);
const report = formatNativeServiceDoctorResult(result);
expect(report).toMatchObject({ ok: false, failureKind: "inspection" });
expect(report.lines.join("\n")).toContain("could not be inspected");
});
});
+424
View File
@@ -0,0 +1,424 @@
import { basename, join } from "node:path";
import {
createDevelopmentNativeServicePlan,
resolveProductionNativeServicePlan,
validateNativeServicePlan,
type DevelopmentNativeServicePlanInput,
type NativeServiceBackend,
type NativeServiceId,
type NativeServicePlan,
type NativeServicePlanDependencies,
type NativeServicePlanFailure,
type NativeServicePlanValidationFailure,
type NativeServicePrerequisite,
type NativeServiceShell,
type ProductionNativeServicePlanInput,
} from "./servicePlan.js";
export type InstalledNativeServiceMode = "none" | "production" | "development" | "ambiguous";
export interface InstalledNativeServiceDefinition {
id: NativeServiceId;
contents: string;
}
export interface InstalledNativeServiceContext {
shell: NativeServiceShell;
environment: Readonly<Record<string, string>>;
}
export type InstalledNativeServiceInspection<T> =
| { ok: true; value: T }
| { ok: false; message: string };
export type NativeServiceDoctorTarget =
| {
kind: "installed-development";
input: DevelopmentNativeServicePlanInput;
}
| {
kind: "prospective-production";
input: ProductionNativeServicePlanInput;
reason: string;
}
| {
kind: "inspection-failure";
message: string;
};
interface NativeServiceDoctorScope {
kind: "installed-development" | "prospective-production";
reason: string | null;
}
export type NativeServiceDoctorResult =
| {
kind: "inspection-failure";
message: string;
}
| {
kind: "plan-resolution-failure";
scope: NativeServiceDoctorScope;
failures: readonly NativeServicePlanFailure[];
}
| {
kind: "plan-validation";
scope: NativeServiceDoctorScope;
plan: NativeServicePlan;
validation: { ok: true } | { ok: false; failures: readonly NativeServicePlanValidationFailure[] };
};
export interface NativeServiceDoctorReport {
ok: boolean;
failureKind: "none" | "requirements" | "infrastructure" | "inspection";
lines: readonly string[];
plan: NativeServicePlan | null;
failedPrerequisites: readonly NativeServicePrerequisite[];
}
interface ParsedServiceDefinition {
id: NativeServiceId;
shell: NativeServiceShell;
environment: Readonly<Record<string, string>>;
workingDirectory: string | null;
shellCommand: string;
}
export function inferInstalledNativeServiceMode(serviceIds: ReadonlySet<NativeServiceId>): InstalledNativeServiceMode {
if (serviceIds.size === 0) return "none";
const hasProductionWeb = serviceIds.has("web");
const hasDevelopmentUi = serviceIds.has("uiDev");
if (hasProductionWeb && !hasDevelopmentUi) return "production";
if (hasDevelopmentUi && !hasProductionWeb) return "development";
return "ambiguous";
}
export function inspectInstalledProductionServiceContext(
backend: NativeServiceBackend,
definitions: readonly InstalledNativeServiceDefinition[],
): InstalledNativeServiceInspection<InstalledNativeServiceContext> {
const parsed = parseConsistentDefinitions(backend, definitions);
if (!parsed.ok) return parsed;
const withWorkingDirectory = parsed.value.find((definition) => definition.workingDirectory !== null);
if (withWorkingDirectory !== undefined) {
return {
ok: false,
message: `Installed production service ${withWorkingDirectory.id} unexpectedly has working directory ${withWorkingDirectory.workingDirectory ?? ""}.`,
};
}
return {
ok: true,
value: {
shell: parsed.value[0]?.shell ?? impossibleMissingDefinition(),
environment: parsed.value[0]?.environment ?? impossibleMissingDefinition(),
},
};
}
export function inspectInstalledDevelopmentServiceInput(
backend: NativeServiceBackend,
definitions: readonly InstalledNativeServiceDefinition[],
): InstalledNativeServiceInspection<DevelopmentNativeServicePlanInput> {
const parsed = parseConsistentDefinitions(backend, definitions);
if (!parsed.ok) return parsed;
const first = parsed.value[0] ?? impossibleMissingDefinition();
if (first.workingDirectory === null) {
return { ok: false, message: "Installed development services do not declare a working directory." };
}
const input: DevelopmentNativeServicePlanInput = {
backend,
shell: first.shell,
environment: first.environment,
workingDirectory: first.workingDirectory,
packageJsonPath: join(first.workingDirectory, "package.json"),
};
const expectedPlan = createDevelopmentNativeServicePlan(input);
for (const definition of parsed.value) {
const expected = expectedPlan.services.find((service) => service.id === definition.id);
if (expected?.shellCommand !== definition.shellCommand) {
return {
ok: false,
message: `Installed ${definition.id} service command does not match the canonical development plan.`,
};
}
}
return { ok: true, value: input };
}
export async function runNativeServiceDoctor(
target: NativeServiceDoctorTarget,
dependencies: NativeServicePlanDependencies,
): Promise<NativeServiceDoctorResult> {
if (target.kind === "inspection-failure") return target;
const scope: NativeServiceDoctorScope = target.kind === "installed-development"
? { kind: target.kind, reason: null }
: { kind: target.kind, reason: target.reason };
let plan: NativeServicePlan;
if (target.kind === "installed-development") {
plan = createDevelopmentNativeServicePlan(target.input);
} else {
const resolution = await resolveProductionNativeServicePlan(target.input, dependencies);
if (!resolution.ok) {
return { kind: "plan-resolution-failure", scope, failures: resolution.failures };
}
plan = resolution.plan;
}
const validation = await validateNativeServicePlan(plan, dependencies.probe);
return { kind: "plan-validation", scope, plan, validation };
}
export function formatNativeServiceDoctorResult(result: NativeServiceDoctorResult): NativeServiceDoctorReport {
if (result.kind === "inspection-failure") {
return {
ok: false,
failureKind: "inspection",
lines: [
`✗ Installed native-service plan could not be inspected: ${result.message}`,
" Run `pi-web install` or `pi-web install --dev` to replace mixed, partial, or outdated service definitions.",
],
plan: null,
failedPrerequisites: [],
};
}
const lines = [scopeHeading(result.scope)];
if (result.kind === "plan-resolution-failure") {
let infrastructure = false;
for (const failure of result.failures) {
if (failure.kind === "probe-infrastructure") {
infrastructure = true;
lines.push(`✗ Native service probe infrastructure failure (${failure.reason}): ${failure.message}`);
} else if (failure.kind === "entrypoint-inspection-failure") {
infrastructure = true;
lines.push(`✗ Could not inspect bundled ${failure.serviceId} entrypoint ${failure.entrypointPath}: ${failure.message}`);
} else {
lines.push(`${failure.namedCommand} is unavailable to the native service manager, and bundled entrypoint ${failure.bundledEntrypointPath} is missing.`);
if (failure.namedCommandFailure !== null) lines.push(` ${failure.namedCommandFailure}`);
}
}
if (infrastructure) lines.push(" This infrastructure failure is not proof of a PATH mismatch.");
return {
ok: false,
failureKind: infrastructure ? "infrastructure" : "requirements",
lines,
plan: null,
failedPrerequisites: [],
};
}
const configuredOverrides = result.plan.services.filter((service) => service.strategy.kind === "configured-override");
for (const service of configuredOverrides) {
lines.push(`! ${service.description} uses a configured command override; doctor does not execute arbitrary configured commands.`);
}
if (result.validation.ok) {
lines.push("✓ All verifiable native-service plan requirements are satisfied in the service-manager context.");
return { ok: true, failureKind: "none", lines, plan: result.plan, failedPrerequisites: [] };
}
const failedPrerequisites: NativeServicePrerequisite[] = [];
let infrastructure = false;
for (const failure of result.validation.failures) {
if (failure.kind === "probe-infrastructure") {
infrastructure = true;
lines.push(`✗ Native service probe infrastructure failure (${failure.reason}): ${failure.message}`);
} else {
failedPrerequisites.push(failure.prerequisite);
lines.push(`✗ Native service requirement failed: ${failure.prerequisite.description}`);
if (failure.detail !== null && failure.detail !== failure.prerequisite.description) lines.push(` ${failure.detail}`);
}
}
if (infrastructure) lines.push(" This infrastructure failure is not proof of a PATH mismatch.");
return {
ok: false,
failureKind: infrastructure ? "infrastructure" : "requirements",
lines,
plan: result.plan,
failedPrerequisites,
};
}
function scopeHeading(scope: NativeServiceDoctorScope): string {
if (scope.kind === "installed-development") return "Installed development native-service plan:";
return `Prospective production native-service plan (${scope.reason ?? "installed strategy is unknown"}):`;
}
function parseConsistentDefinitions(
backend: NativeServiceBackend,
definitions: readonly InstalledNativeServiceDefinition[],
): InstalledNativeServiceInspection<readonly ParsedServiceDefinition[]> {
if (definitions.length === 0) return { ok: false, message: "No installed service definitions were provided." };
const parsed: ParsedServiceDefinition[] = [];
for (const definition of definitions) {
const result = backend.kind === "systemd"
? parseSystemdDefinition(definition)
: parseLaunchdDefinition(definition);
if (!result.ok) return result;
parsed.push(result.value);
}
const first = parsed[0] ?? impossibleMissingDefinition();
for (const definition of parsed.slice(1)) {
if (definition.shell.executable !== first.shell.executable) {
return { ok: false, message: "Installed service definitions use different login shells." };
}
if (!recordsEqual(definition.environment, first.environment)) {
return { ok: false, message: "Installed service definitions use different environments." };
}
if (definition.workingDirectory !== first.workingDirectory) {
return { ok: false, message: "Installed service definitions use different working directories." };
}
}
return { ok: true, value: parsed };
}
function parseSystemdDefinition(
definition: InstalledNativeServiceDefinition,
): InstalledNativeServiceInspection<ParsedServiceDefinition> {
const execStart = /^ExecStart=(?:\/usr\/bin\/env )?(.+?) -lc (.+)$/mu.exec(definition.contents);
if (execStart?.[1] === undefined || execStart[2] === undefined) {
return { ok: false, message: `Installed ${definition.id} systemd unit has an unrecognized ExecStart.` };
}
const shell = installedShell(execStart[1]);
if (!shell.ok) return shell;
const shellCommand = parseShellQuotedValue(shell.value.name, execStart[2]);
if (shellCommand === undefined) {
return { ok: false, message: `Installed ${definition.id} systemd unit has an unrecognized shell command.` };
}
const environment: Record<string, string> = {};
for (const match of definition.contents.matchAll(/^Environment="((?:\\.|[^"])*)"$/gmu)) {
const assignment = systemdUnescape(match[1] ?? "");
const separator = assignment.indexOf("=");
if (separator <= 0) return { ok: false, message: `Installed ${definition.id} systemd unit has a malformed environment entry.` };
environment[assignment.slice(0, separator)] = assignment.slice(separator + 1);
}
const workingDirectoryMatch = /^WorkingDirectory=(.+)$/mu.exec(definition.contents);
const workingDirectory = workingDirectoryMatch?.[1] === undefined
? null
: parseSystemdValue(workingDirectoryMatch[1]);
if (workingDirectoryMatch !== null && workingDirectory === undefined) {
return { ok: false, message: `Installed ${definition.id} systemd unit has a malformed working directory.` };
}
return {
ok: true,
value: { id: definition.id, shell: shell.value, environment, workingDirectory: workingDirectory ?? null, shellCommand },
};
}
function parseLaunchdDefinition(
definition: InstalledNativeServiceDefinition,
): InstalledNativeServiceInspection<ParsedServiceDefinition> {
const argumentsBlock = /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/u.exec(definition.contents)?.[1];
if (argumentsBlock === undefined) {
return { ok: false, message: `Installed ${definition.id} LaunchAgent has no ProgramArguments array.` };
}
const arguments_ = [...argumentsBlock.matchAll(/<string>([\s\S]*?)<\/string>/gu)].map((match) => xmlUnescape(match[1] ?? ""));
if (arguments_.length !== 4 || arguments_[0] !== "/usr/bin/env" || arguments_[2] !== "-lc") {
return { ok: false, message: `Installed ${definition.id} LaunchAgent has unrecognized ProgramArguments.` };
}
const shell = installedShell(arguments_[1] ?? "");
if (!shell.ok) return shell;
const environment: Record<string, string> = {};
const environmentBlock = /<key>EnvironmentVariables<\/key>\s*<dict>([\s\S]*?)<\/dict>/u.exec(definition.contents)?.[1];
if (environmentBlock !== undefined) {
for (const match of environmentBlock.matchAll(/<key>([\s\S]*?)<\/key>\s*<string>([\s\S]*?)<\/string>/gu)) {
environment[xmlUnescape(match[1] ?? "")] = xmlUnescape(match[2] ?? "");
}
}
const workingDirectory = launchdString(definition.contents, "WorkingDirectory");
return {
ok: true,
value: {
id: definition.id,
shell: shell.value,
environment,
workingDirectory,
shellCommand: arguments_[3] ?? "",
},
};
}
function installedShell(executable: string): InstalledNativeServiceInspection<NativeServiceShell> {
const name = basename(executable).replace(/^-/, "");
if (name !== "bash" && name !== "zsh" && name !== "fish") {
return { ok: false, message: `Installed service definition uses unsupported login shell ${executable}.` };
}
return {
ok: true,
value: { name, executable, source: "detected", detectedExecutable: executable },
};
}
function parseSystemdValue(value: string): string | undefined {
if (!value.startsWith('"') && !value.endsWith('"')) return value;
if (!value.startsWith('"') || !value.endsWith('"')) return undefined;
return systemdUnescape(value.slice(1, -1));
}
function systemdUnescape(value: string): string {
let result = "";
for (let index = 0; index < value.length; index += 1) {
const character = value[index];
if (character === "\\" && index + 1 < value.length) {
result += value[index + 1] ?? "";
index += 1;
} else {
result += character ?? "";
}
}
return result;
}
function parseShellQuotedValue(shell: NativeServiceShell["name"], value: string): string | undefined {
if (!value.startsWith("'") || !value.endsWith("'")) return undefined;
const inner = value.slice(1, -1);
if (shell === "fish") return fishSingleQuoteUnescape(inner);
return inner.replaceAll("'\\''", "'").replaceAll("$$", "$").replaceAll("%%", "%");
}
function fishSingleQuoteUnescape(value: string): string {
let result = "";
for (let index = 0; index < value.length; index += 1) {
const character = value[index];
if (character === "\\" && index + 1 < value.length) {
result += value[index + 1] ?? "";
index += 1;
} else {
result += character ?? "";
}
}
return result.replaceAll("$$", "$").replaceAll("%%", "%");
}
function launchdString(contents: string, key: string): string | null {
const escapedKey = key.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&");
const value = new RegExp(`<key>${escapedKey}<\\/key>\\s*<string>([\\s\\S]*?)<\\/string>`, "u").exec(contents)?.[1];
return value === undefined ? null : xmlUnescape(value);
}
function xmlUnescape(value: string): string {
return value
.replaceAll("&apos;", "'")
.replaceAll("&quot;", '"')
.replaceAll("&gt;", ">")
.replaceAll("&lt;", "<")
.replaceAll("&amp;", "&");
}
function recordsEqual(left: Readonly<Record<string, string>>, right: Readonly<Record<string, string>>): boolean {
const leftEntries = Object.entries(left);
return leftEntries.length === Object.keys(right).length
&& leftEntries.every(([key, value]) => right[key] === value);
}
function impossibleMissingDefinition(): never {
throw new Error("Expected at least one installed native service definition");
}
+93
View File
@@ -102,6 +102,25 @@ describe("systemd authoritative native-service probe", () => {
});
});
it("bounds a hung unit, cleans it up, and reports the timeout", async () => {
const runner = queuedRunner([
{ kind: "timeout", stdout: "", stderr: "" },
completed(0),
completed(0),
]);
const probe = new SystemdNativeServiceProbe({
commandRunner: runner,
createUniqueId: () => "fixed",
commandTimeoutMs: 100,
});
await expect(probe.run(request())).resolves.toMatchObject({
kind: "infrastructure-failure",
reason: "timeout",
});
expect(runner.calls.map(({ command }) => command)).toEqual(["systemd-run", "systemctl", "systemctl"]);
});
it("bounds a hung unit and distinguishes cleanup failure", async () => {
const runner = queuedRunner([
{ kind: "timeout", stdout: "", stderr: "" },
@@ -276,6 +295,80 @@ describe("launchd authoritative native-service probe", () => {
expect(runner.calls).toHaveLength(1);
expect(fileSystem.removeDirectory).toHaveBeenCalledWith("/tmp/probe");
});
it("cleans a loaded label after malformed launchctl output", async () => {
const runner = queuedRunner([
completed(0),
completed(0, "pid = 123\n"),
completed(0),
]);
const fileSystem = launchdFileSystem({});
const probe = new LaunchdNativeServiceProbe({
commandRunner: runner,
fileSystem,
uid: 505,
createUniqueId: () => "fixed",
now: () => 0,
sleep: () => Promise.resolve(),
probeTimeoutMs: 20,
pollIntervalMs: 10,
commandTimeoutMs: 100,
});
await expect(probe.run(request("launchd"))).resolves.toMatchObject({
kind: "infrastructure-failure",
reason: "malformed-output",
});
expect(runner.calls.at(-1)?.args[0]).toBe("bootout");
expect(fileSystem.removeDirectory).toHaveBeenCalledWith("/tmp/probe");
});
it("cleans a loaded label when probe output cannot be read", async () => {
const runner = queuedRunner([
completed(0),
completed(0, "state = not running\nlast exit code = 0\n"),
completed(0),
]);
const fileSystem = launchdFileSystem({});
const probe = new LaunchdNativeServiceProbe({
commandRunner: runner,
fileSystem,
uid: 506,
createUniqueId: () => "fixed",
now: () => 0,
sleep: () => Promise.resolve(),
probeTimeoutMs: 20,
pollIntervalMs: 10,
commandTimeoutMs: 100,
});
const result = await probe.run(request("launchd"));
expect(result).toMatchObject({ kind: "infrastructure-failure", reason: "manager" });
expect(result.kind === "infrastructure-failure" && result.message).toContain("Could not read launchd probe output");
expect(runner.calls.at(-1)?.args[0]).toBe("bootout");
expect(fileSystem.removeDirectory).toHaveBeenCalledWith("/tmp/probe");
});
it("reports temporary-file cleanup failures", async () => {
const runner = queuedRunner([completed(1, "", "bootstrap denied")]);
const fileSystem = launchdFileSystem({});
fileSystem.removeDirectory.mockRejectedValueOnce(new Error("rm denied"));
const probe = new LaunchdNativeServiceProbe({
commandRunner: runner,
fileSystem,
uid: 506,
createUniqueId: () => "fixed",
now: () => 0,
sleep: () => Promise.resolve(),
probeTimeoutMs: 20,
pollIntervalMs: 10,
commandTimeoutMs: 100,
});
const result = await probe.run(request("launchd"));
expect(result).toMatchObject({ kind: "infrastructure-failure", reason: "cleanup" });
expect(result.kind === "infrastructure-failure" && result.message).toContain("rm denied");
});
});
describe("probe service definitions", () => {
+2 -2
View File
@@ -365,7 +365,7 @@ function prerequisiteProbeCommand(
outputPrefix: string,
): string {
return prerequisites.map((prerequisite) => {
const check = prerequisiteCheck(shell, prerequisite);
const check = nativeServicePrerequisiteShellCheck(shell, prerequisite);
const encodedId = Buffer.from(prerequisite.id, "utf8").toString("base64");
const satisfied = markerCommand(shell, outputPrefix, encodedId, "satisfied");
const unsatisfied = markerCommand(shell, outputPrefix, encodedId, "unsatisfied");
@@ -373,7 +373,7 @@ function prerequisiteProbeCommand(
}).join("; ") || ":";
}
function prerequisiteCheck(shell: NativeServiceShellName, prerequisite: NativeServicePrerequisite): string {
export function nativeServicePrerequisiteShellCheck(shell: NativeServiceShellName, prerequisite: NativeServicePrerequisite): string {
switch (prerequisite.kind) {
case "command-available":
return `command -v ${shellQuote(shell, prerequisite.command)}`;