@agfpd/iapeer 0.2.93 → 0.2.94
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/launch/cmdlog.ts +64 -22
- package/src/launch/launchd.ts +34 -0
- package/src/lifecycle/index.ts +9 -1
package/package.json
CHANGED
package/src/launch/cmdlog.ts
CHANGED
|
@@ -38,6 +38,14 @@ import { join } from 'path'
|
|
|
38
38
|
export const CMDLOG_CAP_BYTES = 8 * 1024 * 1024
|
|
39
39
|
export const CMDLOG_KEEP_BYTES = 1024 * 1024
|
|
40
40
|
|
|
41
|
+
/** Pane-log tail-keep cap (capPaneLogs): a per-identity pane-log over CAP is cut to
|
|
42
|
+
* its last KEEP. KEEP_BYTES INVARIANT — must stay ≥ the pane-log reader's seed
|
|
43
|
+
* window (readyGateModel SEED_BYTES = 4 MiB, the tail fed into the headless xterm
|
|
44
|
+
* for composer-occupancy / viewport detection); capping below it would starve that
|
|
45
|
+
* reader of the tail it reconstructs the live screen from. 8 MiB = 2× the seed. */
|
|
46
|
+
export const PANELOG_CAP_BYTES = 32 * 1024 * 1024
|
|
47
|
+
export const PANELOG_KEEP_BYTES = 8 * 1024 * 1024
|
|
48
|
+
|
|
41
49
|
const CMDLOG_SUBDIR = 'tmux-cmdlog'
|
|
42
50
|
|
|
43
51
|
/** The per-identity command-log dir: <eventLogDir>/tmux-cmdlog/<identity>. The
|
|
@@ -78,12 +86,34 @@ export function hardenCmdLogDir(dir: string): void {
|
|
|
78
86
|
}
|
|
79
87
|
}
|
|
80
88
|
|
|
89
|
+
/** Tail-keep ONE file: if it exceeds capBytes, rewrite it as its last keepBytes.
|
|
90
|
+
* Safe against a live O_APPEND writer (tmux `cat >>`, the supervisor append-fd):
|
|
91
|
+
* the next write lands at the new EOF (proven by live truncation test). Lines
|
|
92
|
+
* appended between the tail-read and the rewrite are lost — acceptable mid-life
|
|
93
|
+
* noise. Returns true iff the file was capped. Best-effort: swallows FS errors. */
|
|
94
|
+
function tailKeepFile(path: string, capBytes: number, keepBytes: number): boolean {
|
|
95
|
+
try {
|
|
96
|
+
if (statSync(path).size <= capBytes) return false
|
|
97
|
+
const fd = openSync(path, 'r')
|
|
98
|
+
let tail: Buffer
|
|
99
|
+
try {
|
|
100
|
+
const size = fstatSync(fd).size
|
|
101
|
+
const start = Math.max(0, size - keepBytes)
|
|
102
|
+
tail = Buffer.alloc(Math.min(keepBytes, size))
|
|
103
|
+
readSync(fd, tail, 0, tail.length, start)
|
|
104
|
+
} finally {
|
|
105
|
+
closeSync(fd)
|
|
106
|
+
}
|
|
107
|
+
writeFileSync(path, tail, { mode: 0o600 })
|
|
108
|
+
return true
|
|
109
|
+
} catch {
|
|
110
|
+
return false // per-file best-effort
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
81
114
|
/** Tail-keep cap over every command log under <eventLogDir>/tmux-cmdlog/<identity>.
|
|
82
|
-
* A file over capBytes is rewritten as its last keepBytes.
|
|
83
|
-
*
|
|
84
|
-
* the new EOF (proven by live truncation test). Lines appended between the
|
|
85
|
-
* tail-read and the rewrite are lost — acceptable mid-life noise; the death
|
|
86
|
-
* tail is never here because the peer is alive while being capped. */
|
|
115
|
+
* A file over capBytes is rewritten as its last keepBytes. The death tail is never
|
|
116
|
+
* here because the peer is alive while being capped. */
|
|
87
117
|
export function capCmdLogs(
|
|
88
118
|
eventLogDir: string,
|
|
89
119
|
capBytes: number = CMDLOG_CAP_BYTES,
|
|
@@ -107,24 +137,36 @@ export function capCmdLogs(
|
|
|
107
137
|
for (const name of files) {
|
|
108
138
|
if (!name.endsWith('.log')) continue
|
|
109
139
|
const path = join(dir, name)
|
|
110
|
-
|
|
111
|
-
if (statSync(path).size <= capBytes) continue
|
|
112
|
-
const fd = openSync(path, 'r')
|
|
113
|
-
let tail: Buffer
|
|
114
|
-
try {
|
|
115
|
-
const size = fstatSync(fd).size
|
|
116
|
-
const start = Math.max(0, size - keepBytes)
|
|
117
|
-
tail = Buffer.alloc(Math.min(keepBytes, size))
|
|
118
|
-
readSync(fd, tail, 0, tail.length, start)
|
|
119
|
-
} finally {
|
|
120
|
-
closeSync(fd)
|
|
121
|
-
}
|
|
122
|
-
writeFileSync(path, tail, { mode: 0o600 })
|
|
123
|
-
capped.push(path)
|
|
124
|
-
} catch {
|
|
125
|
-
/* per-file best-effort */
|
|
126
|
-
}
|
|
140
|
+
if (tailKeepFile(path, capBytes, keepBytes)) capped.push(path)
|
|
127
141
|
}
|
|
128
142
|
}
|
|
129
143
|
return capped
|
|
130
144
|
}
|
|
145
|
+
|
|
146
|
+
/** Tail-keep cap over the per-identity PANE-LOGS (<logDir>/<identity>.log) — the RAW
|
|
147
|
+
* TUI byte stream that pipe-pane (tmux) / the supervisor append for a session's whole
|
|
148
|
+
* life. Unlike the lifecycle/delivery logs (their own size-rotation under logs/iapeer),
|
|
149
|
+
* the pane-log had NO bound and grew to hundreds of MB per warm peer on an always-on
|
|
150
|
+
* host. The supervise tick caps it here with the same tail-keep mechanism as the
|
|
151
|
+
* cmd-logs. logDir (logs/lifecycle) holds ONLY pane-logs, so only top-level *.log is
|
|
152
|
+
* touched — never the rotated logs elsewhere. KEEP_BYTES stays ≥ the reader seed
|
|
153
|
+
* window (see PANELOG_KEEP_BYTES). Best-effort by construction. */
|
|
154
|
+
export function capPaneLogs(
|
|
155
|
+
logDir: string,
|
|
156
|
+
capBytes: number = PANELOG_CAP_BYTES,
|
|
157
|
+
keepBytes: number = PANELOG_KEEP_BYTES,
|
|
158
|
+
): string[] {
|
|
159
|
+
const capped: string[] = []
|
|
160
|
+
let names: string[]
|
|
161
|
+
try {
|
|
162
|
+
names = readdirSync(logDir)
|
|
163
|
+
} catch {
|
|
164
|
+
return capped
|
|
165
|
+
}
|
|
166
|
+
for (const name of names) {
|
|
167
|
+
if (!name.endsWith('.log')) continue
|
|
168
|
+
const path = join(logDir, name)
|
|
169
|
+
if (tailKeepFile(path, capBytes, keepBytes)) capped.push(path)
|
|
170
|
+
}
|
|
171
|
+
return capped
|
|
172
|
+
}
|
package/src/launch/launchd.ts
CHANGED
|
@@ -63,6 +63,24 @@ export function isFoundationOwnedPlist(path: string): boolean {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/** Read one EnvironmentVariables value from an already-written plist. Used to
|
|
67
|
+
* PRESERVE a host-specific env var across a plist regeneration (install-runtime
|
|
68
|
+
* rewrites the plist from scratch, which would otherwise drop it). Returns
|
|
69
|
+
* undefined when the file / key is absent or unreadable. The key is a literal
|
|
70
|
+
* env-var name (no regex metachars) and the values we preserve are constrained
|
|
71
|
+
* identifiers (no XML entities), so a direct `<key>…</key><string>…</string>`
|
|
72
|
+
* match is sufficient. */
|
|
73
|
+
function readPlistEnvVar(path: string, key: string): string | undefined {
|
|
74
|
+
try {
|
|
75
|
+
const m = readFileSync(path, 'utf8').match(
|
|
76
|
+
new RegExp(`<key>${key}</key>\\s*<string>([^<]*)</string>`),
|
|
77
|
+
)
|
|
78
|
+
return m ? m[1] : undefined
|
|
79
|
+
} catch {
|
|
80
|
+
return undefined
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
66
84
|
/**
|
|
67
85
|
* Resolve an executable to an ABSOLUTE path against `env.PATH` (the RICH
|
|
68
86
|
* provisioning PATH), so the result can be baked into a launchd plist whose own
|
|
@@ -576,6 +594,22 @@ export function installAlwaysOnPlist(opts: InstallAlwaysOnPlistOptions): string
|
|
|
576
594
|
const resolved = opts.runtimeBin ?? resolveExecutable(INFRA_RUNTIME_DEFAULT_BIN[opts.runtime] ?? opts.runtime, env)
|
|
577
595
|
if (resolved) environment[binEnvVar] = resolved
|
|
578
596
|
}
|
|
597
|
+
// Host-specific notifier fallback backstop (NOTIFIER_FALLBACK_TARGET): the final
|
|
598
|
+
// human target the notifier's alarm chain falls back to when every other delivery
|
|
599
|
+
// fails. DELIBERATELY host-resolved at generation time (like NOTIFIER_RUNTIME_BIN
|
|
600
|
+
// above), NEVER a hardcoded literal — a baked peer name would ship in the public
|
|
601
|
+
// package and inject a non-existent target into every foreign install's plist (the
|
|
602
|
+
// exact foreign-install break the notifier package removed from its own default).
|
|
603
|
+
// Source: env when the operator sets it, else PRESERVE the value already baked in
|
|
604
|
+
// THIS host's existing plist so a regeneration (install-runtime rewrites the file)
|
|
605
|
+
// never drops it. Unset on a fresh / foreign host → not baked → the notifier
|
|
606
|
+
// package's own default applies. Notifier-only (the var is notifier-scoped).
|
|
607
|
+
if (opts.runtime === 'notifier') {
|
|
608
|
+
const fallback =
|
|
609
|
+
env.NOTIFIER_FALLBACK_TARGET?.trim() ||
|
|
610
|
+
(existsSync(path) ? readPlistEnvVar(path, 'NOTIFIER_FALLBACK_TARGET') : undefined)
|
|
611
|
+
if (fallback) environment.NOTIFIER_FALLBACK_TARGET = fallback
|
|
612
|
+
}
|
|
579
613
|
const spec: LaunchdPlistSpec = {
|
|
580
614
|
label: launchdLabel(opts.personality),
|
|
581
615
|
programArguments: [...(opts.entrypointArgv ?? defaultEntrypointArgv(env)), opts.personality, opts.runtime],
|
package/src/lifecycle/index.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
} from '../core/constants.ts'
|
|
27
27
|
import { buildProcessAddress, buildSocketPath, parseSessionName } from '../core/socket.ts'
|
|
28
28
|
import { err, ok, type Result } from '../core/errors.ts'
|
|
29
|
-
import { capCmdLogs, cmdLogDirFor } from '../launch/cmdlog.ts'
|
|
29
|
+
import { capCmdLogs, capPaneLogs, cmdLogDirFor } from '../launch/cmdlog.ts'
|
|
30
30
|
import { resolveGlobalRoot } from '../storage/index.ts'
|
|
31
31
|
import { readPeerProfile, resolveIdentity } from '../identity/index.ts'
|
|
32
32
|
import {
|
|
@@ -1217,6 +1217,14 @@ export function superviseTick(cfg: LifecycleConfig, deps: SuperviseDeps = {}): S
|
|
|
1217
1217
|
// over the cap. Best-effort by construction; the killer's command is the LAST
|
|
1218
1218
|
// line of a dying log, so capping a LIVE peer's log never costs the death tail.
|
|
1219
1219
|
capCmdLogs(cfg.eventLogDir)
|
|
1220
|
+
// Pane-log volume cap (launch/cmdlog.ts capPaneLogs): the per-identity pane-log
|
|
1221
|
+
// (<logDir>/<identity>.log) is the RAW TUI byte stream pipe-pane/the supervisor
|
|
1222
|
+
// append for a session's whole life — it had NO bound and grew to hundreds of MB
|
|
1223
|
+
// per warm peer on an always-on host. Tail-keep it each tick (KEEP ≥ the occupancy
|
|
1224
|
+
// reader's 4 MiB seed window, so capping never starves composer detection).
|
|
1225
|
+
// Not H4-gated: log janitoring is orthogonal to lifecycle ownership, so launchd-
|
|
1226
|
+
// managed peers' pane-logs are capped too. Best-effort by construction.
|
|
1227
|
+
capPaneLogs(cfg.logDir)
|
|
1220
1228
|
// Durable decision trace (eventlog.ts): every reap/death/eager-fresh gets a line
|
|
1221
1229
|
// so a postmortem can answer "when & how did peer X's prior session end" even
|
|
1222
1230
|
// after the .idle-reaped / .deaths markers are consumed. alive / skipped-launchd
|