@mono-agent/agent-app 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +304 -32
- package/dist/acp-bridge.d.ts.map +1 -1
- package/dist/acp-bridge.js +113 -54
- package/dist/acp-bridge.js.map +1 -1
- package/dist/acp-session-store.d.ts +12 -0
- package/dist/acp-session-store.d.ts.map +1 -0
- package/dist/acp-session-store.js +74 -0
- package/dist/acp-session-store.js.map +1 -0
- package/dist/agent-artifact-paths.d.ts +8 -0
- package/dist/agent-artifact-paths.d.ts.map +1 -0
- package/dist/agent-artifact-paths.js +11 -0
- package/dist/agent-artifact-paths.js.map +1 -0
- package/dist/agent-root-coordinator.d.ts +101 -0
- package/dist/agent-root-coordinator.d.ts.map +1 -0
- package/dist/agent-root-coordinator.js +470 -0
- package/dist/agent-root-coordinator.js.map +1 -0
- package/dist/app-controller-channels.d.ts +11 -1
- package/dist/app-controller-channels.d.ts.map +1 -1
- package/dist/app-controller-channels.js +145 -20
- package/dist/app-controller-channels.js.map +1 -1
- package/dist/app-controller-continuation.d.ts +2 -0
- package/dist/app-controller-continuation.d.ts.map +1 -1
- package/dist/app-controller-continuation.js.map +1 -1
- package/dist/app-controller-lifecycle.d.ts +5 -0
- package/dist/app-controller-lifecycle.d.ts.map +1 -1
- package/dist/app-controller-lifecycle.js +24 -5
- package/dist/app-controller-lifecycle.js.map +1 -1
- package/dist/app-controller-maintenance.d.ts +2 -0
- package/dist/app-controller-maintenance.d.ts.map +1 -1
- package/dist/app-controller-maintenance.js +32 -2
- package/dist/app-controller-maintenance.js.map +1 -1
- package/dist/app-controller-process-jobs.d.ts +46 -0
- package/dist/app-controller-process-jobs.d.ts.map +1 -0
- package/dist/app-controller-process-jobs.js +281 -0
- package/dist/app-controller-process-jobs.js.map +1 -0
- package/dist/app-controller-responder.d.ts +15 -0
- package/dist/app-controller-responder.d.ts.map +1 -1
- package/dist/app-controller-responder.js +146 -12
- package/dist/app-controller-responder.js.map +1 -1
- package/dist/app-controller-traceability.d.ts +6 -0
- package/dist/app-controller-traceability.d.ts.map +1 -1
- package/dist/app-controller-traceability.js +10 -0
- package/dist/app-controller-traceability.js.map +1 -1
- package/dist/app-controller-utils.d.ts +8 -0
- package/dist/app-controller-utils.d.ts.map +1 -1
- package/dist/app-controller-utils.js +24 -1
- package/dist/app-controller-utils.js.map +1 -1
- package/dist/app-controller.d.ts +40 -1
- package/dist/app-controller.d.ts.map +1 -1
- package/dist/app-controller.js +124 -39
- package/dist/app-controller.js.map +1 -1
- package/dist/background-log-maintenance.d.ts +85 -2
- package/dist/background-log-maintenance.d.ts.map +1 -1
- package/dist/background-log-maintenance.js +407 -136
- package/dist/background-log-maintenance.js.map +1 -1
- package/dist/background-runtime.d.ts.map +1 -1
- package/dist/background-runtime.js +22 -41
- package/dist/background-runtime.js.map +1 -1
- package/dist/background.d.ts +18 -26
- package/dist/background.d.ts.map +1 -1
- package/dist/background.js +83 -138
- package/dist/background.js.map +1 -1
- package/dist/channel-drivers/cron.d.ts +12 -1
- package/dist/channel-drivers/cron.d.ts.map +1 -1
- package/dist/channel-drivers/cron.js +121 -18
- package/dist/channel-drivers/cron.js.map +1 -1
- package/dist/channel-drivers/native-notify.d.ts +3 -0
- package/dist/channel-drivers/native-notify.d.ts.map +1 -1
- package/dist/channel-drivers/native-notify.js +2 -1
- package/dist/channel-drivers/native-notify.js.map +1 -1
- package/dist/channel-drivers/slack.d.ts.map +1 -1
- package/dist/channel-drivers/slack.js +41 -4
- package/dist/channel-drivers/slack.js.map +1 -1
- package/dist/channel-drivers/telegram.d.ts.map +1 -1
- package/dist/channel-drivers/telegram.js +46 -3
- package/dist/channel-drivers/telegram.js.map +1 -1
- package/dist/channel-drivers/tui.d.ts +13 -2
- package/dist/channel-drivers/tui.d.ts.map +1 -1
- package/dist/channel-drivers/tui.js +112 -3
- package/dist/channel-drivers/tui.js.map +1 -1
- package/dist/channels.d.ts +1 -1
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +4 -2
- package/dist/channels.js.map +1 -1
- package/dist/cli-args.d.ts +15 -1
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +82 -12
- package/dist/cli-args.js.map +1 -1
- package/dist/cli-background-command.d.ts +9 -2
- package/dist/cli-background-command.d.ts.map +1 -1
- package/dist/cli-background-command.js +174 -66
- package/dist/cli-background-command.js.map +1 -1
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +50 -12
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +21 -1
- package/dist/cli-help.js.map +1 -1
- package/dist/config-reference.d.ts.map +1 -1
- package/dist/config-reference.js +99 -2
- package/dist/config-reference.js.map +1 -1
- package/dist/configured-agent.d.ts +44 -3
- package/dist/configured-agent.d.ts.map +1 -1
- package/dist/configured-agent.js +805 -156
- package/dist/configured-agent.js.map +1 -1
- package/dist/conversation-state-roots.d.ts +59 -0
- package/dist/conversation-state-roots.d.ts.map +1 -0
- package/dist/conversation-state-roots.js +248 -0
- package/dist/conversation-state-roots.js.map +1 -0
- package/dist/cron-control-store.d.ts +91 -0
- package/dist/cron-control-store.d.ts.map +1 -0
- package/dist/cron-control-store.js +1251 -0
- package/dist/cron-control-store.js.map +1 -0
- package/dist/cron-operator-service.d.ts +54 -0
- package/dist/cron-operator-service.d.ts.map +1 -0
- package/dist/cron-operator-service.js +352 -0
- package/dist/cron-operator-service.js.map +1 -0
- package/dist/doctor-session-history.d.ts +9 -0
- package/dist/doctor-session-history.d.ts.map +1 -0
- package/dist/doctor-session-history.js +313 -0
- package/dist/doctor-session-history.js.map +1 -0
- package/dist/doctor.d.ts +2 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +320 -9
- package/dist/doctor.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/interaction-bridge.d.ts +1 -0
- package/dist/interaction-bridge.d.ts.map +1 -1
- package/dist/interaction-bridge.js +54 -6
- package/dist/interaction-bridge.js.map +1 -1
- package/dist/jobs-command.d.ts +17 -0
- package/dist/jobs-command.d.ts.map +1 -0
- package/dist/jobs-command.js +207 -0
- package/dist/jobs-command.js.map +1 -0
- package/dist/launchd-lifecycle-lock.d.ts +36 -0
- package/dist/launchd-lifecycle-lock.d.ts.map +1 -0
- package/dist/launchd-lifecycle-lock.js +124 -0
- package/dist/launchd-lifecycle-lock.js.map +1 -0
- package/dist/launchd-log-monitor-status.d.ts +7 -0
- package/dist/launchd-log-monitor-status.d.ts.map +1 -0
- package/dist/launchd-log-monitor-status.js +224 -0
- package/dist/launchd-log-monitor-status.js.map +1 -0
- package/dist/launchd-logs.d.ts +9 -1
- package/dist/launchd-logs.d.ts.map +1 -1
- package/dist/launchd-logs.js +69 -16
- package/dist/launchd-logs.js.map +1 -1
- package/dist/launchd-maintenance-command.d.ts +10 -0
- package/dist/launchd-maintenance-command.d.ts.map +1 -0
- package/dist/launchd-maintenance-command.js +2 -0
- package/dist/launchd-maintenance-command.js.map +1 -0
- package/dist/launchd-maintenance-entry.d.ts +30 -0
- package/dist/launchd-maintenance-entry.d.ts.map +1 -0
- package/dist/launchd-maintenance-entry.js +206 -0
- package/dist/launchd-maintenance-entry.js.map +1 -0
- package/dist/launchd-maintenance-gate.d.ts +28 -0
- package/dist/launchd-maintenance-gate.d.ts.map +1 -0
- package/dist/launchd-maintenance-gate.js +80 -0
- package/dist/launchd-maintenance-gate.js.map +1 -0
- package/dist/launchd-private-files.d.ts +3 -0
- package/dist/launchd-private-files.d.ts.map +1 -0
- package/dist/launchd-private-files.js +58 -0
- package/dist/launchd-private-files.js.map +1 -0
- package/dist/launchd.d.ts +56 -2
- package/dist/launchd.d.ts.map +1 -1
- package/dist/launchd.js +210 -5
- package/dist/launchd.js.map +1 -1
- package/dist/local-configuration.d.ts.map +1 -1
- package/dist/local-configuration.js +33 -7
- package/dist/local-configuration.js.map +1 -1
- package/dist/managed-launchd-maintenance-environment.d.ts +3 -0
- package/dist/managed-launchd-maintenance-environment.d.ts.map +1 -0
- package/dist/managed-launchd-maintenance-environment.js +12 -0
- package/dist/managed-launchd-maintenance-environment.js.map +1 -0
- package/dist/managed-runtime-launch-proof.d.ts +11 -0
- package/dist/managed-runtime-launch-proof.d.ts.map +1 -0
- package/dist/managed-runtime-launch-proof.js +59 -0
- package/dist/managed-runtime-launch-proof.js.map +1 -0
- package/dist/managed-runtime-maintenance-entry.d.ts +10 -0
- package/dist/managed-runtime-maintenance-entry.d.ts.map +1 -0
- package/dist/managed-runtime-maintenance-entry.js +244 -0
- package/dist/managed-runtime-maintenance-entry.js.map +1 -0
- package/dist/managed-web-logs.d.ts +29 -11
- package/dist/managed-web-logs.d.ts.map +1 -1
- package/dist/managed-web-logs.js +244 -121
- package/dist/managed-web-logs.js.map +1 -1
- package/dist/managed-web-maintenance-environment.d.ts +6 -0
- package/dist/managed-web-maintenance-environment.d.ts.map +1 -0
- package/dist/managed-web-maintenance-environment.js +19 -0
- package/dist/managed-web-maintenance-environment.js.map +1 -0
- package/dist/mcp-apps.d.ts +37 -0
- package/dist/mcp-apps.d.ts.map +1 -0
- package/dist/mcp-apps.js +1753 -0
- package/dist/mcp-apps.js.map +1 -0
- package/dist/memory-command.d.ts +12 -0
- package/dist/memory-command.d.ts.map +1 -1
- package/dist/memory-command.js +510 -1
- package/dist/memory-command.js.map +1 -1
- package/dist/modules/known-tools.d.ts +1 -1
- package/dist/modules/known-tools.d.ts.map +1 -1
- package/dist/modules/known-tools.js +2 -0
- package/dist/modules/known-tools.js.map +1 -1
- package/dist/posted-reply-history.d.ts.map +1 -1
- package/dist/posted-reply-history.js +12 -0
- package/dist/posted-reply-history.js.map +1 -1
- package/dist/proactive-notify.d.ts +4 -1
- package/dist/proactive-notify.d.ts.map +1 -1
- package/dist/proactive-notify.js +43 -5
- package/dist/proactive-notify.js.map +1 -1
- package/dist/process-jobs-config.d.ts +82 -0
- package/dist/process-jobs-config.d.ts.map +1 -0
- package/dist/process-jobs-config.js +423 -0
- package/dist/process-jobs-config.js.map +1 -0
- package/dist/process-jobs-context.d.ts +24 -0
- package/dist/process-jobs-context.d.ts.map +1 -0
- package/dist/process-jobs-context.js +135 -0
- package/dist/process-jobs-context.js.map +1 -0
- package/dist/process-jobs-root-registry.d.ts +115 -0
- package/dist/process-jobs-root-registry.d.ts.map +1 -0
- package/dist/process-jobs-root-registry.js +978 -0
- package/dist/process-jobs-root-registry.js.map +1 -0
- package/dist/process-jobs-runtime.d.ts +35 -0
- package/dist/process-jobs-runtime.d.ts.map +1 -0
- package/dist/process-jobs-runtime.js +177 -0
- package/dist/process-jobs-runtime.js.map +1 -0
- package/dist/process-jobs-service.d.ts +70 -0
- package/dist/process-jobs-service.d.ts.map +1 -0
- package/dist/process-jobs-service.js +1989 -0
- package/dist/process-jobs-service.js.map +1 -0
- package/dist/process-jobs-store.d.ts +151 -0
- package/dist/process-jobs-store.d.ts.map +1 -0
- package/dist/process-jobs-store.js +1290 -0
- package/dist/process-jobs-store.js.map +1 -0
- package/dist/reply-artifacts.d.ts +65 -0
- package/dist/reply-artifacts.d.ts.map +1 -0
- package/dist/reply-artifacts.js +1147 -0
- package/dist/reply-artifacts.js.map +1 -0
- package/dist/reply-part-budget.d.ts +16 -0
- package/dist/reply-part-budget.d.ts.map +1 -0
- package/dist/reply-part-budget.js +57 -0
- package/dist/reply-part-budget.js.map +1 -0
- package/dist/request-model-override.d.ts +19 -0
- package/dist/request-model-override.d.ts.map +1 -1
- package/dist/request-model-override.js +54 -1
- package/dist/request-model-override.js.map +1 -1
- package/dist/request-scoped-mcp.d.ts +31 -0
- package/dist/request-scoped-mcp.d.ts.map +1 -0
- package/dist/request-scoped-mcp.js +197 -0
- package/dist/request-scoped-mcp.js.map +1 -0
- package/dist/run-history.d.ts.map +1 -1
- package/dist/run-history.js +50 -254
- package/dist/run-history.js.map +1 -1
- package/dist/runtime-option-extensions.d.ts +21 -0
- package/dist/runtime-option-extensions.d.ts.map +1 -1
- package/dist/runtime-option-extensions.js +53 -0
- package/dist/runtime-option-extensions.js.map +1 -1
- package/dist/secure-file-replace.d.ts +3 -0
- package/dist/secure-file-replace.d.ts.map +1 -1
- package/dist/secure-file-replace.js +31 -7
- package/dist/secure-file-replace.js.map +1 -1
- package/dist/session-history.d.ts +146 -0
- package/dist/session-history.d.ts.map +1 -0
- package/dist/session-history.js +347 -0
- package/dist/session-history.js.map +1 -0
- package/dist/sessions.d.ts +45 -2
- package/dist/sessions.d.ts.map +1 -1
- package/dist/sessions.js +849 -30
- package/dist/sessions.js.map +1 -1
- package/dist/web-command.d.ts +12 -11
- package/dist/web-command.d.ts.map +1 -1
- package/dist/web-command.js +554 -80
- package/dist/web-command.js.map +1 -1
- package/dist/web-log-maintenance-command.d.ts +7 -0
- package/dist/web-log-maintenance-command.d.ts.map +1 -0
- package/dist/web-log-maintenance-command.js +21 -0
- package/dist/web-log-maintenance-command.js.map +1 -0
- package/dist/web-log-maintenance-status.d.ts +26 -0
- package/dist/web-log-maintenance-status.d.ts.map +1 -0
- package/dist/web-log-maintenance-status.js +171 -0
- package/dist/web-log-maintenance-status.js.map +1 -0
- package/dist/web-log-maintenance.d.ts +32 -0
- package/dist/web-log-maintenance.d.ts.map +1 -0
- package/dist/web-log-maintenance.js +163 -0
- package/dist/web-log-maintenance.js.map +1 -0
- package/dist/web-maintenance-paths.d.ts +12 -0
- package/dist/web-maintenance-paths.d.ts.map +1 -0
- package/dist/web-maintenance-paths.js +24 -0
- package/dist/web-maintenance-paths.js.map +1 -0
- package/dist/wizard/prompts.d.ts.map +1 -1
- package/dist/wizard/prompts.js +1 -0
- package/dist/wizard/prompts.js.map +1 -1
- package/dist/wizard/run.js +1 -1
- package/dist/wizard/run.js.map +1 -1
- package/package.json +15 -15
- package/schema/mono-agent.config.schema.json +102 -1
- package/skills/mono-agent-composer/references/config-blueprint.md +26 -2
- package/skills/mono-agent-composer/references/discovery-questions.md +2 -2
- package/skills/mono-agent-composer/references/feature-coverage.md +6 -4
- package/skills/mono-agent-composer/references/package-map.md +19 -3
- package/skills/mono-agent-composer/references/playbooks.md +3 -3
- package/skills/mono-agent-composer/references/validation.md +4 -2
package/dist/web-command.js
CHANGED
|
@@ -2,25 +2,28 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { networkInterfaces } from "node:os";
|
|
4
4
|
import { readFile, rm } from "node:fs/promises";
|
|
5
|
-
import { dirname,
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
6
|
import process from "node:process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { pruneTraceSources } from "@mono-agent/observability";
|
|
9
9
|
import { isLoopbackHost } from "@mono-agent/agent-contracts";
|
|
10
|
-
import { accountHomeDirectory } from "./account-home.js";
|
|
11
10
|
import { resolveGlobalTraceRegistryDir } from "./app-config.js";
|
|
12
|
-
import { acquireFilesystemLifecycleLock, ensureOwnerPrivateLaunchdDirectory, writeOwnerPrivateLaunchdFile, } from "./background.js";
|
|
11
|
+
import { acquireFilesystemLifecycleLock, ensureOwnerPrivateLaunchdDirectory, inspectOwnerPrivateLaunchdPlist, readOwnerPrivateLaunchdPlist, writeOwnerPrivateLaunchdFile, } from "./background.js";
|
|
13
12
|
import { selectBackgroundOperationalEnvironment } from "./background-environment.js";
|
|
14
13
|
import { ensureManagedBackgroundRuntime } from "./background-runtime.js";
|
|
15
|
-
import { bootout, bootstrap, buildWebPlistXml, defaultPathEnv, launchdServiceInfo, makeLaunchctlRunner, } from "./launchd.js";
|
|
16
|
-
import {
|
|
14
|
+
import { bootout, bootstrap, buildEnvironmentArguments, buildWebMaintenancePlistXml, buildWebPlistXml, defaultPathEnv, INTERNAL_WEB_LOG_MAINTENANCE_COMMAND, launchdMaintenanceEntrypointPathForCli, launchdServiceInfo, launchdWebMaintenanceInfo, makeLaunchctlRunner, WEB_LAUNCHD_LABEL, WEB_MAINTENANCE_LAUNCHD_LABEL, webMaintenanceCalendarMinute, } from "./launchd.js";
|
|
15
|
+
import { beginLaunchdLogMaintenanceIntent, clearLaunchdLogMaintenanceIntent, inspectLaunchdLogs, markLaunchdLogMaintenanceRestoring, markLaunchdLogMaintenanceStopped, readLaunchdLogMaintenanceIntent, rotateStoppedLaunchdLogs, } from "./launchd-logs.js";
|
|
16
|
+
import { inspectLegacyManagedWebLogArtifacts, maintainLegacyManagedWebLogArtifacts, startManagedWebLogMonitor, } from "./managed-web-logs.js";
|
|
17
|
+
import { managedWebLogMaintenanceEnvironment } from "./managed-web-maintenance-environment.js";
|
|
18
|
+
import { verifyManagedRuntimeMaintenanceEntrypoint } from "./managed-runtime-maintenance-entry.js";
|
|
19
|
+
import { managedWebPaths } from "./web-maintenance-paths.js";
|
|
20
|
+
import { readWebLogMaintenanceStatus, readWebLogMonitorStatus, writeWebLogMaintenanceStatus, writeWebLogMonitorStatus, } from "./web-log-maintenance-status.js";
|
|
17
21
|
import * as ui from "./ui.js";
|
|
18
|
-
export {
|
|
22
|
+
export { WEB_LAUNCHD_LABEL } from "./launchd.js";
|
|
19
23
|
export const DEFAULT_WEB_HOST = "0.0.0.0";
|
|
20
24
|
export const DEFAULT_WEB_PORT = 5050;
|
|
21
25
|
// Deliberately outside `com.mono-agent.*`: fleet discovery reserves that prefix
|
|
22
26
|
// for configured agent instances.
|
|
23
|
-
export const WEB_LAUNCHD_LABEL = "com.mono-agent-web";
|
|
24
27
|
export const MANAGED_WEB_WORKER_ENV = "MONO_AGENT_MANAGED_WEB_WORKER";
|
|
25
28
|
const DEFAULT_LOG_LINES = 200;
|
|
26
29
|
const WEB_SERVICE_SCHEMA = "mono-agent.web-service.v1";
|
|
@@ -34,22 +37,8 @@ const TAILSCALE_STATUS_RETRY_MS = 200;
|
|
|
34
37
|
const WEB_PACKAGE_NAME = "@mono-agent/web";
|
|
35
38
|
const WEB_THEMES = ["evergreen", "ocean", "plum", "terracotta"];
|
|
36
39
|
const DEFAULT_WEB_THEME = "evergreen";
|
|
37
|
-
export function webPaths(homeDir
|
|
38
|
-
|
|
39
|
-
const logDir = join(stateDir, "logs");
|
|
40
|
-
const launchAgentsDir = resolve(homeDir, "Library", "LaunchAgents");
|
|
41
|
-
return {
|
|
42
|
-
stateDir,
|
|
43
|
-
recordPath: join(stateDir, "service.json"),
|
|
44
|
-
tailscalePath: join(stateDir, "tailscale-serve.json"),
|
|
45
|
-
launchd: {
|
|
46
|
-
launchAgentsDir,
|
|
47
|
-
logDir,
|
|
48
|
-
plistPath: join(launchAgentsDir, `${WEB_LAUNCHD_LABEL}.plist`),
|
|
49
|
-
stdoutPath: join(logDir, "web.out.log"),
|
|
50
|
-
stderrPath: join(logDir, "web.err.log"),
|
|
51
|
-
},
|
|
52
|
-
};
|
|
40
|
+
export function webPaths(homeDir) {
|
|
41
|
+
return managedWebPaths(homeDir);
|
|
53
42
|
}
|
|
54
43
|
export function renderWebHelp() {
|
|
55
44
|
return [
|
|
@@ -155,38 +144,21 @@ async function runWebForeground(options, deps) {
|
|
|
155
144
|
}
|
|
156
145
|
printWebUrls(stdout, handle.url, handle.port ?? port, host, deps.discoverNetworkAddresses);
|
|
157
146
|
stdout.write("No app authentication is enabled; network reachability is the access boundary. Press Ctrl-C to stop.\n");
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
147
|
+
const monitor = options.env[MANAGED_WEB_WORKER_ENV] === "1"
|
|
148
|
+
? (deps.startManagedLogMonitor ?? startManagedWebLogMonitor)(paths.launchd, {
|
|
149
|
+
runner: deps.launchctl ?? makeLaunchctlRunner(),
|
|
150
|
+
getuid: deps.getuid ?? requiredUid,
|
|
151
|
+
stderr: (text) => { stderr.write(text); },
|
|
152
|
+
recordStatus: async (status) => await writeWebLogMonitorStatus(paths.monitorStatusPath, status, deps.writePrivateFile ?? writeOwnerPrivateLaunchdFile),
|
|
153
|
+
})
|
|
154
|
+
: undefined;
|
|
163
155
|
try {
|
|
164
|
-
|
|
165
|
-
(deps.waitForShutdown ?? waitForShutdownSignal)().then(() => "shutdown"),
|
|
166
|
-
managedLogOutcome,
|
|
167
|
-
]);
|
|
156
|
+
await (deps.waitForShutdown ?? waitForShutdownSignal)();
|
|
168
157
|
}
|
|
169
158
|
finally {
|
|
170
|
-
|
|
159
|
+
monitor?.stop();
|
|
171
160
|
await handle.stop();
|
|
172
161
|
}
|
|
173
|
-
if (outcome === "unsafe") {
|
|
174
|
-
stderr.write(ui.errorLine("Managed web log maintenance found an unsafe log path; the worker stopped without changing it."));
|
|
175
|
-
return 1;
|
|
176
|
-
}
|
|
177
|
-
if (outcome === "rollover") {
|
|
178
|
-
try {
|
|
179
|
-
await (deps.rolloverManagedLogs ?? rolloverManagedWebLogs)(paths);
|
|
180
|
-
}
|
|
181
|
-
catch (error) {
|
|
182
|
-
stderr.write(ui.errorLine(`Managed web log rollover failed: ${errorMessage(error)}`));
|
|
183
|
-
return 1;
|
|
184
|
-
}
|
|
185
|
-
// KeepAlive restarts only after an unsuccessful exit. The old stdout/stderr
|
|
186
|
-
// descriptors now point at unlinked retiring files; launchd opens fresh
|
|
187
|
-
// active paths for the replacement worker.
|
|
188
|
-
return 75;
|
|
189
|
-
}
|
|
190
162
|
stdout.write("mono-agent web stopped.\n");
|
|
191
163
|
return 0;
|
|
192
164
|
}
|
|
@@ -216,6 +188,7 @@ async function startWebBackground(options, deps, restart) {
|
|
|
216
188
|
let replacementLaunchAttempted = false;
|
|
217
189
|
let publicationAttempted = false;
|
|
218
190
|
let publicationSnapshot;
|
|
191
|
+
let pendingMaintenanceIntent;
|
|
219
192
|
const fail = async (message, error) => {
|
|
220
193
|
stderr.write(ui.errorLine(`${message}${error === undefined ? "" : `: ${errorMessage(error)}`}`));
|
|
221
194
|
if (previousStopped && previous !== undefined) {
|
|
@@ -231,10 +204,14 @@ async function startWebBackground(options, deps, restart) {
|
|
|
231
204
|
let safeToRestorePublication = true;
|
|
232
205
|
if (replacementLaunchAttempted) {
|
|
233
206
|
try {
|
|
207
|
+
const helper = await launchdServiceInfo(launchctl, WEB_MAINTENANCE_LAUNCHD_LABEL, getuid());
|
|
208
|
+
if (helper.loaded) {
|
|
209
|
+
safeToRestorePublication = await stopLaunchdOnly(WEB_MAINTENANCE_LAUNCHD_LABEL, helper, launchctl, getuid(), deps);
|
|
210
|
+
}
|
|
234
211
|
const replacement = await launchdServiceInfo(launchctl, WEB_LAUNCHD_LABEL, getuid());
|
|
235
212
|
if (replacement.loaded) {
|
|
236
|
-
const stopped = await stopLaunchdOnly(replacement, launchctl, getuid(), deps);
|
|
237
|
-
safeToRestorePublication = stopped;
|
|
213
|
+
const stopped = await stopLaunchdOnly(WEB_LAUNCHD_LABEL, replacement, launchctl, getuid(), deps);
|
|
214
|
+
safeToRestorePublication = safeToRestorePublication && stopped;
|
|
238
215
|
stderr.write(stopped
|
|
239
216
|
? ui.style.yellow("⚠ The failed initial web worker was stopped.\n")
|
|
240
217
|
: ui.style.yellow("⚠ The failed initial web worker could not be proven stopped; inspect `mono-agent web status` and logs.\n"));
|
|
@@ -259,11 +236,46 @@ async function startWebBackground(options, deps, restart) {
|
|
|
259
236
|
try {
|
|
260
237
|
const uid = getuid();
|
|
261
238
|
const existing = await launchdServiceInfo(launchctl, WEB_LAUNCHD_LABEL, uid);
|
|
239
|
+
let helper = await launchdServiceInfo(launchctl, WEB_MAINTENANCE_LAUNCHD_LABEL, uid);
|
|
262
240
|
if (existing.loaded && !restart) {
|
|
263
241
|
if (options.theme !== undefined) {
|
|
264
242
|
stderr.write(ui.errorLine(`mono-agent web is already managed by launchd; use \`mono-agent web restart --theme ${options.theme}\` to change its theme.`));
|
|
265
243
|
return 1;
|
|
266
244
|
}
|
|
245
|
+
if (helper.loaded) {
|
|
246
|
+
try {
|
|
247
|
+
const loadedHelper = await (deps.inspectMaintenanceService ?? launchdWebMaintenanceInfo)(launchctl, uid);
|
|
248
|
+
await inspectInstalledWebMaintenanceHelper(paths, undefined, deps.verifyMaintenanceEntrypoint, loadedHelper.definition, true);
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
stderr.write(ui.errorLine(`The running web worker was preserved, but its maintenance helper is stale or unverifiable: ${errorMessage(error)}`));
|
|
252
|
+
stderr.write(ui.hint("Repair the paired definitions with: mono-agent web restart"));
|
|
253
|
+
return 1;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
try {
|
|
258
|
+
await ensureWebDirectories(paths);
|
|
259
|
+
const currentCliPath = fileURLToPath(new URL("./cli.js", import.meta.url));
|
|
260
|
+
const runtime = await (deps.ensureManagedRuntime ?? ((input) => ensureManagedBackgroundRuntime(input)))({
|
|
261
|
+
currentCliPath,
|
|
262
|
+
nodePath: process.execPath,
|
|
263
|
+
});
|
|
264
|
+
await publishHelperForExistingMain(paths, maintenanceInputsForRuntime(runtime, paths), deps.writePrivateFile ?? writeOwnerPrivateLaunchdFile);
|
|
265
|
+
const booted = await bootstrap(launchctl, paths.maintenancePlistPath, uid);
|
|
266
|
+
if (booted.code !== 0) {
|
|
267
|
+
throw new Error(commandDetail(booted) || `launchctl bootstrap exited ${String(booted.code)}`);
|
|
268
|
+
}
|
|
269
|
+
helper = await launchdServiceInfo(launchctl, WEB_MAINTENANCE_LAUNCHD_LABEL, uid);
|
|
270
|
+
if (!helper.loaded)
|
|
271
|
+
throw new Error("launchd did not retain the maintenance helper");
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
stderr.write(ui.errorLine(`The running web worker was preserved, but its missing maintenance helper could not be installed: ${errorMessage(error)}`));
|
|
275
|
+
stderr.write(ui.hint("Repair the paired definitions with: mono-agent web restart"));
|
|
276
|
+
return 1;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
267
279
|
stdout.write(ui.style.dim("mono-agent web is already managed by launchd.\n"));
|
|
268
280
|
return await statusWeb(options, deps, true);
|
|
269
281
|
}
|
|
@@ -271,9 +283,47 @@ async function startWebBackground(options, deps, restart) {
|
|
|
271
283
|
if (recordRead.kind === "invalid") {
|
|
272
284
|
return await fail(`Refusing to start because ${recordRead.detail}`);
|
|
273
285
|
}
|
|
286
|
+
try {
|
|
287
|
+
pendingMaintenanceIntent = await readLaunchdLogMaintenanceIntent(paths.launchd);
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
return await fail("Refusing to start because the durable web log-maintenance intent is unsafe", error);
|
|
291
|
+
}
|
|
292
|
+
if (pendingMaintenanceIntent?.phase === "stopping") {
|
|
293
|
+
stderr.write(ui.errorLine("Refusing to change either LaunchAgent because an abandoned stopping intent lacks complete prior PID-death proof."));
|
|
294
|
+
stderr.write(ui.hint("Recover safely with `mono-agent web stop`, then `mono-agent web start`; restart cannot promote an unproven stop."));
|
|
295
|
+
return 1;
|
|
296
|
+
}
|
|
274
297
|
const existingPlist = await readOptionalText(paths.launchd.plistPath);
|
|
298
|
+
if (pendingMaintenanceIntent !== undefined) {
|
|
299
|
+
let currentMainIdentity;
|
|
300
|
+
try {
|
|
301
|
+
currentMainIdentity = await inspectOwnerPrivateLaunchdPlist(paths.launchd.plistPath);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
stderr.write(ui.errorLine("Refusing to recover web log maintenance because its main LaunchAgent identity is absent or unsafe."));
|
|
305
|
+
stderr.write(ui.hint("Recover safely with `mono-agent web stop`, then `mono-agent web start`; stop clears stale authority only after both jobs are down."));
|
|
306
|
+
return 1;
|
|
307
|
+
}
|
|
308
|
+
if (pendingMaintenanceIntent.plistFingerprint !== currentMainIdentity) {
|
|
309
|
+
stderr.write(ui.errorLine("Refusing to recover web log maintenance because its durable intent authenticates an older main LaunchAgent identity."));
|
|
310
|
+
stderr.write(ui.hint("Recover safely with `mono-agent web stop`, then `mono-agent web start`; restart cannot reuse stale rotation authority."));
|
|
311
|
+
return 1;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
let existingHelperInputs;
|
|
315
|
+
if (await readOptionalText(paths.maintenancePlistPath) !== undefined) {
|
|
316
|
+
try {
|
|
317
|
+
existingHelperInputs = await inspectInstalledWebMaintenanceHelper(paths, undefined, deps.verifyMaintenanceEntrypoint);
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
// Explicit start/restart may repair a stopped or stale helper. A loaded
|
|
321
|
+
// healthy worker took the preserve-and-restart-instruction path above.
|
|
322
|
+
}
|
|
323
|
+
}
|
|
275
324
|
publicationSnapshot = {
|
|
276
325
|
...(existingPlist === undefined ? {} : { plist: existingPlist }),
|
|
326
|
+
...(existingHelperInputs === undefined ? {} : { helperInputs: existingHelperInputs }),
|
|
277
327
|
...(recordRead.kind === "valid" ? { recordText: recordRead.contents } : {}),
|
|
278
328
|
};
|
|
279
329
|
if (existing.loaded && restart) {
|
|
@@ -284,6 +334,7 @@ async function startWebBackground(options, deps, restart) {
|
|
|
284
334
|
throw new Error("the loaded web service record is missing");
|
|
285
335
|
previous = {
|
|
286
336
|
plist: existingPlist,
|
|
337
|
+
...(existingHelperInputs === undefined ? {} : { helperInputs: existingHelperInputs }),
|
|
287
338
|
recordText: recordRead.contents,
|
|
288
339
|
record: recordRead.record,
|
|
289
340
|
};
|
|
@@ -292,18 +343,9 @@ async function startWebBackground(options, deps, restart) {
|
|
|
292
343
|
return await fail("Refusing restart because the current web service could not be snapshotted", error);
|
|
293
344
|
}
|
|
294
345
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
stderr.write(ui.errorLine("Could not prove the existing mono-agent web worker stopped; its definition was preserved."));
|
|
299
|
-
return 1;
|
|
300
|
-
}
|
|
301
|
-
previousStopped = restart && previous !== undefined;
|
|
302
|
-
}
|
|
303
|
-
const priorRecord = recordRead.kind === "valid" ? recordRead.record : undefined;
|
|
304
|
-
const host = effectiveHost(options, priorRecord?.host);
|
|
305
|
-
const port = options.port ?? priorRecord?.port ?? DEFAULT_WEB_PORT;
|
|
306
|
-
const theme = selectedWebTheme(options.theme, priorRecord?.theme);
|
|
346
|
+
// Establish structured helper inputs before unloading anything. If runtime
|
|
347
|
+
// installation fails, the current pair remains untouched and no rollback
|
|
348
|
+
// needs authority that was not captured.
|
|
307
349
|
await ensureWebDirectories(paths);
|
|
308
350
|
const currentCliPath = fileURLToPath(new URL("./cli.js", import.meta.url));
|
|
309
351
|
let runtime;
|
|
@@ -316,6 +358,50 @@ async function startWebBackground(options, deps, restart) {
|
|
|
316
358
|
catch (error) {
|
|
317
359
|
return await fail("Could not install the durable web runtime", error);
|
|
318
360
|
}
|
|
361
|
+
const helperInputs = maintenanceInputsForRuntime(runtime, paths);
|
|
362
|
+
if (publicationSnapshot.plist !== undefined && publicationSnapshot.helperInputs === undefined) {
|
|
363
|
+
publicationSnapshot = { ...publicationSnapshot, helperInputs };
|
|
364
|
+
}
|
|
365
|
+
if (previous !== undefined && previous.helperInputs === undefined) {
|
|
366
|
+
previous = { ...previous, helperInputs };
|
|
367
|
+
}
|
|
368
|
+
const helperWasLoaded = helper.loaded;
|
|
369
|
+
if (helper.loaded) {
|
|
370
|
+
const stopped = await stopLaunchdOnly(WEB_MAINTENANCE_LAUNCHD_LABEL, helper, launchctl, uid, deps);
|
|
371
|
+
if (!stopped) {
|
|
372
|
+
stderr.write(ui.errorLine("Could not prove the web-maintenance helper stopped; both definitions were preserved."));
|
|
373
|
+
return 1;
|
|
374
|
+
}
|
|
375
|
+
helper = { loaded: false };
|
|
376
|
+
}
|
|
377
|
+
if (existing.loaded) {
|
|
378
|
+
const stopped = await stopLaunchdOnly(WEB_LAUNCHD_LABEL, existing, launchctl, uid, deps);
|
|
379
|
+
if (!stopped) {
|
|
380
|
+
if (helperWasLoaded) {
|
|
381
|
+
const restoredHelper = await bootstrap(launchctl, paths.maintenancePlistPath, uid);
|
|
382
|
+
const helperRestored = restoredHelper.code === 0
|
|
383
|
+
&& (await launchdServiceInfo(launchctl, WEB_MAINTENANCE_LAUNCHD_LABEL, uid)).loaded;
|
|
384
|
+
if (!helperRestored) {
|
|
385
|
+
stderr.write(ui.style.yellow(`⚠ The worker remained loaded, but its prior maintenance helper could not be proven restored: ${commandDetail(restoredHelper) || "launchd did not retain the helper"}\n`));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
stderr.write(ui.errorLine("Could not prove the existing mono-agent web worker stopped; its definition was preserved."));
|
|
389
|
+
return 1;
|
|
390
|
+
}
|
|
391
|
+
previousStopped = restart && previous !== undefined;
|
|
392
|
+
}
|
|
393
|
+
const priorRecord = recordRead.kind === "valid" ? recordRead.record : undefined;
|
|
394
|
+
const host = effectiveHost(options, priorRecord?.host);
|
|
395
|
+
const port = options.port ?? priorRecord?.port ?? DEFAULT_WEB_PORT;
|
|
396
|
+
const theme = selectedWebTheme(options.theme, priorRecord?.theme);
|
|
397
|
+
if (existingPlist !== undefined) {
|
|
398
|
+
try {
|
|
399
|
+
pendingMaintenanceIntent = await maintainStoppedWebLogsBeforePublication(paths, pendingMaintenanceIntent, deps.now ?? Date.now);
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
return await fail("Could not complete the proven stopped-window web log maintenance pass", error);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
319
405
|
const tailscaleRunner = deps.tailscale ?? makeTailscaleRunner(options.env);
|
|
320
406
|
const priorTailscaleOwnership = await readTailscaleOwnership(paths.tailscalePath);
|
|
321
407
|
const recordedTailscaleDnsName = priorTailscaleOwnership.kind === "valid"
|
|
@@ -362,12 +448,17 @@ async function startWebBackground(options, deps, restart) {
|
|
|
362
448
|
publicationAttempted = true;
|
|
363
449
|
try {
|
|
364
450
|
await writePrivateFile(paths.recordPath, `${JSON.stringify(record, undefined, 2)}\n`);
|
|
365
|
-
await
|
|
451
|
+
await publishPairedWebPlists(paths, plist, helperInputs, writePrivateFile);
|
|
366
452
|
}
|
|
367
453
|
catch (error) {
|
|
368
454
|
return await fail("Could not publish the web LaunchAgent definition", error);
|
|
369
455
|
}
|
|
370
456
|
replacementLaunchAttempted = true;
|
|
457
|
+
const helperBootstrapped = await bootstrap(launchctl, paths.maintenancePlistPath, uid);
|
|
458
|
+
if (helperBootstrapped.code !== 0) {
|
|
459
|
+
writeCommandDetail(stderr, helperBootstrapped);
|
|
460
|
+
return await fail(`launchctl could not start mono-agent web maintenance (exit ${String(helperBootstrapped.code)})`);
|
|
461
|
+
}
|
|
371
462
|
const bootstrapped = await bootstrap(launchctl, paths.launchd.plistPath, uid);
|
|
372
463
|
if (bootstrapped.code !== 0) {
|
|
373
464
|
writeCommandDetail(stderr, bootstrapped);
|
|
@@ -378,6 +469,25 @@ async function startWebBackground(options, deps, restart) {
|
|
|
378
469
|
stderr.write(ui.hint("Inspect `mono-agent web logs`, then retry `mono-agent web restart`."));
|
|
379
470
|
return await fail("mono-agent web did not become healthy before the startup timeout");
|
|
380
471
|
}
|
|
472
|
+
if (pendingMaintenanceIntent !== undefined) {
|
|
473
|
+
try {
|
|
474
|
+
await inspectInstalledWebMaintenanceHelper(paths, undefined, deps.verifyMaintenanceEntrypoint);
|
|
475
|
+
await clearLaunchdLogMaintenanceIntent(paths.launchd, pendingMaintenanceIntent);
|
|
476
|
+
const priorStatus = await readWebLogMaintenanceStatus(paths.maintenanceStatusPath);
|
|
477
|
+
if (priorStatus.kind !== "valid" || priorStatus.status.state !== "degraded") {
|
|
478
|
+
await writeWebLogMaintenanceStatus(paths.maintenanceStatusPath, {
|
|
479
|
+
version: 1,
|
|
480
|
+
state: "success",
|
|
481
|
+
phase: "complete",
|
|
482
|
+
updatedAt: new Date((deps.now ?? Date.now)()).toISOString(),
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
pendingMaintenanceIntent = undefined;
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
return await fail("The replacement worker is live, but stale rotation authority could not be revoked", error);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
381
491
|
const tailscale = tailscaleDnsName === undefined
|
|
382
492
|
? { kind: "unavailable", detail: "the node's exact Tailscale DNS name could not be resolved; no Serve handler was changed" }
|
|
383
493
|
: await ensureTailscaleServe(paths, host, port, options.env, { ...deps, tailscale: tailscaleRunner }, tailscaleDnsName);
|
|
@@ -415,11 +525,12 @@ async function stopWebBackground(options, deps) {
|
|
|
415
525
|
const uid = (deps.getuid ?? requiredUid)();
|
|
416
526
|
const paths = webPaths(deps.homeDir);
|
|
417
527
|
const initialService = await launchdServiceInfo(launchctl, WEB_LAUNCHD_LABEL, uid);
|
|
528
|
+
const initialHelper = await launchdServiceInfo(launchctl, WEB_MAINTENANCE_LAUNCHD_LABEL, uid);
|
|
418
529
|
// A running worker owns the backend state lease. Stop must reach launchd
|
|
419
530
|
// bootout without opening or preparing that state first. On a pristine,
|
|
420
531
|
// already-stopped install, preparation creates a valid marked state root
|
|
421
532
|
// before the filesystem lifecycle lock needs to place its lock directory.
|
|
422
|
-
if (!initialService.loaded) {
|
|
533
|
+
if (!initialService.loaded && !initialHelper.loaded) {
|
|
423
534
|
try {
|
|
424
535
|
await (deps.prepareState ?? defaultPrepareWebState)({ stateDir: paths.stateDir, env: options.env });
|
|
425
536
|
}
|
|
@@ -434,18 +545,58 @@ async function stopWebBackground(options, deps) {
|
|
|
434
545
|
return 1;
|
|
435
546
|
}
|
|
436
547
|
try {
|
|
548
|
+
const helper = await launchdServiceInfo(launchctl, WEB_MAINTENANCE_LAUNCHD_LABEL, uid);
|
|
549
|
+
const helperWasLoaded = helper.loaded;
|
|
550
|
+
if (helper.loaded && !await stopLaunchdOnly(WEB_MAINTENANCE_LAUNCHD_LABEL, helper, launchctl, uid, deps)) {
|
|
551
|
+
stderr.write(ui.errorLine("Could not prove mono-agent web maintenance stopped; both LaunchAgents were preserved."));
|
|
552
|
+
return 1;
|
|
553
|
+
}
|
|
437
554
|
const service = await launchdServiceInfo(launchctl, WEB_LAUNCHD_LABEL, uid);
|
|
438
|
-
if (service.loaded && !await stopLaunchdOnly(service, launchctl, uid, deps)) {
|
|
555
|
+
if (service.loaded && !await stopLaunchdOnly(WEB_LAUNCHD_LABEL, service, launchctl, uid, deps)) {
|
|
556
|
+
if (helperWasLoaded) {
|
|
557
|
+
let helperRestored = false;
|
|
558
|
+
let restoreDetail = "launchd did not retain the helper";
|
|
559
|
+
try {
|
|
560
|
+
const restoredHelper = await bootstrap(launchctl, paths.maintenancePlistPath, uid);
|
|
561
|
+
helperRestored = restoredHelper.code === 0
|
|
562
|
+
&& (await launchdServiceInfo(launchctl, WEB_MAINTENANCE_LAUNCHD_LABEL, uid)).loaded;
|
|
563
|
+
restoreDetail = commandDetail(restoredHelper) || restoreDetail;
|
|
564
|
+
}
|
|
565
|
+
catch (error) {
|
|
566
|
+
restoreDetail = errorMessage(error);
|
|
567
|
+
}
|
|
568
|
+
if (!helperRestored) {
|
|
569
|
+
stderr.write(ui.style.yellow(`⚠ The worker remained loaded, but its prior maintenance helper could not be proven restored: ${restoreDetail}\n`));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
439
572
|
stderr.write(ui.errorLine("Could not prove mono-agent web stopped; its LaunchAgent and Tailscale handler were preserved."));
|
|
440
573
|
return 1;
|
|
441
574
|
}
|
|
442
575
|
const tailscaleResult = await removeOwnedTailscaleServe(paths, deps);
|
|
576
|
+
try {
|
|
577
|
+
let intent = await readLaunchdLogMaintenanceIntent(paths.launchd);
|
|
578
|
+
// Explicit stop has just proven both launchd jobs and every observed PID
|
|
579
|
+
// gone. Convert pre-proof or stale restoration authority into the same
|
|
580
|
+
// durable stopped proof before clearing it; clear itself never accepts a
|
|
581
|
+
// raw stopping intent.
|
|
582
|
+
if (intent?.phase === "stopping" || intent?.phase === "restoring") {
|
|
583
|
+
intent = await markLaunchdLogMaintenanceStopped(paths.launchd, intent);
|
|
584
|
+
}
|
|
585
|
+
await clearLaunchdLogMaintenanceIntent(paths.launchd, intent);
|
|
586
|
+
await rm(paths.monitorStatusPath, { force: true });
|
|
587
|
+
await rm(paths.maintenanceStatusPath, { force: true });
|
|
588
|
+
}
|
|
589
|
+
catch (error) {
|
|
590
|
+
stderr.write(ui.errorLine(`Web stopped, but maintenance recovery state could not be safely cleared: ${errorMessage(error)}`));
|
|
591
|
+
return 1;
|
|
592
|
+
}
|
|
593
|
+
await rm(paths.maintenancePlistPath, { force: true });
|
|
443
594
|
await rm(paths.launchd.plistPath, { force: true });
|
|
444
595
|
if (tailscaleResult.kind === "unavailable") {
|
|
445
596
|
stderr.write(ui.style.yellow(`⚠ Web stopped, but the owned Tailscale handler was preserved: ${tailscaleResult.detail}\n`));
|
|
446
597
|
return 1;
|
|
447
598
|
}
|
|
448
|
-
stdout.write(service.loaded
|
|
599
|
+
stdout.write(service.loaded || helper.loaded
|
|
449
600
|
? `${ui.badge("ok")}${ui.style.bold("Stopped mono-agent web")} and removed its LaunchAgent.\n`
|
|
450
601
|
: "mono-agent web was already stopped; removed its LaunchAgent if present.\n");
|
|
451
602
|
return 0;
|
|
@@ -463,8 +614,14 @@ async function statusWeb(options, deps, strictExit) {
|
|
|
463
614
|
const port = record?.port ?? DEFAULT_WEB_PORT;
|
|
464
615
|
const theme = record?.theme ?? DEFAULT_WEB_THEME;
|
|
465
616
|
let service = { loaded: false };
|
|
617
|
+
let helper = { loaded: false };
|
|
466
618
|
if ((deps.platform ?? process.platform) === "darwin") {
|
|
467
|
-
|
|
619
|
+
const runner = deps.launchctl ?? makeLaunchctlRunner();
|
|
620
|
+
const uid = (deps.getuid ?? requiredUid)();
|
|
621
|
+
[service, helper] = await Promise.all([
|
|
622
|
+
launchdServiceInfo(runner, WEB_LAUNCHD_LABEL, uid),
|
|
623
|
+
(deps.inspectMaintenanceService ?? launchdWebMaintenanceInfo)(runner, uid),
|
|
624
|
+
]);
|
|
468
625
|
}
|
|
469
626
|
const healthState = recordRead.kind === "invalid" || !service.loaded
|
|
470
627
|
? "unavailable"
|
|
@@ -472,6 +629,89 @@ async function statusWeb(options, deps, strictExit) {
|
|
|
472
629
|
? await webHealthStatus(healthUrl(host, port))
|
|
473
630
|
: await deps.healthcheck(healthUrl(host, port)) ? "ok" : "unavailable";
|
|
474
631
|
const healthy = healthState === "ok";
|
|
632
|
+
const maintenanceProblems = [];
|
|
633
|
+
let maintenanceSummary = helper.loaded ? "idle" : "helper missing";
|
|
634
|
+
let pendingIntent;
|
|
635
|
+
let pendingRecoveryRequiresStopStart = false;
|
|
636
|
+
try {
|
|
637
|
+
pendingIntent = await readLaunchdLogMaintenanceIntent(paths.launchd);
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
pendingIntent = undefined;
|
|
641
|
+
maintenanceProblems.push("maintenance intent is unreadable or unsafe");
|
|
642
|
+
}
|
|
643
|
+
const maintenanceStatus = await readWebLogMaintenanceStatus(paths.maintenanceStatusPath);
|
|
644
|
+
const monitorStatus = await readWebLogMonitorStatus(paths.monitorStatusPath);
|
|
645
|
+
if (helper.loaded) {
|
|
646
|
+
try {
|
|
647
|
+
await inspectInstalledWebMaintenanceHelper(paths, undefined, deps.verifyMaintenanceEntrypoint, helper.definition, true);
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
maintenanceProblems.push("helper definition or managed-runtime closure is invalid");
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
maintenanceProblems.push("maintenance helper is not loaded");
|
|
655
|
+
}
|
|
656
|
+
if (pendingIntent !== undefined) {
|
|
657
|
+
pendingRecoveryRequiresStopStart = pendingIntent.phase === "stopping";
|
|
658
|
+
if (!pendingRecoveryRequiresStopStart) {
|
|
659
|
+
try {
|
|
660
|
+
pendingRecoveryRequiresStopStart = pendingIntent.plistFingerprint
|
|
661
|
+
!== await inspectOwnerPrivateLaunchdPlist(paths.launchd.plistPath);
|
|
662
|
+
}
|
|
663
|
+
catch {
|
|
664
|
+
pendingRecoveryRequiresStopStart = true;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (helper.pid !== undefined) {
|
|
668
|
+
maintenanceSummary = `maintenance in progress (${pendingIntent.phase})`;
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
maintenanceSummary = "maintenance recovery required";
|
|
672
|
+
maintenanceProblems.push("durable maintenance intent has no live helper");
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
else if (helper.pid !== undefined) {
|
|
676
|
+
maintenanceSummary = maintenanceStatus.kind === "valid" && maintenanceStatus.status.state === "running"
|
|
677
|
+
? `maintenance in progress (${maintenanceStatus.status.phase})`
|
|
678
|
+
: "maintenance in progress";
|
|
679
|
+
}
|
|
680
|
+
else if (maintenanceStatus.kind === "valid") {
|
|
681
|
+
if (maintenanceStatus.status.state === "failed" || maintenanceStatus.status.state === "degraded") {
|
|
682
|
+
maintenanceSummary = maintenanceStatus.status.state;
|
|
683
|
+
maintenanceProblems.push(maintenanceStatus.status.detail ?? "the last maintenance pass did not complete cleanly");
|
|
684
|
+
}
|
|
685
|
+
if ((maintenanceStatus.status.refusals?.length ?? 0) > 0) {
|
|
686
|
+
maintenanceProblems.push("bounded legacy artifacts were refused and preserved");
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
else if (maintenanceStatus.kind === "invalid") {
|
|
690
|
+
maintenanceProblems.push("maintenance status is unreadable or unsafe");
|
|
691
|
+
}
|
|
692
|
+
if (monitorStatus.kind === "invalid") {
|
|
693
|
+
maintenanceProblems.push("worker monitor status is unreadable or unsafe");
|
|
694
|
+
}
|
|
695
|
+
else if (monitorStatus.kind === "valid"
|
|
696
|
+
&& ["helper-unloaded", "request-failed", "inspection-failed"].includes(monitorStatus.status.lastOutcome)) {
|
|
697
|
+
maintenanceProblems.push(`worker monitor reports ${monitorStatus.status.lastOutcome}`);
|
|
698
|
+
}
|
|
699
|
+
try {
|
|
700
|
+
const [logs, legacy] = await Promise.all([
|
|
701
|
+
inspectLaunchdLogs(paths.launchd),
|
|
702
|
+
inspectLegacyManagedWebLogArtifacts(paths.launchd),
|
|
703
|
+
]);
|
|
704
|
+
if (!logs.canMaintain || !legacy.canMaintain)
|
|
705
|
+
maintenanceProblems.push("managed web log inventory is unsafe");
|
|
706
|
+
else if (logs.needsMaintenance || legacy.needsMaintenance) {
|
|
707
|
+
maintenanceSummary = maintenanceSummary === "idle"
|
|
708
|
+
? "due"
|
|
709
|
+
: `${maintenanceSummary}; due`;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
maintenanceProblems.push("managed web log inventory could not be inspected");
|
|
714
|
+
}
|
|
475
715
|
stdout.write(ui.rule("Web console status"));
|
|
476
716
|
stdout.write(ui.keyValue([
|
|
477
717
|
["service", service.loaded
|
|
@@ -481,6 +721,7 @@ async function statusWeb(options, deps, strictExit) {
|
|
|
481
721
|
["theme", recordRead.kind === "invalid" ? "invalid service record" : theme],
|
|
482
722
|
["state", paths.stateDir],
|
|
483
723
|
["pid", service.pid === undefined ? "—" : String(service.pid)],
|
|
724
|
+
["log maintenance", maintenanceSummary],
|
|
484
725
|
["authentication", "none (network reachability is the boundary)"],
|
|
485
726
|
]));
|
|
486
727
|
if (recordRead.kind === "invalid")
|
|
@@ -505,7 +746,14 @@ async function statusWeb(options, deps, strictExit) {
|
|
|
505
746
|
stdout.write(ui.hint("Start it with: mono-agent web start"));
|
|
506
747
|
else if (!healthy)
|
|
507
748
|
stdout.write(ui.hint("Inspect: mono-agent web logs"));
|
|
508
|
-
|
|
749
|
+
for (const problem of [...new Set(maintenanceProblems)])
|
|
750
|
+
stdout.write(ui.errorLine(problem));
|
|
751
|
+
if (pendingIntent !== undefined && helper.pid === undefined) {
|
|
752
|
+
stdout.write(pendingRecoveryRequiresStopStart
|
|
753
|
+
? ui.hint("Recover it with `mono-agent web stop`, then `mono-agent web start`; the prior stop or plist identity is unproven.")
|
|
754
|
+
: ui.hint("Recover it with exactly: mono-agent web restart"));
|
|
755
|
+
}
|
|
756
|
+
return (strictExit && (!healthy || maintenanceProblems.length > 0)) || recordRead.kind === "invalid" ? 1 : 0;
|
|
509
757
|
}
|
|
510
758
|
async function tailWebLogs(options, deps) {
|
|
511
759
|
const guard = requireDarwin(deps, "logs");
|
|
@@ -526,15 +774,31 @@ async function resetWeb(options, deps) {
|
|
|
526
774
|
const stdout = deps.stdout ?? process.stdout;
|
|
527
775
|
const paths = webPaths(deps.homeDir);
|
|
528
776
|
if ((deps.platform ?? process.platform) === "darwin") {
|
|
529
|
-
const
|
|
530
|
-
|
|
777
|
+
const launchctl = deps.launchctl ?? makeLaunchctlRunner();
|
|
778
|
+
const uid = (deps.getuid ?? requiredUid)();
|
|
779
|
+
const [service, helper] = await Promise.all([
|
|
780
|
+
launchdServiceInfo(launchctl, WEB_LAUNCHD_LABEL, uid),
|
|
781
|
+
launchdServiceInfo(launchctl, WEB_MAINTENANCE_LAUNCHD_LABEL, uid),
|
|
782
|
+
]);
|
|
783
|
+
if (service.loaded || helper.loaded) {
|
|
531
784
|
stderr.write(ui.errorLine("Refusing to reset while mono-agent web is running."));
|
|
532
785
|
stderr.write(ui.hint("Run `mono-agent web stop`, then repeat `mono-agent web reset --all --yes`."));
|
|
533
786
|
return 1;
|
|
534
787
|
}
|
|
535
788
|
}
|
|
536
789
|
await (deps.prepareState ?? defaultPrepareWebState)({ stateDir: paths.stateDir, env: options.env });
|
|
790
|
+
const release = await (deps.acquireLifecycleLock ?? acquireWebLifecycleLock)(paths);
|
|
791
|
+
if (release === undefined) {
|
|
792
|
+
stderr.write(ui.errorLine("Another mono-agent web lifecycle command is active."));
|
|
793
|
+
return 1;
|
|
794
|
+
}
|
|
537
795
|
try {
|
|
796
|
+
if (await readOptionalText(paths.launchd.plistPath) !== undefined
|
|
797
|
+
|| await readOptionalText(paths.maintenancePlistPath) !== undefined) {
|
|
798
|
+
stderr.write(ui.errorLine("Refusing to reset while web LaunchAgent definitions remain installed."));
|
|
799
|
+
stderr.write(ui.hint("Run `mono-agent web stop`, then repeat `mono-agent web reset --all --yes`."));
|
|
800
|
+
return 1;
|
|
801
|
+
}
|
|
538
802
|
const tailscaleOwnershipBefore = await readOptionalText(paths.tailscalePath);
|
|
539
803
|
await (deps.resetState ?? defaultResetWebState)({ stateDir: paths.stateDir, env: options.env });
|
|
540
804
|
if (tailscaleOwnershipBefore !== undefined) {
|
|
@@ -550,6 +814,9 @@ async function resetWeb(options, deps) {
|
|
|
550
814
|
stderr.write(ui.errorLine(`Could not reset the web console: ${errorMessage(error)}`));
|
|
551
815
|
return 1;
|
|
552
816
|
}
|
|
817
|
+
finally {
|
|
818
|
+
await release().catch(() => undefined);
|
|
819
|
+
}
|
|
553
820
|
stdout.write(`${ui.badge("ok")}Reset all mono-agent web conversations, messages, attachments, and settings.\n`);
|
|
554
821
|
return 0;
|
|
555
822
|
}
|
|
@@ -586,19 +853,209 @@ async function ensureWebDirectories(paths) {
|
|
|
586
853
|
await ensureOwnerPrivateLaunchdDirectory(path);
|
|
587
854
|
}
|
|
588
855
|
}
|
|
856
|
+
function maintenanceInputsForRuntime(runtime, paths) {
|
|
857
|
+
return {
|
|
858
|
+
nodePath: runtime.nodePath,
|
|
859
|
+
cliPath: launchdMaintenanceEntrypointPathForCli(runtime.cliPath),
|
|
860
|
+
cwd: paths.stateDir,
|
|
861
|
+
expectedManagedRuntimeLaunch: runtime.launchProof,
|
|
862
|
+
environment: managedWebLogMaintenanceEnvironment(),
|
|
863
|
+
calendarMinute: webMaintenanceCalendarMinute(),
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
async function publishPairedWebPlists(paths, mainPlist, helperInputs, writer) {
|
|
867
|
+
await writer(paths.launchd.plistPath, mainPlist);
|
|
868
|
+
// Every main write creates a new inode, even when bytes are identical. Read
|
|
869
|
+
// the post-write composite identity before generating the paired helper.
|
|
870
|
+
const mainIdentity = await inspectOwnerPrivateLaunchdPlist(paths.launchd.plistPath);
|
|
871
|
+
const helperPlist = buildWebMaintenancePlistXml({
|
|
872
|
+
label: WEB_MAINTENANCE_LAUNCHD_LABEL,
|
|
873
|
+
...helperInputs,
|
|
874
|
+
expectedWebPlistIdentity: mainIdentity,
|
|
875
|
+
});
|
|
876
|
+
await writer(paths.maintenancePlistPath, helperPlist);
|
|
877
|
+
const committedHelper = await readOwnerPrivateLaunchdPlist(paths.maintenancePlistPath);
|
|
878
|
+
if (committedHelper.contents !== helperPlist) {
|
|
879
|
+
throw new Error("The web-maintenance helper changed while paired publication was verified.");
|
|
880
|
+
}
|
|
881
|
+
if (await inspectOwnerPrivateLaunchdPlist(paths.launchd.plistPath) !== mainIdentity) {
|
|
882
|
+
throw new Error("The main web LaunchAgent changed while its helper was paired.");
|
|
883
|
+
}
|
|
884
|
+
return mainIdentity;
|
|
885
|
+
}
|
|
886
|
+
async function publishHelperForExistingMain(paths, helperInputs, writer) {
|
|
887
|
+
const mainIdentity = await inspectOwnerPrivateLaunchdPlist(paths.launchd.plistPath);
|
|
888
|
+
const helperPlist = buildWebMaintenancePlistXml({
|
|
889
|
+
label: WEB_MAINTENANCE_LAUNCHD_LABEL,
|
|
890
|
+
...helperInputs,
|
|
891
|
+
expectedWebPlistIdentity: mainIdentity,
|
|
892
|
+
});
|
|
893
|
+
await writer(paths.maintenancePlistPath, helperPlist);
|
|
894
|
+
const committedHelper = await readOwnerPrivateLaunchdPlist(paths.maintenancePlistPath);
|
|
895
|
+
if (committedHelper.contents !== helperPlist
|
|
896
|
+
|| await inspectOwnerPrivateLaunchdPlist(paths.launchd.plistPath) !== mainIdentity) {
|
|
897
|
+
throw new Error("The existing main web LaunchAgent changed while its helper was published.");
|
|
898
|
+
}
|
|
899
|
+
return mainIdentity;
|
|
900
|
+
}
|
|
901
|
+
async function maintainStoppedWebLogsBeforePublication(paths, pending, now) {
|
|
902
|
+
const mainIdentity = await inspectOwnerPrivateLaunchdPlist(paths.launchd.plistPath);
|
|
903
|
+
if (pending !== undefined
|
|
904
|
+
&& (pending.label !== WEB_LAUNCHD_LABEL || pending.plistFingerprint !== mainIdentity)) {
|
|
905
|
+
throw new Error("The pending web log-maintenance intent does not authenticate the current main plist.");
|
|
906
|
+
}
|
|
907
|
+
if (pending?.phase === "stopping") {
|
|
908
|
+
throw new Error("The abandoned stopping intent has no complete prior PID-death proof; refusing rotation.");
|
|
909
|
+
}
|
|
910
|
+
if (pending?.phase === "restoring")
|
|
911
|
+
return pending;
|
|
912
|
+
const [logs, legacy] = await Promise.all([
|
|
913
|
+
inspectLaunchdLogs(paths.launchd),
|
|
914
|
+
inspectLegacyManagedWebLogArtifacts(paths.launchd),
|
|
915
|
+
]);
|
|
916
|
+
if (!logs.canMaintain) {
|
|
917
|
+
throw new Error(`Managed web log maintenance refused unsafe paths: ${logs.issues.join("; ")}`);
|
|
918
|
+
}
|
|
919
|
+
if (!logs.needsMaintenance && !legacy.needsMaintenance && pending === undefined)
|
|
920
|
+
return undefined;
|
|
921
|
+
let intent = pending;
|
|
922
|
+
if (intent === undefined) {
|
|
923
|
+
intent = {
|
|
924
|
+
version: 1,
|
|
925
|
+
phase: "stopping",
|
|
926
|
+
label: WEB_LAUNCHD_LABEL,
|
|
927
|
+
plistFingerprint: mainIdentity,
|
|
928
|
+
};
|
|
929
|
+
await beginLaunchdLogMaintenanceIntent(paths.launchd, intent);
|
|
930
|
+
intent = await markLaunchdLogMaintenanceStopped(paths.launchd, intent);
|
|
931
|
+
}
|
|
932
|
+
await writeWebLogMaintenanceStatus(paths.maintenanceStatusPath, {
|
|
933
|
+
version: 1,
|
|
934
|
+
state: "running",
|
|
935
|
+
phase: "rotating",
|
|
936
|
+
updatedAt: new Date(now()).toISOString(),
|
|
937
|
+
});
|
|
938
|
+
const refusals = [...legacy.issues];
|
|
939
|
+
if (legacy.canMaintain && legacy.needsMaintenance) {
|
|
940
|
+
const maintained = await maintainLegacyManagedWebLogArtifacts(paths.launchd);
|
|
941
|
+
refusals.push(...maintained.refusals);
|
|
942
|
+
}
|
|
943
|
+
await rotateStoppedLaunchdLogs(paths.launchd);
|
|
944
|
+
if (await inspectOwnerPrivateLaunchdPlist(paths.launchd.plistPath) !== mainIdentity) {
|
|
945
|
+
throw new Error("The main web LaunchAgent changed during stopped-window maintenance.");
|
|
946
|
+
}
|
|
947
|
+
intent = await markLaunchdLogMaintenanceRestoring(paths.launchd, intent);
|
|
948
|
+
await writeWebLogMaintenanceStatus(paths.maintenanceStatusPath, {
|
|
949
|
+
version: 1,
|
|
950
|
+
state: refusals.length === 0 ? "running" : "degraded",
|
|
951
|
+
phase: "restoring",
|
|
952
|
+
updatedAt: new Date(now()).toISOString(),
|
|
953
|
+
...(refusals.length === 0 ? {} : { refusals }),
|
|
954
|
+
});
|
|
955
|
+
return intent;
|
|
956
|
+
}
|
|
957
|
+
async function inspectInstalledWebMaintenanceHelper(paths, expectedMainIdentity, verifyEntrypoint = verifyManagedRuntimeMaintenanceEntrypoint, loadedDefinition, requireLoadedDefinition = false) {
|
|
958
|
+
const helper = await readOwnerPrivateLaunchdPlist(paths.maintenancePlistPath);
|
|
959
|
+
const strings = plistProgramArguments(helper.contents);
|
|
960
|
+
const expectedPrefix = [
|
|
961
|
+
"/usr/bin/env",
|
|
962
|
+
"-i",
|
|
963
|
+
...buildEnvironmentArguments(managedWebLogMaintenanceEnvironment()),
|
|
964
|
+
];
|
|
965
|
+
const nodeIndex = expectedPrefix.length;
|
|
966
|
+
if (strings.length !== expectedPrefix.length + 7
|
|
967
|
+
|| expectedPrefix.some((value, index) => strings[index] !== value)
|
|
968
|
+
|| strings[nodeIndex + 2] !== INTERNAL_WEB_LOG_MAINTENANCE_COMMAND
|
|
969
|
+
|| strings[nodeIndex + 3] !== "--expected-managed-runtime-launch"
|
|
970
|
+
|| strings[nodeIndex + 5] !== "--expected-web-plist-identity") {
|
|
971
|
+
throw new Error("The web-maintenance helper has an unexpected private argv definition.");
|
|
972
|
+
}
|
|
973
|
+
const nodePath = strings[nodeIndex];
|
|
974
|
+
const cliPath = strings[nodeIndex + 1];
|
|
975
|
+
const expectedManagedRuntimeLaunch = strings[nodeIndex + 4];
|
|
976
|
+
const embeddedMainIdentity = strings[nodeIndex + 6];
|
|
977
|
+
const cwd = plistStringForKey(helper.contents, "WorkingDirectory");
|
|
978
|
+
const calendarMinute = plistIntegerForKey(helper.contents, "Minute");
|
|
979
|
+
const inputs = {
|
|
980
|
+
nodePath,
|
|
981
|
+
cliPath,
|
|
982
|
+
cwd,
|
|
983
|
+
expectedManagedRuntimeLaunch,
|
|
984
|
+
environment: managedWebLogMaintenanceEnvironment(),
|
|
985
|
+
calendarMinute,
|
|
986
|
+
};
|
|
987
|
+
const rebuilt = buildWebMaintenancePlistXml({
|
|
988
|
+
label: WEB_MAINTENANCE_LAUNCHD_LABEL,
|
|
989
|
+
...inputs,
|
|
990
|
+
expectedWebPlistIdentity: embeddedMainIdentity,
|
|
991
|
+
});
|
|
992
|
+
if (rebuilt !== helper.contents)
|
|
993
|
+
throw new Error("The web-maintenance helper definition is not canonical.");
|
|
994
|
+
const mainIdentity = await inspectOwnerPrivateLaunchdPlist(paths.launchd.plistPath);
|
|
995
|
+
if (embeddedMainIdentity !== mainIdentity
|
|
996
|
+
|| (expectedMainIdentity !== undefined && expectedMainIdentity !== mainIdentity)) {
|
|
997
|
+
throw new Error("The web-maintenance helper does not carry the current composite main-plist identity.");
|
|
998
|
+
}
|
|
999
|
+
if (loadedDefinition !== undefined
|
|
1000
|
+
&& (loadedDefinition.plistPath !== paths.maintenancePlistPath
|
|
1001
|
+
|| loadedDefinition.nodePath !== nodePath
|
|
1002
|
+
|| loadedDefinition.cliPath !== cliPath
|
|
1003
|
+
|| loadedDefinition.cwd !== cwd
|
|
1004
|
+
|| loadedDefinition.expectedManagedRuntimeLaunch !== expectedManagedRuntimeLaunch
|
|
1005
|
+
|| loadedDefinition.expectedWebPlistIdentity !== embeddedMainIdentity)) {
|
|
1006
|
+
throw new Error("launchd's cached web-maintenance helper definition does not match the paired plist.");
|
|
1007
|
+
}
|
|
1008
|
+
if (loadedDefinition === undefined && requireLoadedDefinition) {
|
|
1009
|
+
throw new Error("launchd did not expose the cached web-maintenance helper definition.");
|
|
1010
|
+
}
|
|
1011
|
+
await verifyEntrypoint({
|
|
1012
|
+
currentEntrypointPath: cliPath,
|
|
1013
|
+
launchProof: expectedManagedRuntimeLaunch,
|
|
1014
|
+
});
|
|
1015
|
+
return inputs;
|
|
1016
|
+
}
|
|
1017
|
+
function plistProgramArguments(xml) {
|
|
1018
|
+
const array = /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/u.exec(xml)?.[1];
|
|
1019
|
+
if (array === undefined)
|
|
1020
|
+
throw new Error("The web-maintenance helper is missing ProgramArguments.");
|
|
1021
|
+
return [...array.matchAll(/<string>([\s\S]*?)<\/string>/gu)].map((match) => decodeXml(match[1] ?? ""));
|
|
1022
|
+
}
|
|
1023
|
+
function plistStringForKey(xml, key) {
|
|
1024
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
1025
|
+
const value = new RegExp(`<key>${escapedKey}<\\/key>\\s*<string>([\\s\\S]*?)<\\/string>`, "u").exec(xml)?.[1];
|
|
1026
|
+
if (value === undefined)
|
|
1027
|
+
throw new Error(`The web-maintenance helper is missing ${key}.`);
|
|
1028
|
+
return decodeXml(value);
|
|
1029
|
+
}
|
|
1030
|
+
function plistIntegerForKey(xml, key) {
|
|
1031
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
1032
|
+
const raw = new RegExp(`<key>${escapedKey}<\\/key>\\s*<integer>([0-9]+)<\\/integer>`, "u").exec(xml)?.[1];
|
|
1033
|
+
const value = raw === undefined ? Number.NaN : Number(raw);
|
|
1034
|
+
if (!Number.isSafeInteger(value))
|
|
1035
|
+
throw new Error(`The web-maintenance helper has an invalid ${key}.`);
|
|
1036
|
+
return value;
|
|
1037
|
+
}
|
|
1038
|
+
function decodeXml(value) {
|
|
1039
|
+
return value
|
|
1040
|
+
.replace(/</gu, "<")
|
|
1041
|
+
.replace(/>/gu, ">")
|
|
1042
|
+
.replace(/"/gu, "\"")
|
|
1043
|
+
.replace(/'/gu, "'")
|
|
1044
|
+
.replace(/&/gu, "&");
|
|
1045
|
+
}
|
|
589
1046
|
async function acquireWebLifecycleLock(paths) {
|
|
590
1047
|
await ensureOwnerPrivateLaunchdDirectory(dirname(paths.stateDir));
|
|
591
1048
|
await ensureOwnerPrivateLaunchdDirectory(paths.stateDir);
|
|
592
1049
|
return await acquireFilesystemLifecycleLock({ label: WEB_LAUNCHD_LABEL, paths: paths.launchd });
|
|
593
1050
|
}
|
|
594
|
-
async function stopLaunchdOnly(service, runner, uid, deps) {
|
|
595
|
-
const result = await bootout(runner,
|
|
1051
|
+
async function stopLaunchdOnly(label, service, runner, uid, deps) {
|
|
1052
|
+
const result = await bootout(runner, label, uid);
|
|
596
1053
|
if (result.code !== 0)
|
|
597
1054
|
return false;
|
|
598
1055
|
const sleep = deps.sleep ?? ((ms) => new Promise((resolvePromise) => setTimeout(resolvePromise, ms)));
|
|
599
1056
|
const deadline = (deps.now ?? Date.now)() + READY_TIMEOUT_MS;
|
|
600
1057
|
for (;;) {
|
|
601
|
-
const current = await launchdServiceInfo(runner,
|
|
1058
|
+
const current = await launchdServiceInfo(runner, label, uid);
|
|
602
1059
|
const pidAlive = service.pid !== undefined && (deps.isAlive ?? processIsAlive)(service.pid);
|
|
603
1060
|
if (!current.loaded && !pidAlive)
|
|
604
1061
|
return true;
|
|
@@ -609,16 +1066,27 @@ async function stopLaunchdOnly(service, runner, uid, deps) {
|
|
|
609
1066
|
}
|
|
610
1067
|
async function restorePreviousWebService(snapshot, paths, runner, uid, deps) {
|
|
611
1068
|
try {
|
|
1069
|
+
const currentHelper = await launchdServiceInfo(runner, WEB_MAINTENANCE_LAUNCHD_LABEL, uid);
|
|
1070
|
+
if (currentHelper.loaded && !await stopLaunchdOnly(WEB_MAINTENANCE_LAUNCHD_LABEL, currentHelper, runner, uid, deps)) {
|
|
1071
|
+
return { ok: false, detail: "the failed replacement helper could not be stopped" };
|
|
1072
|
+
}
|
|
612
1073
|
const current = await launchdServiceInfo(runner, WEB_LAUNCHD_LABEL, uid);
|
|
613
|
-
if (current.loaded && !await stopLaunchdOnly(current, runner, uid, deps)) {
|
|
1074
|
+
if (current.loaded && !await stopLaunchdOnly(WEB_LAUNCHD_LABEL, current, runner, uid, deps)) {
|
|
614
1075
|
return { ok: false, detail: "the failed replacement worker could not be stopped" };
|
|
615
1076
|
}
|
|
616
1077
|
const writer = deps.writePrivateFile ?? writeOwnerPrivateLaunchdFile;
|
|
1078
|
+
if (snapshot.helperInputs === undefined) {
|
|
1079
|
+
return { ok: false, detail: "the prior main definition has no structured helper inputs" };
|
|
1080
|
+
}
|
|
617
1081
|
if (snapshot.recordText === undefined)
|
|
618
1082
|
await rm(paths.recordPath, { force: true });
|
|
619
1083
|
else
|
|
620
1084
|
await writer(paths.recordPath, snapshot.recordText);
|
|
621
|
-
await
|
|
1085
|
+
await publishPairedWebPlists(paths, snapshot.plist, snapshot.helperInputs, writer);
|
|
1086
|
+
const helperBooted = await bootstrap(runner, paths.maintenancePlistPath, uid);
|
|
1087
|
+
if (helperBooted.code !== 0) {
|
|
1088
|
+
return { ok: false, detail: commandDetail(helperBooted) || `helper launchctl bootstrap exited ${String(helperBooted.code)}` };
|
|
1089
|
+
}
|
|
622
1090
|
const booted = await bootstrap(runner, paths.launchd.plistPath, uid);
|
|
623
1091
|
if (booted.code !== 0) {
|
|
624
1092
|
return { ok: false, detail: commandDetail(booted) || `launchctl bootstrap exited ${String(booted.code)}` };
|
|
@@ -648,10 +1116,16 @@ async function restoreWebPublication(paths, snapshot, writer) {
|
|
|
648
1116
|
await rm(paths.recordPath, { force: true });
|
|
649
1117
|
else
|
|
650
1118
|
await writer(paths.recordPath, snapshot.recordText);
|
|
651
|
-
if (snapshot.plist === undefined)
|
|
1119
|
+
if (snapshot.plist === undefined) {
|
|
1120
|
+
await rm(paths.maintenancePlistPath, { force: true });
|
|
652
1121
|
await rm(paths.launchd.plistPath, { force: true });
|
|
653
|
-
|
|
654
|
-
|
|
1122
|
+
}
|
|
1123
|
+
else {
|
|
1124
|
+
if (snapshot.helperInputs === undefined) {
|
|
1125
|
+
throw new Error("Cannot restore a main web plist without structured helper inputs.");
|
|
1126
|
+
}
|
|
1127
|
+
await publishPairedWebPlists(paths, snapshot.plist, snapshot.helperInputs, writer);
|
|
1128
|
+
}
|
|
655
1129
|
}
|
|
656
1130
|
async function waitForWebReady(host, port, launchctl, uid, deps) {
|
|
657
1131
|
const now = deps.now ?? Date.now;
|