@agfpd/iapeer 0.2.79 → 0.2.80
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/package.json +1 -1
- package/src/cli/index.ts +10 -2
- package/src/daemon/main.ts +38 -2
- package/src/launch/launchd.ts +27 -0
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -61,7 +61,7 @@ import {
|
|
|
61
61
|
validateProfileStandard,
|
|
62
62
|
} from '../identity/profileStandard.ts'
|
|
63
63
|
import { runAlwaysOn } from '../launch/launchdRun.ts'
|
|
64
|
-
import { installDaemonPlist, startConfiguredDaemon } from '../daemon/main.ts'
|
|
64
|
+
import { ensureDaemonStarted, installDaemonPlist, startConfiguredDaemon } from '../daemon/main.ts'
|
|
65
65
|
import { MARKETPLACE_NAME, onboardHost } from '../onboard/index.ts'
|
|
66
66
|
import { readMemoryProvider } from '../status/index.ts'
|
|
67
67
|
import { appendLifecycleEvent } from '../lifecycle/eventlog.ts'
|
|
@@ -969,6 +969,14 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
969
969
|
// reports the would-be actions without touching anything. --infra <csv> ALSO
|
|
970
970
|
// onboards infra runtimes (§6): npx-install each package (auto-resolved) + deploy
|
|
971
971
|
// its declared set. notifier → timer+watcher auto; telegram → operator-add after.
|
|
972
|
+
// STEP 0 (non-disableable): ensure the router daemon is up. Onboard is the
|
|
973
|
+
// explicit "set up the host" action, so starting the always-on daemon belongs
|
|
974
|
+
// here — the operator never types raw `launchctl bootstrap`. Idempotent (no-op
|
|
975
|
+
// if already loaded). A failure here is a real, host-breaking problem → exit 1.
|
|
976
|
+
const dstart = await ensureDaemonStarted({ dryRun: flags['dry-run'] === true, env })
|
|
977
|
+
const dLabel = dstart.state === 'would-start' ? 'would-start (dry-run)' : dstart.state
|
|
978
|
+
out(`daemon: ${dLabel}${dstart.healthy === false ? ' — UNHEALTHY' : ''}${dstart.detail ? ` — ${dstart.detail}` : ''}\n`)
|
|
979
|
+
const daemonFailed = dstart.state === 'failed' || dstart.healthy === false
|
|
972
980
|
const r = onboardHost({ dryRun: flags['dry-run'] === true, env })
|
|
973
981
|
for (const m of r.marketplaces) {
|
|
974
982
|
out(`marketplace ${MARKETPLACE_NAME} @ ${m.runtime}: ${m.state}${m.detail ? ` — ${m.detail}` : ''}\n`)
|
|
@@ -1038,7 +1046,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1038
1046
|
})
|
|
1039
1047
|
const memLabel = mem.provider ? `${mem.provider.provider} ${mem.provider.version}` : 'none'
|
|
1040
1048
|
out(`memory: ${mem.state}${mem.detail ? ` — ${mem.detail}` : ''} (slot: ${memLabel})\n`)
|
|
1041
|
-
return r.marketplaces.some(m => m.state === 'failed') || infraFailed ? 1 : 0
|
|
1049
|
+
return daemonFailed || r.marketplaces.some(m => m.state === 'failed') || infraFailed ? 1 : 0
|
|
1042
1050
|
}
|
|
1043
1051
|
case 'native-memory': {
|
|
1044
1052
|
// The canonized runtime-memory lever (контракт «Слот памяти» §Native-память):
|
package/src/daemon/main.ts
CHANGED
|
@@ -23,11 +23,13 @@ import { DAEMON_PLIST_LABEL } from '../core/constants.ts'
|
|
|
23
23
|
import { IapError } from '../core/errors.ts'
|
|
24
24
|
import { pluginLogsDir } from '../storage/index.ts'
|
|
25
25
|
import {
|
|
26
|
+
bootstrapDaemon,
|
|
26
27
|
isFoundationOwnedPlist,
|
|
27
28
|
launchAgentsDir,
|
|
28
29
|
renderLaunchdPlist,
|
|
29
30
|
} from '../launch/launchd.ts'
|
|
30
|
-
import type { LaunchdPlistSpec } from '../launch/launchd.ts'
|
|
31
|
+
import type { BootstrapState, LaunchdPlistSpec } from '../launch/launchd.ts'
|
|
32
|
+
import { waitForDaemonHealthy } from '../update/index.ts'
|
|
31
33
|
import {
|
|
32
34
|
drainAllEphemeralQueues,
|
|
33
35
|
drainEphemeralQueue,
|
|
@@ -450,6 +452,40 @@ export function installDaemonPlist(opts: InstallDaemonPlistOptions = {}): Instal
|
|
|
450
452
|
return { path, changed: true }
|
|
451
453
|
}
|
|
452
454
|
|
|
455
|
+
export interface DaemonStartResult {
|
|
456
|
+
/** 'would-start' = dryRun report; otherwise the bootstrap outcome. */
|
|
457
|
+
state: BootstrapState | 'would-start'
|
|
458
|
+
/** Socket health after a (non-sandbox) bootstrap; undefined when not probed. */
|
|
459
|
+
healthy?: boolean
|
|
460
|
+
detail?: string
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* `iapeer onboard`'s first, non-disableable step: ensure the foundation daemon is up.
|
|
465
|
+
* Writes the plist if missing (idempotent write-if-changed — defensive when onboard
|
|
466
|
+
* runs on a host where the plist is absent), idempotently bootstraps it (no-op when
|
|
467
|
+
* already loaded — NOT a restart), then health-checks the socket. dryRun reports the
|
|
468
|
+
* intent without touching launchctl. Sandbox-safe: bootstrapDaemon + waitForDaemonHealthy
|
|
469
|
+
* both short-circuit under IAPEER_TEST_SANDBOX, so this never loads a real job in tests.
|
|
470
|
+
*/
|
|
471
|
+
export async function ensureDaemonStarted(
|
|
472
|
+
opts: { env?: NodeJS.ProcessEnv; dryRun?: boolean } = {},
|
|
473
|
+
): Promise<DaemonStartResult> {
|
|
474
|
+
const env = opts.env ?? process.env
|
|
475
|
+
if (opts.dryRun) {
|
|
476
|
+
return {
|
|
477
|
+
state: 'would-start',
|
|
478
|
+
detail: 'install plist if missing + launchctl bootstrap com.agfpd.iapeer (idempotent) + health-check',
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
installDaemonPlist({ env })
|
|
482
|
+
const b = bootstrapDaemon(env)
|
|
483
|
+
if (b.state === 'failed') return { state: 'failed', detail: b.detail }
|
|
484
|
+
if (b.state === 'skipped-sandbox') return { state: 'skipped-sandbox', detail: b.detail }
|
|
485
|
+
const h = await waitForDaemonHealthy({ env })
|
|
486
|
+
return { state: b.state, healthy: h.healthy, detail: h.healthy ? undefined : h.detail }
|
|
487
|
+
}
|
|
488
|
+
|
|
453
489
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
454
490
|
// CLI — launchd-held process / foreground acceptance entrypoint
|
|
455
491
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -463,7 +499,7 @@ if (import.meta.main) {
|
|
|
463
499
|
const { path: p, changed } = installDaemonPlist()
|
|
464
500
|
process.stdout.write(
|
|
465
501
|
`daemon plist ${changed ? 'written' : 'unchanged'}: ${p}\n` +
|
|
466
|
-
`NOT loaded.
|
|
502
|
+
`NOT loaded. \`iapeer onboard\` starts it (idempotent); or manually: launchctl bootstrap gui/$(id -u) ${p}\n`,
|
|
467
503
|
)
|
|
468
504
|
process.exit(0)
|
|
469
505
|
} else {
|
package/src/launch/launchd.ts
CHANGED
|
@@ -382,6 +382,33 @@ export function cycleDaemon(env: NodeJS.ProcessEnv = process.env): DaemonRestart
|
|
|
382
382
|
return cycleLaunchdJob(DAEMON_PLIST_LABEL, join(launchAgentsDir(env), `${DAEMON_PLIST_LABEL}.plist`), env)
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Ensure the foundation daemon (`com.agfpd.iapeer`) is LOADED in the gui domain — an
|
|
387
|
+
* IDEMPOTENT `launchctl bootstrap` (no-op when already loaded), NOT a restart. This is
|
|
388
|
+
* the "start it now" step `iapeer onboard` runs, so an operator never types raw
|
|
389
|
+
* launchctl. Composed on bootstrapJobCore (undead-job-safe wait-for-gone + backoff).
|
|
390
|
+
* SANDBOX FAIL-SAFE: under IAPEER_TEST_SANDBOX it never calls launchctl (`bootstrap
|
|
391
|
+
* gui/<uid>` is host-global regardless of IAPEER_ROOT), consulting BOTH the passed env
|
|
392
|
+
* and the process env — mirror of launchctlBootstrap's guard.
|
|
393
|
+
*/
|
|
394
|
+
export function bootstrapDaemon(env: NodeJS.ProcessEnv = process.env): BootstrapResult {
|
|
395
|
+
if (env.IAPEER_TEST_SANDBOX === '1' || process.env.IAPEER_TEST_SANDBOX === '1') {
|
|
396
|
+
return { state: 'skipped-sandbox', label: DAEMON_PLIST_LABEL, detail: 'IAPEER_TEST_SANDBOX=1 — not loading a real launchd job' }
|
|
397
|
+
}
|
|
398
|
+
const uid = currentUid()
|
|
399
|
+
const plistPath = join(launchAgentsDir(env), `${DAEMON_PLIST_LABEL}.plist`)
|
|
400
|
+
const core = bootstrapJobCore(uid, DAEMON_PLIST_LABEL, plistPath, {
|
|
401
|
+
run: args => {
|
|
402
|
+
const r = spawnSync('launchctl', args, { encoding: 'utf8' })
|
|
403
|
+
return { status: r.status, stderr: r.stderr ?? '' }
|
|
404
|
+
},
|
|
405
|
+
sleepMs: ms => spawnSync('sleep', [String(ms / 1000)]),
|
|
406
|
+
})
|
|
407
|
+
return core.state === 'failed'
|
|
408
|
+
? { state: 'failed', label: DAEMON_PLIST_LABEL, detail: core.detail }
|
|
409
|
+
: { state: core.state, label: DAEMON_PLIST_LABEL }
|
|
410
|
+
}
|
|
411
|
+
|
|
385
412
|
/**
|
|
386
413
|
* Restart any launchd job onto the executable(s) currently referenced by its plist:
|
|
387
414
|
* bootout → bootstrap, never kickstart. Used by the daemon update path AND by
|