























@@ -3,7 +3,10 @@ import fs from "node:fs/promises";
33import os from "node:os";
44import path from "node:path";
55import { resolveRestartSentinelPath } from "../../infra/restart-sentinel.js";
6-import { SUPERVISOR_HINT_ENV_VARS } from "../../infra/supervisor-markers.js";
6+import {
7+SUPERVISOR_HINT_ENV_VARS,
8+type RespawnSupervisor,
9+} from "../../infra/supervisor-markers.js";
710import {
811CONTROL_PLANE_UPDATE_SENTINEL_META_ENV,
912type ControlPlaneUpdateSentinelMetaFile,
@@ -12,6 +15,7 @@ import { MANAGED_SERVICE_UPDATE_HANDOFF_TEMP_PREFIX } from "../../infra/update-m
1215import type { UpdateRestartSentinelMeta } from "../../infra/update-restart-sentinel-payload.js";
13161417const PARENT_EXIT_GRACE_MS = 60_000;
18+const SYSTEMD_RUN_CANDIDATE_PATHS = ["/usr/bin/systemd-run", "/bin/systemd-run"] as const;
1519const SERVICE_IDENTITY_ENV_VARS = new Set<string>([
1620"OPENCLAW_LAUNCHD_LABEL",
1721"OPENCLAW_SYSTEMD_UNIT",
@@ -319,12 +323,95 @@ async function resolveManagedServiceHandoffCwd(root: string): Promise<string> {
319323return root;
320324}
321325326+async function resolveExecutableOnPath(
327+name: string,
328+env: NodeJS.ProcessEnv,
329+fallbackPaths: readonly string[],
330+): Promise<string | null> {
331+const candidates = new Set<string>();
332+const pathValue = env.PATH?.trim();
333+if (pathValue) {
334+for (const dir of pathValue.split(path.delimiter)) {
335+if (dir.trim()) {
336+candidates.add(path.join(dir, name));
337+}
338+}
339+}
340+for (const candidate of fallbackPaths) {
341+candidates.add(candidate);
342+}
343+344+for (const candidate of candidates) {
345+try {
346+await fs.access(candidate, fs.constants.X_OK);
347+return candidate;
348+} catch {
349+// Try the next candidate.
350+}
351+}
352+return null;
353+}
354+355+function sanitizeSystemdUnitFragment(value: string | undefined): string {
356+const normalized = value?.trim().replace(/[^A-Za-z0-9_.:@-]+/gu, "-") ?? "";
357+return normalized.replace(/^-+|-+$/gu, "").slice(0, 80);
358+}
359+360+function buildSystemdHandoffUnitName(handoffId: string | undefined): string {
361+const suffix =
362+sanitizeSystemdUnitFragment(handoffId) ||
363+sanitizeSystemdUnitFragment(`${process.pid}-${Date.now()}`) ||
364+"handoff";
365+return `openclaw-update-${suffix}.scope`;
366+}
367+368+async function resolveHandoffSpawn(params: {
369+supervisor?: RespawnSupervisor | null;
370+env: NodeJS.ProcessEnv;
371+execPath: string;
372+scriptPath: string;
373+paramsPath: string;
374+handoffId?: string;
375+}): Promise<{ command: string; args: string[] }> {
376+if (params.supervisor !== "systemd") {
377+return {
378+command: params.execPath,
379+args: [params.scriptPath, params.paramsPath],
380+};
381+}
382+383+const systemdRunPath = await resolveExecutableOnPath(
384+"systemd-run",
385+params.env,
386+SYSTEMD_RUN_CANDIDATE_PATHS,
387+);
388+if (!systemdRunPath) {
389+throw new Error(
390+"systemd-run is required to start the managed update handoff outside openclaw-gateway.service",
391+);
392+}
393+394+return {
395+command: systemdRunPath,
396+args: [
397+"--user",
398+"--scope",
399+"--collect",
400+`--unit=${buildSystemdHandoffUnitName(params.handoffId)}`,
401+params.execPath,
402+params.scriptPath,
403+params.paramsPath,
404+],
405+};
406+}
407+322408export async function startManagedServiceUpdateHandoff(params: {
323409root: string;
324410timeoutMs?: number;
325411restartDelayMs?: number;
326412meta: UpdateRestartSentinelMeta;
327413handoffId?: string;
414+supervisor?: RespawnSupervisor | null;
328415env?: NodeJS.ProcessEnv;
329416execPath?: string;
330417argv1?: string;
@@ -368,7 +455,15 @@ export async function startManagedServiceUpdateHandoff(params: {
368455[CONTROL_PLANE_UPDATE_SENTINEL_META_ENV]: metaPath,
369456OPENCLAW_UPDATE_RUN_HANDOFF: "1",
370457};
371-const child = spawn(params.execPath ?? process.execPath, [scriptPath, paramsPath], {
458+const spawnTarget = await resolveHandoffSpawn({
459+supervisor: params.supervisor,
460+ env,
461+execPath: params.execPath ?? process.execPath,
462+ scriptPath,
463+ paramsPath,
464+handoffId: params.handoffId,
465+});
466+const child = spawn(spawnTarget.command, spawnTarget.args, {
372467cwd: handoffCwd,
373468 env,
374469detached: true,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。