Skip to content

Commit 975d40d

Browse files
committed
docs: document launchd daemon helpers
1 parent 0d35da9 commit 975d40d

6 files changed

Lines changed: 28 additions & 1 deletion

File tree

src/daemon/gateway-entrypoint.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/** Resolves gateway dist entrypoints used by installed daemon command lines. */
12
import path from "node:path";
23
import { pathExists } from "../utils.js";
34

@@ -36,6 +37,8 @@ export function buildGatewayDistEntrypointCandidates(...inputs: string[]): strin
3637
distDirs.push(distDir);
3738
}
3839

40+
// Prefer canonical basenames across every observed dist dir before falling
41+
// through, so repaired services converge on the same entrypoint order.
3942
const candidates: string[] = [];
4043
for (const basename of GATEWAY_DIST_ENTRYPOINT_BASENAMES) {
4144
for (const distDir of distDirs) {

src/daemon/inspect.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/** Inspects installed platform services for extra OpenClaw or legacy gateway jobs. */
12
import fs from "node:fs/promises";
23
import path from "node:path";
34
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
@@ -75,7 +76,8 @@ function hasGatewaySubcommandArg(args: string[]): boolean {
7576
}
7677

7778
export function detectMarkerLineWithGateway(contents: string): Marker | null {
78-
// Join line continuations (trailing backslash) into single lines
79+
// Join line continuations before scanning systemd ExecStart commands; marker
80+
// detection must ignore relationship-only unit keys.
7981
const lower = normalizeLowercaseStringOrEmpty(contents.replace(/\\\r?\n\s*/g, " "));
8082
for (const line of lower.split(/\r?\n/)) {
8183
const trimmed = line.trim();
@@ -163,6 +165,8 @@ function detectLaunchdGatewayExecutionMarker(contents: string): Marker | null {
163165
if (!hasGatewaySubcommandArg(programArguments)) {
164166
return null;
165167
}
168+
// Only execution command fields identify gateway jobs; labels alone catch too
169+
// many unrelated helper jobs.
166170
const launchCommand = normalizeLowercaseStringOrEmpty(
167171
[...program, ...programArguments].filter(Boolean).join("\n"),
168172
);
@@ -296,6 +300,8 @@ async function scanLaunchdDir(params: {
296300
if (!marker) {
297301
continue;
298302
}
303+
// Managed current services are expected; this scan reports extra jobs that
304+
// can compete for ports or survive old installs.
299305
if (isIgnoredLaunchdLabel(label)) {
300306
continue;
301307
}

src/daemon/launchd-current-service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/** Detects whether the current process is running inside a launchd service label. */
12
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
23

34
export type CurrentProcessLaunchdServiceLabelOptions = {
@@ -31,6 +32,8 @@ export function isCurrentProcessLaunchdServiceLabel(
3132
normalizeOptionalString(env.OPENCLAW_SERVICE_MARKER) === "openclaw" &&
3233
Boolean(normalizeOptionalString(env.OPENCLAW_SERVICE_KIND))
3334
) {
35+
// Managed wrappers inject service metadata; trust it when launchd's own
36+
// label variables are absent or renamed by the host environment.
3437
return true;
3538
}
3639
return options.allowConfiguredLabelFallback !== false && currentLabels.length === 0;

src/daemon/launchd-plist.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/** Reads and renders macOS LaunchAgent plists for gateway service installs. */
12
import fs from "node:fs/promises";
23
import type { GatewayServiceEnvironmentValueSource } from "./service-types.js";
34

@@ -119,6 +120,8 @@ async function readLaunchAgentEnvironmentFile(
119120
].filter((candidate): candidate is string => Boolean(candidate)),
120121
),
121122
);
123+
// Corrupted wrapper args can still point near the generated env dir. Try the
124+
// sibling canonical env file before giving up so repair rewrites retain env.
122125
for (const candidate of candidateEnvFilePaths) {
123126
try {
124127
content = await fs.readFile(candidate, "utf8");
@@ -237,6 +240,8 @@ export async function readLaunchAgentProgramArgumentsFromFile(
237240
const effectiveProgramArguments = unwrapGeneratedEnvWrapperArgs(args, options);
238241
const environment = { ...inlineEnvironment, ...fileEnvironment };
239242
const environmentValueSources: Record<string, GatewayServiceEnvironmentValueSource> = {};
243+
// Track source provenance so repair flows can tell inline plist env from the
244+
// generated env file and preserve both when they overlap.
240245
for (const key of Object.keys(inlineEnvironment)) {
241246
environmentValueSources[key] = Object.hasOwn(fileEnvironment, key)
242247
? "inline-and-file"

src/daemon/launchd-restart-handoff.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/** Detached macOS launchd restart handoff for restarting from inside the service. */
12
import { spawn } from "node:child_process";
23
import os from "node:os";
34
import path from "node:path";
@@ -97,6 +98,8 @@ function buildLaunchdRestartScript(
9798
mode: LaunchdRestartHandoffMode,
9899
restartLogEnv: LaunchdRestartLogEnv,
99100
): string {
101+
// The detached shell waits for the caller before touching launchd so the
102+
// current gateway process can exit cleanly after scheduling the handoff.
100103
const waitForCallerPid = `wait_pid="$4"
101104
label="$5"
102105
${renderPosixRestartLogSetup(restartLogEnv)}

src/daemon/launchd.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/** macOS LaunchAgent installer, runtime inspection, and lifecycle controls. */
12
import fs from "node:fs/promises";
23
import path from "node:path";
34
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
@@ -61,6 +62,8 @@ function normalizeOpenClawUpdateLaunchdLabel(label: unknown): string | null {
6162
if (trimmed.startsWith(OPENCLAW_UPDATE_LAUNCHD_LABEL_PREFIX)) {
6263
return trimmed;
6364
}
65+
// Manual update jobs include a timestamp-like suffix and should be cleaned up
66+
// without matching arbitrary ai.openclaw labels.
6467
return OPENCLAW_MANUAL_UPDATE_LAUNCHD_LABEL_PATTERN.test(trimmed) ? trimmed : null;
6568
}
6669

@@ -200,6 +203,8 @@ async function prepareLaunchAgentProgramArguments(params: {
200203
return { programArguments: params.programArguments };
201204
}
202205

206+
// Environment values with secrets live in an owner-only env file instead of
207+
// inline plist XML, which can be harder to rotate and audit.
203208
const envDir = resolveLaunchAgentEnvDir(params.env);
204209
const envFilePath = resolveLaunchAgentEnvFilePath(params.env, params.label);
205210
const wrapperPath = resolveLaunchAgentEnvWrapperPath(params.env, params.label);
@@ -327,6 +332,8 @@ export async function findStaleOpenClawUpdateLaunchdJobs(
327332
if (result.code !== 0) {
328333
return [];
329334
}
335+
// Never report the active gateway label as stale even when a wrapper exposes
336+
// update-like launchd metadata through the current environment.
330337
return parseLaunchctlListOpenClawUpdateJobs(result.stdout).filter(
331338
(job) => !isCurrentGatewayLaunchdLabel(job.label, env),
332339
);

0 commit comments

Comments
 (0)