Download src/daemon/launchd-stop.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 8.54 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/daemon/launchd-stop.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/daemon/launchd-stop.ts
-
curl -L -o launchd-stop.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/daemon/launchd-stop.ts
8.54 kB
| /** LaunchAgent stop semantics and in-service maintenance parking. */ | |
| import { formatPortDiagnostics } from "../infra/ports-format.js"; | |
| import { inspectPortUsage } from "../infra/ports-inspect.js"; | |
| import { probePortUsage } from "../infra/ports-probe.js"; | |
| import { cleanStaleGatewayProcessesSync } from "../infra/restart-stale-pids.js"; | |
| import { sleep } from "../utils.js"; | |
| import { isCurrentProcessInsideLaunchdService } from "./launchd-current-service.js"; | |
| import { | |
| execLaunchctl, | |
| formatLaunchctlResultDetail, | |
| isLaunchctlNotLoaded, | |
| } from "./launchd-exec.js"; | |
| import { resolveLaunchAgentLabel } from "./launchd-label.js"; | |
| import { LAUNCH_AGENT_EXIT_TIMEOUT_SECONDS } from "./launchd-plist.js"; | |
| import { scheduleDetachedLaunchdMaintenancePark } from "./launchd-restart-handoff.js"; | |
| import { | |
| resolveLaunchAgentGatewayContext, | |
| resolveLaunchAgentGuiDomain, | |
| waitForLaunchAgentStopped, | |
| } from "./launchd-runtime.js"; | |
| import { formatLine } from "./output.js"; | |
| import { createGatewayLifecycleMutationReporter } from "./service-mutation.js"; | |
| import type { GatewayServiceControlArgs, GatewayServiceEnv } from "./service-types.js"; | |
| import { assertGatewayServiceUpdateCurrent } from "./service-update-authority.js"; | |
| const LAUNCH_AGENT_STOP_PORT_RELEASE_TIMEOUT_MS = LAUNCH_AGENT_EXIT_TIMEOUT_SECONDS * 1_000; | |
| const LAUNCH_AGENT_STOP_PORT_RELEASE_POLL_MS = 100; | |
| async function bootoutLaunchAgentOrThrow(params: { | |
| serviceTarget: string; | |
| warning: string; | |
| stdout: NodeJS.WritableStream; | |
| onMutation?: () => void; | |
| assertCurrent?: () => void; | |
| }): Promise<void> { | |
| params.assertCurrent?.(); | |
| const bootout = await execLaunchctl(["bootout", params.serviceTarget]); | |
| if (bootout.code !== 0 && !isLaunchctlNotLoaded(bootout)) { | |
| throw new Error( | |
| `${params.warning}; launchctl bootout failed: ${formatLaunchctlResultDetail(bootout)}`, | |
| ); | |
| } | |
| params.onMutation?.(); | |
| params.stdout.write(`${formatLine("Warning", params.warning)}\n`); | |
| } | |
| async function waitForGatewayPortRelease( | |
| port: number, | |
| probeHosts: readonly string[], | |
| ): Promise<boolean> { | |
| const deadline = Date.now() + LAUNCH_AGENT_STOP_PORT_RELEASE_TIMEOUT_MS; | |
| while (Date.now() < deadline) { | |
| await sleep(Math.min(LAUNCH_AGENT_STOP_PORT_RELEASE_POLL_MS, deadline - Date.now())); | |
| const status = await probePortUsage(port, probeHosts); | |
| if (status === "free") { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| async function assertGatewayPortReleasedAfterStop( | |
| env: GatewayServiceEnv, | |
| assertCurrent?: () => void, | |
| ): Promise<void> { | |
| const { env: cleanupEnv, port, probeHosts } = await resolveLaunchAgentGatewayContext(env); | |
| if (port === null) { | |
| return; | |
| } | |
| assertCurrent?.(); | |
| assertGatewayServiceUpdateCurrent(); | |
| cleanStaleGatewayProcessesSync(port, { | |
| env: cleanupEnv, | |
| assertCurrent: assertGatewayServiceUpdateCurrent, | |
| }); | |
| const diagnostics = await inspectPortUsage(port, { | |
| probeHosts, | |
| }).catch(() => null); | |
| if (diagnostics?.status !== "busy") { | |
| return; | |
| } | |
| if (await waitForGatewayPortRelease(port, probeHosts)) { | |
| return; | |
| } | |
| throw new Error( | |
| [ | |
| `gateway port ${port} is still busy after LaunchAgent stop`, | |
| ...formatPortDiagnostics(diagnostics), | |
| ].join("\n"), | |
| ); | |
| } | |
| export async function stopLaunchAgent({ | |
| stdout, | |
| env, | |
| disable: persistDisable, | |
| onMutation, | |
| assertCurrent, | |
| }: GatewayServiceControlArgs): Promise<void> { | |
| const serviceEnv = env ?? (process.env as GatewayServiceEnv); | |
| const domain = resolveLaunchAgentGuiDomain(); | |
| const label = resolveLaunchAgentLabel(serviceEnv); | |
| const serviceTarget = `${domain}/${label}`; | |
| const reportMutation = createGatewayLifecycleMutationReporter(onMutation); | |
| if (await isCurrentProcessInsideLaunchdService(label, process.env)) { | |
| throw new Error( | |
| `Refusing to stop LaunchAgent ${label} from inside the same launchd service; run this command from an external shell.`, | |
| ); | |
| } | |
| assertCurrent?.(); | |
| if (!persistDisable) { | |
| // Default: bootout only. Removes the job from the current launchd domain without | |
| // persisting a disable, so KeepAlive auto-recovery survives future crashes and | |
| // `openclaw gateway start` re-enables cleanly without a manual `launchctl enable`. | |
| const bootout = await execLaunchctl(["bootout", serviceTarget]); | |
| if (bootout.code !== 0 && !isLaunchctlNotLoaded(bootout)) { | |
| throw new Error(`launchctl bootout failed: ${formatLaunchctlResultDetail(bootout)}`); | |
| } | |
| reportMutation("bootout"); | |
| await assertGatewayPortReleasedAfterStop(serviceEnv, assertCurrent); | |
| stdout.write(`${formatLine("Stopped LaunchAgent", serviceTarget)}\n`); | |
| return; | |
| } | |
| // --disable: persistently suppress KeepAlive/RunAtLoad before stopping. | |
| // Without this, launchd can relaunch the process as soon as `stop` exits. | |
| const disableResult = await execLaunchctl(["disable", serviceTarget]); | |
| if (disableResult.code !== 0) { | |
| await bootoutLaunchAgentOrThrow({ | |
| serviceTarget, | |
| assertCurrent, | |
| stdout, | |
| warning: `launchctl disable failed; used bootout fallback and left service unloaded: ${formatLaunchctlResultDetail(disableResult)}`, | |
| onMutation: () => reportMutation("disable-bootout"), | |
| }); | |
| await assertGatewayPortReleasedAfterStop(serviceEnv, assertCurrent); | |
| stdout.write(`${formatLine("Stopped LaunchAgent (degraded)", serviceTarget)}\n`); | |
| return; | |
| } | |
| reportMutation("disable"); | |
| // `launchctl stop` targets the plain label (not the fully-qualified service target). | |
| assertCurrent?.(); | |
| const stop = await execLaunchctl(["stop", label]); | |
| if (stop.code !== 0 && !isLaunchctlNotLoaded(stop)) { | |
| await bootoutLaunchAgentOrThrow({ | |
| serviceTarget, | |
| assertCurrent, | |
| stdout, | |
| warning: `launchctl stop failed; used bootout fallback and left service unloaded: ${formatLaunchctlResultDetail(stop)}`, | |
| onMutation: () => reportMutation("disable-bootout"), | |
| }); | |
| await assertGatewayPortReleasedAfterStop(serviceEnv, assertCurrent); | |
| stdout.write(`${formatLine("Stopped LaunchAgent (degraded)", serviceTarget)}\n`); | |
| return; | |
| } | |
| reportMutation("disable-stop"); | |
| const stopState = await waitForLaunchAgentStopped(serviceTarget); | |
| if (stopState.state !== "stopped" && stopState.state !== "not-loaded") { | |
| const warning = | |
| stopState.state === "unknown" | |
| ? `launchctl print could not confirm stop; used bootout fallback and left service unloaded: ${stopState.detail ?? "unknown error"}` | |
| : "launchctl stop did not fully stop the service; used bootout fallback and left service unloaded"; | |
| await bootoutLaunchAgentOrThrow({ | |
| serviceTarget, | |
| assertCurrent, | |
| stdout, | |
| warning, | |
| onMutation: () => reportMutation("disable-bootout"), | |
| }); | |
| await assertGatewayPortReleasedAfterStop(serviceEnv, assertCurrent); | |
| stdout.write(`${formatLine("Stopped LaunchAgent (degraded)", serviceTarget)}\n`); | |
| return; | |
| } | |
| await assertGatewayPortReleasedAfterStop(serviceEnv, assertCurrent); | |
| stdout.write(`${formatLine("Stopped LaunchAgent", serviceTarget)}\n`); | |
| } | |
| export async function parkCurrentLaunchAgentForMaintenance( | |
| params: { | |
| env?: GatewayServiceEnv; | |
| } = {}, | |
| ): Promise<boolean> { | |
| const serviceEnv = params.env ?? (process.env as GatewayServiceEnv); | |
| const domain = resolveLaunchAgentGuiDomain(); | |
| const label = resolveLaunchAgentLabel(serviceEnv); | |
| if (!(await isCurrentProcessInsideLaunchdService(label, process.env))) { | |
| return false; | |
| } | |
| const serviceTarget = `${domain}/${label}`; | |
| // Disable before exit so KeepAlive cannot spawn a replacement before the | |
| // detached handoff can boot the current job out of launchd. | |
| const disable = await execLaunchctl(["disable", serviceTarget]); | |
| if (disable.code !== 0) { | |
| throw new Error( | |
| `launchctl disable failed while parking ${serviceTarget}: ${formatLaunchctlResultDetail(disable)}`, | |
| ); | |
| } | |
| const handoff = scheduleDetachedLaunchdMaintenancePark({ | |
| env: serviceEnv, | |
| waitForPid: process.pid, | |
| }); | |
| const handoffError = !handoff.ok | |
| ? handoff.error | |
| : (await handoff.value) | |
| ? undefined | |
| : "helper failed to spawn"; | |
| if (handoffError) { | |
| const rollback = await execLaunchctl(["enable", serviceTarget]); | |
| const rollbackDetail = | |
| rollback.code === 0 | |
| ? "restored launchd enable state" | |
| : `launchctl enable rollback failed: ${formatLaunchctlResultDetail(rollback)}`; | |
| throw new Error(`launchd maintenance park handoff failed: ${handoffError}; ${rollbackDetail}`); | |
| } | |
| return true; | |
| } | |