wowdump 0.2.1 → 0.3.1
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/LICENSE +21 -21
- package/README.md +56 -118
- package/dist/agent.js +5 -8
- package/dist/cli.js +497 -0
- package/dist/discovery.js +1 -12
- package/dist/dry-run.js +0 -2
- package/dist/focused-session.js +61 -1329
- package/dist/frida-runtime.js +51 -73
- package/dist/frida-worker.js +100 -0
- package/dist/ghidra.js +769 -0
- package/dist/main.js +66 -0
- package/dist/processes.js +2 -5
- package/dist/reader-broker.js +460 -0
- package/dist/reader-client.js +1 -0
- package/dist/reader-main.js +67 -0
- package/dist/session.js +17 -120
- package/dist/toolchain.js +594 -0
- package/dist/windows-launcher.js +207 -0
- package/dist/windows-reader.js +102 -0
- package/dist/wow-analysis.js +211 -236
- package/package.json +18 -35
- package/skills/wowdump/SKILL.md +15 -0
- package/skills/wowdump/commands.md +44 -0
- package/dist/analysis-path.js +0 -38
- package/dist/analysis-process-log.js +0 -146
- package/dist/broker-client.js +0 -411
- package/dist/broker-codec.js +0 -148
- package/dist/broker-core.js +0 -1045
- package/dist/broker-gateway.js +0 -447
- package/dist/broker-ledger.js +0 -196
- package/dist/broker-main.js +0 -291
- package/dist/broker-protocol.js +0 -119
- package/dist/broker-runtime.js +0 -1283
- package/dist/broker-server.js +0 -466
- package/dist/build-bundle-loader.js +0 -183
- package/dist/build-bundle.js +0 -11
- package/dist/focus-errors.js +0 -63
- package/dist/focus-service.js +0 -1855
- package/dist/mcp-main.js +0 -51
- package/dist/mcp.js +0 -924
- package/dist/process-log-lock.js +0 -195
- package/dist/runtime-config.js +0 -399
- package/resources/builds/retail/12.0.7.68974/build-profile.json +0 -290
- package/resources/builds/retail/12.0.7.68974/data-sources.json +0 -1633
- package/resources/builds/retail/12.0.7.68974/lua-targets.jsonl +0 -5130
- package/resources/builds/retail/12.0.7.68974/manifest.json +0 -63
- package/resources/builds/retail/12.0.7.68974/signatures.json +0 -260
package/dist/focused-session.js
CHANGED
|
@@ -1,1357 +1,89 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
export const TargetKindSchema = z.enum([
|
|
4
|
-
|
|
5
|
-
"cpp_function",
|
|
6
|
-
"data_source",
|
|
7
|
-
"object",
|
|
8
|
-
"address_range",
|
|
9
|
-
]);
|
|
10
|
-
const HexSchema = z.string().regex(/^0x[0-9a-fA-F]+$/);
|
|
11
|
-
const SelectorRangeSchema = z
|
|
12
|
-
.object({ rva: HexSchema, size: z.number().int().positive().max(0x7fffffff) })
|
|
13
|
-
.strict();
|
|
14
|
-
export const FocusSelectorSchema = z
|
|
15
|
-
.object({
|
|
3
|
+
export const TargetKindSchema = z.enum(["lua_wrapper", "cpp_function", "data_source", "object", "address_range"]);
|
|
4
|
+
export const FocusSelectorSchema = z.object({
|
|
16
5
|
schema: z.literal("selector-schema.v1").default("selector-schema.v1"),
|
|
17
6
|
kind: TargetKindSchema,
|
|
18
|
-
|
|
19
|
-
apis: z.array(z.string().min(1)).max(256).optional(),
|
|
20
|
-
namespaces: z.array(z.string().min(1)).max(256).optional(),
|
|
21
|
-
globs: z.array(z.string().min(1)).max(256).optional(),
|
|
22
|
-
rvas: z.array(HexSchema).max(256).optional(),
|
|
23
|
-
dataSourceIds: z.array(z.string().min(1)).max(256).optional(),
|
|
24
|
-
objectIds: z.array(z.string().min(1)).max(256).optional(),
|
|
25
|
-
ranges: z.array(SelectorRangeSchema).max(256).optional(),
|
|
26
|
-
maxTargets: z.number().int().min(1).max(256).default(64),
|
|
27
|
-
// Compatibility inputs are normalized into the plural v1 fields.
|
|
7
|
+
apis: z.array(z.string().min(1)).optional(),
|
|
28
8
|
namespace: z.string().min(1).optional(),
|
|
29
9
|
glob: z.string().min(1).optional(),
|
|
30
|
-
rva:
|
|
31
|
-
address:
|
|
32
|
-
start:
|
|
33
|
-
end:
|
|
34
|
-
})
|
|
35
|
-
.
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"rvas",
|
|
42
|
-
"dataSourceIds",
|
|
43
|
-
"objectIds",
|
|
44
|
-
"ranges",
|
|
45
|
-
"maxTargets",
|
|
46
|
-
]);
|
|
47
|
-
export function validateSelector(input, targetKind) {
|
|
48
|
-
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
49
|
-
throw new Error("selector must be an object");
|
|
50
|
-
if (targetKind)
|
|
51
|
-
for (const key of Object.keys(input))
|
|
52
|
-
if (!VERBATIM_KEYS.has(key))
|
|
53
|
-
throw new Error(`selector field ${key} is not allowed`);
|
|
54
|
-
const parsed = FocusSelectorSchema.parse(targetKind ? { ...input, kind: targetKind } : input);
|
|
55
|
-
const apis = utf8Sorted(parsed.apis ?? []);
|
|
56
|
-
const namespaces = utf8Sorted([
|
|
57
|
-
...(parsed.namespaces ?? []),
|
|
58
|
-
...(parsed.namespace ? [parsed.namespace] : []),
|
|
59
|
-
]);
|
|
60
|
-
const globs = utf8Sorted([
|
|
61
|
-
...(parsed.globs ?? []),
|
|
62
|
-
...(parsed.glob ? [parsed.glob] : []),
|
|
63
|
-
]);
|
|
64
|
-
for (const glob of globs)
|
|
65
|
-
validateGlob(glob);
|
|
66
|
-
const rvas = hexSorted([
|
|
67
|
-
...(parsed.rvas ?? []),
|
|
68
|
-
...(parsed.rva ? [parsed.rva] : []),
|
|
69
|
-
]);
|
|
70
|
-
const dataSourceIds = utf8Sorted(parsed.dataSourceIds ?? []);
|
|
71
|
-
const objectIds = utf8Sorted(parsed.objectIds ?? []);
|
|
72
|
-
const ranges = normalizeRanges(parsed.ranges ?? [], parsed.start, parsed.end);
|
|
73
|
-
const populated = {
|
|
74
|
-
apis,
|
|
75
|
-
namespaces,
|
|
76
|
-
globs,
|
|
77
|
-
rvas,
|
|
78
|
-
dataSourceIds,
|
|
79
|
-
objectIds,
|
|
80
|
-
ranges,
|
|
81
|
-
};
|
|
82
|
-
const allowed = {
|
|
83
|
-
lua_wrapper: new Set(["apis", "namespaces", "globs", "rvas"]),
|
|
84
|
-
cpp_function: new Set(["rvas"]),
|
|
85
|
-
data_source: new Set(["dataSourceIds"]),
|
|
86
|
-
object: new Set(["objectIds"]),
|
|
87
|
-
address_range: new Set(["ranges"]),
|
|
88
|
-
};
|
|
89
|
-
for (const [field, values] of Object.entries(populated)) {
|
|
90
|
-
if (values.length > 0 && !allowed[parsed.kind].has(field))
|
|
91
|
-
throw new Error(`${field} is incompatible with ${parsed.kind}`);
|
|
92
|
-
}
|
|
93
|
-
if (parsed.kind === "cpp_function" && rvas.length === 0)
|
|
94
|
-
throw new Error("cpp_function requires rvas");
|
|
95
|
-
if (parsed.kind === "data_source" && dataSourceIds.length === 0)
|
|
96
|
-
throw new Error("data_source requires dataSourceIds");
|
|
97
|
-
if (parsed.kind === "object" && objectIds.length === 0 && !parsed.address)
|
|
98
|
-
throw new Error("object requires objectIds");
|
|
99
|
-
if (parsed.kind === "address_range" && ranges.length === 0)
|
|
100
|
-
throw new Error("address_range requires ranges");
|
|
101
|
-
if (parsed.kind === "lua_wrapper" &&
|
|
102
|
-
apis.length + namespaces.length + globs.length + rvas.length === 0)
|
|
103
|
-
throw new Error("selector must identify a target");
|
|
104
|
-
const resolvedCount = apis.length +
|
|
105
|
-
namespaces.length +
|
|
106
|
-
globs.length +
|
|
107
|
-
rvas.length +
|
|
108
|
-
dataSourceIds.length +
|
|
109
|
-
objectIds.length +
|
|
110
|
-
ranges.length;
|
|
111
|
-
if (resolvedCount > parsed.maxTargets)
|
|
112
|
-
throw new Error(`selector resolves to ${resolvedCount} inputs, exceeding maxTargets=${parsed.maxTargets}`);
|
|
113
|
-
return {
|
|
114
|
-
schema: "selector-schema.v1",
|
|
115
|
-
kind: parsed.kind,
|
|
116
|
-
combine: parsed.combine,
|
|
117
|
-
...(apis.length ? { apis } : {}),
|
|
118
|
-
...(namespaces.length ? { namespaces } : {}),
|
|
119
|
-
...(globs.length ? { globs } : {}),
|
|
120
|
-
...(rvas.length ? { rvas, rva: rvas[0] } : {}),
|
|
121
|
-
...(dataSourceIds.length ? { dataSourceIds } : {}),
|
|
122
|
-
...(objectIds.length ? { objectIds } : {}),
|
|
123
|
-
...(ranges.length
|
|
124
|
-
? {
|
|
125
|
-
ranges,
|
|
126
|
-
start: ranges[0].rva,
|
|
127
|
-
end: canonicalHex(BigInt(ranges[0].rva) + BigInt(ranges[0].size)),
|
|
128
|
-
}
|
|
129
|
-
: {}),
|
|
130
|
-
...(parsed.address ? { address: canonicalHex(parsed.address) } : {}),
|
|
131
|
-
maxTargets: parsed.maxTargets,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
10
|
+
rva: z.string().regex(/^0x[0-9a-fA-F]+$/).optional(),
|
|
11
|
+
address: z.string().regex(/^0x[0-9a-fA-F]+$/).optional(),
|
|
12
|
+
start: z.string().regex(/^0x[0-9a-fA-F]+$/).optional(),
|
|
13
|
+
end: z.string().regex(/^0x[0-9a-fA-F]+$/).optional()
|
|
14
|
+
}).superRefine((v, ctx) => {
|
|
15
|
+
if (v.kind === "address_range" && (!v.start || !v.end))
|
|
16
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "address_range requires start and end" });
|
|
17
|
+
if (v.kind !== "address_range" && v.kind !== "object" && !v.apis?.length && !v.namespace && !v.glob && !v.rva && !v.address)
|
|
18
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "selector must identify a target" });
|
|
19
|
+
});
|
|
20
|
+
export const validateSelector = (input) => FocusSelectorSchema.parse(input);
|
|
134
21
|
/** Canonical byte representation used for selector identity and replay checks. */
|
|
135
22
|
export const canonicalizeSelector = (input) => {
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const value = validateSelector(selector, explicitKind);
|
|
142
|
-
return JSON.stringify({
|
|
143
|
-
schemaVersion: value.schema,
|
|
144
|
-
targetKind: value.kind,
|
|
145
|
-
combine: value.combine,
|
|
146
|
-
apis: value.apis ?? [],
|
|
147
|
-
namespaces: value.namespaces ?? [],
|
|
148
|
-
globs: value.globs ?? [],
|
|
149
|
-
rvas: value.rvas ?? [],
|
|
150
|
-
dataSourceIds: value.dataSourceIds ?? [],
|
|
151
|
-
objectIds: value.objectIds ?? [],
|
|
152
|
-
ranges: value.ranges ?? [],
|
|
153
|
-
maxTargets: value.maxTargets,
|
|
154
|
-
});
|
|
155
|
-
};
|
|
156
|
-
function canonicalHex(value) {
|
|
157
|
-
return `0x${BigInt(value).toString(16).toUpperCase()}`;
|
|
158
|
-
}
|
|
159
|
-
function utf8Sorted(values) {
|
|
160
|
-
return [...new Set(values)].sort((left, right) => Buffer.compare(Buffer.from(left, "utf8"), Buffer.from(right, "utf8")));
|
|
161
|
-
}
|
|
162
|
-
function hexSorted(values) {
|
|
163
|
-
return [...new Set(values.map(canonicalHex))].sort((left, right) => BigInt(left) < BigInt(right) ? -1 : BigInt(left) > BigInt(right) ? 1 : 0);
|
|
164
|
-
}
|
|
165
|
-
function normalizeRanges(values, start, end) {
|
|
166
|
-
const ranges = values.map((value) => ({
|
|
167
|
-
rva: canonicalHex(value.rva),
|
|
168
|
-
size: value.size,
|
|
169
|
-
}));
|
|
170
|
-
if (start || end) {
|
|
171
|
-
if (!start || !end || BigInt(end) <= BigInt(start))
|
|
172
|
-
throw new Error("address_range requires start < end");
|
|
173
|
-
const size = Number(BigInt(end) - BigInt(start));
|
|
174
|
-
if (!Number.isSafeInteger(size) || size <= 0)
|
|
175
|
-
throw new Error("address range size is invalid");
|
|
176
|
-
ranges.push({ rva: canonicalHex(start), size });
|
|
177
|
-
}
|
|
178
|
-
const unique = new Map(ranges.map((range) => [`${range.rva}:${range.size}`, range]));
|
|
179
|
-
return [...unique.values()].sort((left, right) => BigInt(left.rva) < BigInt(right.rva)
|
|
180
|
-
? -1
|
|
181
|
-
: BigInt(left.rva) > BigInt(right.rva)
|
|
182
|
-
? 1
|
|
183
|
-
: left.size - right.size);
|
|
184
|
-
}
|
|
185
|
-
function validateGlob(value) {
|
|
186
|
-
let escaped = false;
|
|
187
|
-
for (const character of value) {
|
|
188
|
-
if (escaped) {
|
|
189
|
-
escaped = false;
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
if (character === "\\") {
|
|
193
|
-
escaped = true;
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
23
|
+
const value = validateSelector(input);
|
|
24
|
+
const ordered = {};
|
|
25
|
+
for (const key of ["schema", "kind", "apis", "namespace", "glob", "rva", "address", "start", "end"]) {
|
|
26
|
+
if (value[key] !== undefined)
|
|
27
|
+
ordered[key] = Array.isArray(value[key]) ? [...value[key]].sort() : value[key];
|
|
196
28
|
}
|
|
197
|
-
|
|
198
|
-
throw new Error("glob cannot end with an isolated escape");
|
|
199
|
-
}
|
|
200
|
-
const JsonValueSchema = z.lazy(() => z.union([
|
|
201
|
-
z.null(),
|
|
202
|
-
z.boolean(),
|
|
203
|
-
z.number().finite(),
|
|
204
|
-
z.string(),
|
|
205
|
-
z.array(JsonValueSchema),
|
|
206
|
-
z.record(JsonValueSchema),
|
|
207
|
-
]));
|
|
208
|
-
const TriggerActionSchema = z.union([
|
|
209
|
-
z.string().min(1),
|
|
210
|
-
z
|
|
211
|
-
.object({
|
|
212
|
-
userAction: z.string().min(1).optional(),
|
|
213
|
-
expectedTransition: z
|
|
214
|
-
.union([z.string().min(1), JsonValueSchema])
|
|
215
|
-
.optional(),
|
|
216
|
-
description: z.string().min(1).optional(),
|
|
217
|
-
})
|
|
218
|
-
.strict(),
|
|
219
|
-
]);
|
|
220
|
-
const TriggerPhaseSchema = z
|
|
221
|
-
.object({
|
|
222
|
-
phase: z.enum(["before", "during", "after"]),
|
|
223
|
-
phaseIndex: z.number().int().nonnegative(),
|
|
224
|
-
actions: z.array(TriggerActionSchema).default([]),
|
|
225
|
-
userAction: z.string().min(1).optional(),
|
|
226
|
-
expectedTransition: z
|
|
227
|
-
.union([z.string().min(1), JsonValueSchema])
|
|
228
|
-
.optional(),
|
|
229
|
-
})
|
|
230
|
-
.strict();
|
|
231
|
-
const RawTriggerPlanSchema = z
|
|
232
|
-
.object({
|
|
233
|
-
planVersion: z.string().min(1).default("trigger-plan.v1"),
|
|
234
|
-
workloadId: z.string().min(1),
|
|
235
|
-
description: z.string().optional(),
|
|
236
|
-
phases: z.array(TriggerPhaseSchema).optional(),
|
|
237
|
-
before: z.array(TriggerActionSchema).default([]),
|
|
238
|
-
during: z.array(TriggerActionSchema).default([]),
|
|
239
|
-
after: z.array(TriggerActionSchema).default([]),
|
|
240
|
-
expectedChanges: z.array(z.string()).default([]),
|
|
241
|
-
})
|
|
242
|
-
.strict();
|
|
243
|
-
export const TriggerPlanSchema = RawTriggerPlanSchema.transform((value) => ({
|
|
244
|
-
...value,
|
|
245
|
-
phases: value.phases ?? [
|
|
246
|
-
...value.before.map((_, phaseIndex) => ({
|
|
247
|
-
phase: "before",
|
|
248
|
-
phaseIndex,
|
|
249
|
-
actions: [value.before[phaseIndex]],
|
|
250
|
-
})),
|
|
251
|
-
...value.during.map((_, phaseIndex) => ({
|
|
252
|
-
phase: "during",
|
|
253
|
-
phaseIndex,
|
|
254
|
-
actions: [value.during[phaseIndex]],
|
|
255
|
-
})),
|
|
256
|
-
...value.after.map((_, phaseIndex) => ({
|
|
257
|
-
phase: "after",
|
|
258
|
-
phaseIndex,
|
|
259
|
-
actions: [value.after[phaseIndex]],
|
|
260
|
-
})),
|
|
261
|
-
],
|
|
262
|
-
}));
|
|
263
|
-
export function canonicalizeEnvironment(input) {
|
|
264
|
-
const parsedInput = z.record(JsonValueSchema).parse(input);
|
|
265
|
-
const parsed = {
|
|
266
|
-
descriptorVersion: "environment.v1",
|
|
267
|
-
capturedAt: "1970-01-01T00:00:00.000Z",
|
|
268
|
-
...parsedInput,
|
|
269
|
-
};
|
|
270
|
-
const canonical = stableJson(parsed);
|
|
271
|
-
return {
|
|
272
|
-
value: parsed,
|
|
273
|
-
canonical,
|
|
274
|
-
sha256: createHash("sha256").update(canonical).digest("hex"),
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
function stableJson(value) {
|
|
278
|
-
if (Array.isArray(value))
|
|
279
|
-
return `[${value.map(stableJson).join(",")}]`;
|
|
280
|
-
if (value !== null && typeof value === "object")
|
|
281
|
-
return `{${Object.keys(value)
|
|
282
|
-
.sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)))
|
|
283
|
-
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
|
|
284
|
-
.join(",")}}`;
|
|
285
|
-
return JSON.stringify(value);
|
|
286
|
-
}
|
|
287
|
-
const systemClock = {
|
|
288
|
-
now: () => Date.now(),
|
|
289
|
-
monotonicNow: () => performance.now(),
|
|
29
|
+
return JSON.stringify(ordered);
|
|
290
30
|
};
|
|
31
|
+
const systemClock = { now: () => Date.now() };
|
|
291
32
|
export class FocusedSession {
|
|
292
33
|
state = "created";
|
|
293
34
|
seq = 0;
|
|
294
|
-
ingressOrdinal = 0;
|
|
295
35
|
bytes = 0;
|
|
296
36
|
hooks = [];
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
phase = "before";
|
|
300
|
-
droppedEvents = 0;
|
|
301
|
-
droppedBytes = 0;
|
|
302
|
-
droppedReasons = {};
|
|
303
|
-
dropWindows = {};
|
|
304
|
-
externalDroppedEvents = 0;
|
|
305
|
-
externalDroppedBytes = 0;
|
|
306
|
-
externalDroppedReasons = {};
|
|
37
|
+
depth = new Map();
|
|
38
|
+
dropped = 0;
|
|
307
39
|
startedAt = 0;
|
|
308
40
|
stoppedAt;
|
|
309
|
-
pausedAt;
|
|
310
|
-
pauseGaps = [];
|
|
311
41
|
cleanupStatus = { hooks: 0, scripts: 0, interceptors: 0 };
|
|
312
|
-
cleanupPromise;
|
|
313
|
-
cleanupCoreComplete = false;
|
|
314
|
-
cleanupCoreFailed = false;
|
|
315
|
-
cleanupTerminalComplete = false;
|
|
316
|
-
cleanupDetachRetryable = false;
|
|
317
|
-
cleanupIntendedTerminal;
|
|
318
|
-
cleanupStreamPrefixes = [];
|
|
319
|
-
cleanupFragment;
|
|
320
|
-
terminalStatus;
|
|
321
|
-
cleanupSteps = [];
|
|
322
|
-
environmentInfo;
|
|
323
|
-
plan;
|
|
324
|
-
failureClass;
|
|
325
|
-
errorCode;
|
|
326
|
-
fatalNotified = new Set();
|
|
327
|
-
selector;
|
|
328
|
-
verbatimSelector;
|
|
329
|
-
receiveOpen = true;
|
|
330
|
-
frozen = false;
|
|
331
|
-
draining = false;
|
|
332
|
-
appendTail = Promise.resolve();
|
|
333
|
-
appendInFlight = 0;
|
|
334
|
-
appendControllers = new Set();
|
|
335
|
-
appendTransactionsAborted = false;
|
|
336
|
-
cleanupDetachedOperations = [];
|
|
337
|
-
cleanupLateCompletions = 0;
|
|
338
42
|
options;
|
|
339
|
-
constructor(options) {
|
|
340
|
-
this.
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
this.environmentInfo = canonicalizeEnvironment(options.environment);
|
|
355
|
-
this.plan = TriggerPlanSchema.parse(options.triggerPlan);
|
|
356
|
-
this.options = options;
|
|
357
|
-
}
|
|
358
|
-
get status() {
|
|
359
|
-
return this.state;
|
|
360
|
-
}
|
|
361
|
-
get resolvedSelector() {
|
|
362
|
-
return this.selector;
|
|
363
|
-
}
|
|
364
|
-
async validateIdentity() {
|
|
365
|
-
await this.revalidate();
|
|
366
|
-
}
|
|
367
|
-
get manifest() {
|
|
368
|
-
return {
|
|
369
|
-
schema: "wow.focus-session.v1",
|
|
370
|
-
sessionId: this.options.sessionId,
|
|
371
|
-
pid: this.options.pid,
|
|
372
|
-
buildKey: this.options.buildKey,
|
|
373
|
-
moduleBase: this.options.moduleBase,
|
|
374
|
-
imageBase: this.options.imageBase,
|
|
375
|
-
processStartTime: this.options.processStartTime ?? null,
|
|
376
|
-
moduleIdentity: this.options.moduleIdentity ?? null,
|
|
377
|
-
targetKind: this.selector.kind,
|
|
378
|
-
selector: this.verbatimSelector,
|
|
379
|
-
selectorVerbatim: this.verbatimSelector,
|
|
380
|
-
selectorCanonical: canonicalizeSelector({
|
|
381
|
-
...this.verbatimSelector,
|
|
382
|
-
targetKind: this.selector.kind,
|
|
383
|
-
}),
|
|
384
|
-
resolvedTargets: selectorTargets(this.selector),
|
|
385
|
-
environment: this.environmentInfo.value,
|
|
386
|
-
environmentSha256: this.environmentInfo.sha256,
|
|
387
|
-
triggerPlan: this.plan,
|
|
388
|
-
triggerPhase: this.phase,
|
|
389
|
-
effectiveLimits: {
|
|
390
|
-
snapshotIntervalMs: this.options.snapshotIntervalMs ?? 1000,
|
|
391
|
-
maxEvents: this.options.maxEvents ?? 10_000,
|
|
392
|
-
maxBytes: this.options.maxBytes ?? 8 * 1024 * 1024,
|
|
393
|
-
durationMs: this.options.durationMs ?? 0,
|
|
394
|
-
sampling: this.options.sampling ?? 1,
|
|
395
|
-
},
|
|
396
|
-
startTime: this.startedAt ? new Date(this.startedAt).toISOString() : null,
|
|
397
|
-
stopTime: this.stoppedAt ? new Date(this.stoppedAt).toISOString() : null,
|
|
398
|
-
hookIds: this.hooks,
|
|
399
|
-
eventCount: this.seq,
|
|
400
|
-
ingressCount: this.ingressOrdinal,
|
|
401
|
-
eventBytes: this.bytes,
|
|
402
|
-
droppedEventCount: this.droppedEvents,
|
|
403
|
-
droppedByteCount: this.droppedBytes,
|
|
404
|
-
droppedByReason: this.droppedReasons,
|
|
405
|
-
dropWindows: this.dropWindows,
|
|
406
|
-
pauseGaps: this.pauseGaps,
|
|
407
|
-
cleanupStatus: this.cleanupStatus,
|
|
408
|
-
cleanupSteps: this.cleanupSteps,
|
|
409
|
-
cleanupDetachedOperations: this.cleanupDetachedOperations,
|
|
410
|
-
cleanupLateCompletions: this.cleanupLateCompletions,
|
|
411
|
-
frozen: this.frozen,
|
|
412
|
-
failureClass: this.failureClass ?? null,
|
|
413
|
-
errorCode: this.errorCode ?? null,
|
|
414
|
-
state: this.state,
|
|
415
|
-
...(this.terminalStatus ? { terminalStatus: this.terminalStatus } : {}),
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
async start() {
|
|
419
|
-
if (this.state !== "created")
|
|
420
|
-
throw new Error("session already started");
|
|
421
|
-
this.state = "attaching";
|
|
422
|
-
let captureStage = "attachment";
|
|
423
|
-
try {
|
|
424
|
-
await this.revalidate();
|
|
425
|
-
await this.options.transport.attach?.(this.identity());
|
|
426
|
-
captureStage = "capture";
|
|
427
|
-
this.hooks = await this.options.transport.installHooks(this.selector, {
|
|
428
|
-
captureArgs: this.options.captureArgs === true,
|
|
429
|
-
captureReturns: this.options.captureReturns === true,
|
|
430
|
-
captureCallStack: this.options.captureCallStack === true,
|
|
431
|
-
captureMemoryWrites: this.options.captureMemoryWrites === true,
|
|
432
|
-
captureObjectDiff: this.options.captureObjectDiff === true,
|
|
433
|
-
snapshotIntervalMs: this.options.snapshotIntervalMs ?? 1000,
|
|
434
|
-
maxEvents: this.options.maxEvents ?? 10_000,
|
|
435
|
-
maxBytes: this.options.maxBytes ?? 8 * 1024 * 1024,
|
|
436
|
-
sampling: this.options.sampling ?? 1,
|
|
437
|
-
});
|
|
438
|
-
this.state = "armed";
|
|
439
|
-
this.startedAt = this.clock.now();
|
|
440
|
-
const before = await this.options.transport.snapshot?.("before");
|
|
441
|
-
if (before !== undefined)
|
|
442
|
-
await this.rawControlEvent("snapshot", {
|
|
443
|
-
trigger: {
|
|
444
|
-
phase: "before",
|
|
445
|
-
workloadId: this.plan.workloadId,
|
|
446
|
-
snapshotPhase: "before",
|
|
447
|
-
},
|
|
448
|
-
before,
|
|
449
|
-
status: "confirmed",
|
|
450
|
-
});
|
|
451
|
-
await this.options.transport.resumeProducer?.();
|
|
452
|
-
await this.writeManifest();
|
|
453
|
-
this.state = "running";
|
|
454
|
-
await this.writeManifest();
|
|
455
|
-
return this.manifest;
|
|
456
|
-
}
|
|
457
|
-
catch (error) {
|
|
458
|
-
this.markFailure(captureStage === "capture" ? "capture_fatal" : "attachment_fatal", error);
|
|
459
|
-
try {
|
|
460
|
-
await this.cleanup("start_failure");
|
|
461
|
-
}
|
|
462
|
-
catch (cleanupError) {
|
|
463
|
-
// A failed start remains a failed observation, but its cleanup evidence
|
|
464
|
-
// is still terminal and reviewable when all owned resources reached zero.
|
|
465
|
-
if (!this.frozen || this.terminalStatus !== "frozen")
|
|
466
|
-
throw cleanupError;
|
|
467
|
-
}
|
|
468
|
-
throw error;
|
|
43
|
+
constructor(options) { validateSelector(options.target); if (!Number.isInteger(options.pid) || options.pid <= 0)
|
|
44
|
+
throw new Error("pid must be positive"); this.options = options; }
|
|
45
|
+
get status() { return this.state; }
|
|
46
|
+
get manifest() { return { schema: "wow.focus-session.v1", sessionId: this.options.sessionId, pid: this.options.pid, buildKey: this.options.buildKey, moduleBase: this.options.moduleBase, imageBase: this.options.imageBase, target: this.options.target, environment: this.options.environment ?? {}, triggerPlan: this.options.triggerPlan ?? {}, startTime: this.startedAt ? new Date(this.startedAt).toISOString() : null, stopTime: this.stoppedAt ? new Date(this.stoppedAt).toISOString() : null, hookIds: this.hooks, droppedEventCount: this.dropped, cleanupStatus: this.cleanupStatus, state: this.state }; }
|
|
47
|
+
async start() { if (this.state !== "created")
|
|
48
|
+
throw new Error("session already started"); await this.options.transport.revalidateIdentity(this.identity()); this.hooks = await this.options.transport.installHooks(this.options.target); this.startedAt = this.clock.now(); this.state = "running"; await this.writeManifest(); return this.manifest; }
|
|
49
|
+
async pause() { await this.revalidate(); if (this.state !== "running")
|
|
50
|
+
throw new Error("session is not running"); this.state = "paused"; await this.writeManifest(); }
|
|
51
|
+
async resume() { await this.revalidate(); if (this.state !== "paused")
|
|
52
|
+
throw new Error("session is not paused"); this.state = "running"; await this.writeManifest(); }
|
|
53
|
+
async checkpoint(label = "checkpoint") { await this.revalidate(); await this.writeManifest({ checkpoint: label, seq: this.seq }); return this.manifest; }
|
|
54
|
+
async emit(input) {
|
|
55
|
+
if (this.state !== "running") {
|
|
56
|
+
this.dropped++;
|
|
57
|
+
return false;
|
|
469
58
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
if (this.state !== "running")
|
|
474
|
-
throw new Error("session is not running");
|
|
475
|
-
await this.options.transport.pauseProducer?.();
|
|
476
|
-
this.pausedAt = this.clock.now();
|
|
477
|
-
await this.controlEvent("pause");
|
|
478
|
-
this.state = "paused";
|
|
479
|
-
await this.writeManifest();
|
|
480
|
-
}
|
|
481
|
-
async resume() {
|
|
482
|
-
await this.revalidate();
|
|
483
|
-
if (this.state !== "paused")
|
|
484
|
-
throw new Error("session is not paused");
|
|
485
|
-
const end = this.clock.now();
|
|
486
|
-
const start = this.pausedAt ?? end;
|
|
487
|
-
await this.options.transport.resumeProducer?.();
|
|
488
|
-
this.state = "running";
|
|
489
|
-
this.pauseGaps.push({
|
|
490
|
-
start: new Date(start).toISOString(),
|
|
491
|
-
end: new Date(end).toISOString(),
|
|
492
|
-
durationMs: end - start,
|
|
493
|
-
});
|
|
494
|
-
await this.controlEvent("resume", {
|
|
495
|
-
before: { timestamp: new Date(start).toISOString() },
|
|
496
|
-
after: { timestamp: new Date(end).toISOString() },
|
|
497
|
-
diff: { durationMs: end - start, continuityClaimed: false },
|
|
498
|
-
});
|
|
499
|
-
this.pausedAt = undefined;
|
|
500
|
-
await this.writeManifest();
|
|
501
|
-
}
|
|
502
|
-
async checkpoint(label = "checkpoint", trigger = {}, snapshotNow = false) {
|
|
503
|
-
await this.revalidate();
|
|
504
|
-
if (this.state !== "running")
|
|
505
|
-
throw new Error("CHECKPOINT_SESSION_NOT_RUNNING");
|
|
506
|
-
const snapshot = snapshotNow
|
|
507
|
-
? await this.options.transport.snapshot?.("checkpoint")
|
|
508
|
-
: undefined;
|
|
509
|
-
await this.emit({
|
|
510
|
-
target: { kind: this.selector.kind },
|
|
511
|
-
trigger: {
|
|
512
|
-
phase: this.phase,
|
|
513
|
-
workloadId: this.plan.workloadId,
|
|
514
|
-
...trigger,
|
|
515
|
-
},
|
|
516
|
-
status: "dynamic",
|
|
517
|
-
event: { kind: "checkpoint", depth: 0 },
|
|
518
|
-
...(snapshot !== undefined ? { after: snapshot } : {}),
|
|
519
|
-
});
|
|
520
|
-
const prefixes = (await this.options.eventSink.prefixes?.()) ?? [];
|
|
521
|
-
await this.writeManifest({
|
|
522
|
-
checkpoint: label,
|
|
523
|
-
seq: this.seq,
|
|
524
|
-
streamPrefixes: prefixes,
|
|
525
|
-
});
|
|
526
|
-
return { ...this.manifest, streamPrefixes: prefixes };
|
|
527
|
-
}
|
|
528
|
-
async mergeExternalDrops(snapshot, source = "transport") {
|
|
529
|
-
const totalEvents = safeNonNegativeInteger(snapshot.droppedEventCount);
|
|
530
|
-
const totalBytes = safeNonNegativeInteger(snapshot.droppedByteCount);
|
|
531
|
-
const eventDelta = Math.max(0, totalEvents - this.externalDroppedEvents);
|
|
532
|
-
const byteDelta = Math.max(0, totalBytes - this.externalDroppedBytes);
|
|
533
|
-
const reasons = isRecord(snapshot.droppedByReason)
|
|
534
|
-
? snapshot.droppedByReason
|
|
535
|
-
: {};
|
|
536
|
-
if (isRecord(snapshot.dropWindows))
|
|
537
|
-
for (const [reason, window] of Object.entries(snapshot.dropWindows))
|
|
538
|
-
this.dropWindows[`${source}:${reason}`] = window;
|
|
539
|
-
const reasonDelta = {};
|
|
540
|
-
for (const [reason, raw] of Object.entries(reasons)) {
|
|
541
|
-
const value = safeNonNegativeInteger(raw);
|
|
542
|
-
const delta = Math.max(0, value - (this.externalDroppedReasons[reason] ?? 0));
|
|
543
|
-
if (delta)
|
|
544
|
-
reasonDelta[reason] = delta;
|
|
545
|
-
this.externalDroppedReasons[reason] = value;
|
|
59
|
+
if (this.options.sampling !== undefined && this.options.sampling < 1 && Math.random() > this.options.sampling) {
|
|
60
|
+
this.dropped++;
|
|
61
|
+
return false;
|
|
546
62
|
}
|
|
547
|
-
this.
|
|
548
|
-
this.externalDroppedBytes = Math.max(this.externalDroppedBytes, totalBytes);
|
|
549
|
-
if (!eventDelta && !byteDelta && !Object.keys(reasonDelta).length)
|
|
550
|
-
return;
|
|
551
|
-
this.droppedEvents += eventDelta;
|
|
552
|
-
this.droppedBytes += byteDelta;
|
|
553
|
-
for (const [reason, delta] of Object.entries(reasonDelta))
|
|
554
|
-
this.droppedReasons[`${source}:${reason}`] =
|
|
555
|
-
(this.droppedReasons[`${source}:${reason}`] ?? 0) + delta;
|
|
556
|
-
if (["running", "paused"].includes(this.state))
|
|
557
|
-
await this.rawControlEvent("overflow", {
|
|
558
|
-
diff: {
|
|
559
|
-
source,
|
|
560
|
-
droppedEvents: eventDelta,
|
|
561
|
-
droppedBytes: byteDelta,
|
|
562
|
-
droppedByReason: reasonDelta,
|
|
563
|
-
},
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
async emit(input) {
|
|
567
|
-
const ingressOrdinal = ++this.ingressOrdinal;
|
|
568
|
-
if (!this.receiveOpen || this.state !== "running")
|
|
569
|
-
return this.drop("not_running", Buffer.byteLength(JSON.stringify(input)), ingressOrdinal);
|
|
570
|
-
return this.enqueue(async () => this.persistInput(input, ingressOrdinal));
|
|
571
|
-
}
|
|
572
|
-
async emitTransport(input) {
|
|
573
|
-
const ingressOrdinal = ++this.ingressOrdinal;
|
|
574
|
-
if (!this.receiveOpen || this.state !== "running")
|
|
575
|
-
return this.drop("not_running", Buffer.byteLength(JSON.stringify(input)), ingressOrdinal);
|
|
576
|
-
return this.enqueue(async () => this.persistInput(input, ingressOrdinal));
|
|
577
|
-
}
|
|
578
|
-
async persistInput(input, ingressOrdinal) {
|
|
579
|
-
if (this.frozen)
|
|
580
|
-
return this.drop("artifact_frozen", Buffer.byteLength(JSON.stringify(input)), ingressOrdinal);
|
|
581
|
-
const phase = phaseOf(input.trigger);
|
|
582
|
-
if (!this.advancePhase(phase))
|
|
583
|
-
return this.drop("phase_regression", Buffer.byteLength(JSON.stringify(input)));
|
|
584
|
-
if (this.options.sampling !== undefined &&
|
|
585
|
-
this.options.sampling < 1 &&
|
|
586
|
-
(this.options.random ?? Math.random)() > this.options.sampling)
|
|
587
|
-
return this.drop("sampling", Buffer.byteLength(JSON.stringify(input)));
|
|
588
|
-
const event = this.makeEvent(input.event.kind, input, ingressOrdinal, this.seq + 1);
|
|
63
|
+
const event = { ...input, seq: this.seq + 1, sessionId: this.options.sessionId, buildKey: this.options.buildKey, pid: this.options.pid, moduleBase: this.options.moduleBase, environment: this.options.environment ?? {}, event: { ...input.event, timestamp: new Date(this.clock.now()).toISOString(), depth: input.event.depth ?? 0 } };
|
|
589
64
|
const size = Buffer.byteLength(JSON.stringify(event));
|
|
590
|
-
if (this.options.maxEvents && this.seq >= this.options.maxEvents)
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
65
|
+
if ((this.options.maxEvents && this.seq >= this.options.maxEvents) || (this.options.maxBytes && this.bytes + size > this.options.maxBytes)) {
|
|
66
|
+
this.dropped++;
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
594
69
|
this.seq++;
|
|
595
70
|
this.bytes += size;
|
|
596
|
-
|
|
597
|
-
await this.appendEvent(event);
|
|
598
|
-
}
|
|
599
|
-
catch (error) {
|
|
600
|
-
this.seq--;
|
|
601
|
-
this.bytes -= size;
|
|
602
|
-
this.drop(error instanceof Error && error.name === "AbortError"
|
|
603
|
-
? "append_aborted"
|
|
604
|
-
: "writer_failure", size, ingressOrdinal, false);
|
|
605
|
-
throw error;
|
|
606
|
-
}
|
|
71
|
+
await this.options.eventSink.append(event);
|
|
607
72
|
return true;
|
|
608
73
|
}
|
|
609
|
-
async
|
|
610
|
-
|
|
611
|
-
this.
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
async enter(threadId, input) {
|
|
622
|
-
const stack = this.stacks.get(threadId) ?? [];
|
|
623
|
-
const invocationId = `${this.options.sessionId}:${++this.invocationCounter}`;
|
|
624
|
-
const parentInvocationId = stack.at(-1);
|
|
625
|
-
stack.push(invocationId);
|
|
626
|
-
this.stacks.set(threadId, stack);
|
|
627
|
-
return this.emit({
|
|
628
|
-
...input,
|
|
629
|
-
event: {
|
|
630
|
-
kind: "enter",
|
|
631
|
-
threadId,
|
|
632
|
-
depth: stack.length,
|
|
633
|
-
invocationId,
|
|
634
|
-
parentInvocationId,
|
|
635
|
-
recursive: stack.length > 1,
|
|
636
|
-
},
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
async leave(threadId, input) {
|
|
640
|
-
const stack = this.stacks.get(threadId) ?? [];
|
|
641
|
-
const invocationId = stack.at(-1);
|
|
642
|
-
const parentInvocationId = stack.at(-2);
|
|
643
|
-
const ok = await this.emit({
|
|
644
|
-
...input,
|
|
645
|
-
event: {
|
|
646
|
-
kind: "leave",
|
|
647
|
-
threadId,
|
|
648
|
-
depth: Math.max(1, stack.length),
|
|
649
|
-
invocationId,
|
|
650
|
-
parentInvocationId,
|
|
651
|
-
recursive: stack.length > 1,
|
|
652
|
-
},
|
|
653
|
-
});
|
|
654
|
-
stack.pop();
|
|
655
|
-
if (!stack.length)
|
|
656
|
-
this.stacks.delete(threadId);
|
|
657
|
-
return ok;
|
|
658
|
-
}
|
|
659
|
-
async captureDiff(kind, before, after, base) {
|
|
660
|
-
if (!this.options.diffValidator)
|
|
661
|
-
throw new Error("DIFF_VALIDATOR_REQUIRED");
|
|
662
|
-
const diff = await this.options.diffValidator.diff(kind, before, after);
|
|
663
|
-
const validation = await this.options.diffValidator.validate(kind, before, after, diff);
|
|
664
|
-
return this.emit({
|
|
665
|
-
...base,
|
|
666
|
-
before,
|
|
667
|
-
after,
|
|
668
|
-
diff: { value: diff, validation },
|
|
669
|
-
status: validation.valid ? base.status : "partial",
|
|
670
|
-
event: { kind: `${kind}_change`, depth: 0 },
|
|
671
|
-
});
|
|
672
|
-
}
|
|
673
|
-
async targetExited(cleanupDeadline) {
|
|
674
|
-
if (["stopped", "stopping"].includes(this.state))
|
|
675
|
-
return this.manifest;
|
|
676
|
-
this.state = "target_exited";
|
|
677
|
-
await this.rawControlEvent("exception", {
|
|
678
|
-
diff: { reason: "target_exit" },
|
|
679
|
-
});
|
|
680
|
-
try {
|
|
681
|
-
await this.cleanup("target_exit", cleanupDeadline);
|
|
682
|
-
}
|
|
683
|
-
catch (error) {
|
|
684
|
-
if (isAbortError(error) || String(error).includes("CAPTURE_CLEANUP_INCOMPLETE"))
|
|
685
|
-
throw new Error("CAPTURE_CLEANUP_INCOMPLETE");
|
|
686
|
-
throw error;
|
|
687
|
-
}
|
|
688
|
-
return this.manifest;
|
|
689
|
-
}
|
|
690
|
-
async stop(cleanupDeadline) {
|
|
691
|
-
try {
|
|
692
|
-
await this.cleanup("requested", cleanupDeadline);
|
|
693
|
-
}
|
|
694
|
-
catch (error) {
|
|
695
|
-
if (isAbortError(error) || String(error).includes("CAPTURE_CLEANUP_INCOMPLETE"))
|
|
696
|
-
throw new Error("CAPTURE_CLEANUP_INCOMPLETE");
|
|
697
|
-
throw error;
|
|
698
|
-
}
|
|
699
|
-
return this.manifest;
|
|
700
|
-
}
|
|
701
|
-
get clock() {
|
|
702
|
-
return this.options.clock ?? systemClock;
|
|
703
|
-
}
|
|
704
|
-
identity() {
|
|
705
|
-
return {
|
|
706
|
-
pid: this.options.pid,
|
|
707
|
-
buildKey: this.options.buildKey,
|
|
708
|
-
moduleBase: this.options.moduleBase,
|
|
709
|
-
imageBase: this.options.imageBase,
|
|
710
|
-
processStartTime: this.options.processStartTime,
|
|
711
|
-
moduleIdentity: this.options.moduleIdentity,
|
|
712
|
-
};
|
|
713
|
-
}
|
|
714
|
-
async revalidate() {
|
|
715
|
-
const actual = await this.options.transport.revalidateIdentity(this.identity());
|
|
716
|
-
if (actual.pid !== this.options.pid ||
|
|
717
|
-
actual.buildKey !== this.options.buildKey ||
|
|
718
|
-
actual.moduleBase.toLowerCase() !==
|
|
719
|
-
this.options.moduleBase.toLowerCase() ||
|
|
720
|
-
(this.options.processStartTime !== undefined &&
|
|
721
|
-
actual.processStartTime !== this.options.processStartTime) ||
|
|
722
|
-
(this.options.moduleIdentity !== undefined &&
|
|
723
|
-
actual.moduleIdentity !== this.options.moduleIdentity)) {
|
|
724
|
-
this.markFailure("attachment_fatal", new Error("TARGET_IDENTITY_CHANGED"));
|
|
725
|
-
throw new Error("TARGET_IDENTITY_CHANGED");
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
advancePhase(next) {
|
|
729
|
-
const rank = { before: 0, during: 1, after: 2 };
|
|
730
|
-
if (rank[next] < rank[this.phase])
|
|
731
|
-
return false;
|
|
732
|
-
this.phase = next;
|
|
733
|
-
return true;
|
|
734
|
-
}
|
|
735
|
-
drop(reason, bytes, ingressOrdinal = this.ingressOrdinal, emitOverflow = true) {
|
|
736
|
-
this.droppedEvents++;
|
|
737
|
-
this.droppedBytes += bytes;
|
|
738
|
-
this.droppedReasons[reason] = (this.droppedReasons[reason] ?? 0) + 1;
|
|
739
|
-
const timestamp = new Date(this.clock.now()).toISOString();
|
|
740
|
-
const current = isRecord(this.dropWindows[reason])
|
|
741
|
-
? this.dropWindows[reason]
|
|
742
|
-
: {
|
|
743
|
-
firstLostIngressOrdinal: ingressOrdinal,
|
|
744
|
-
firstTimestamp: timestamp,
|
|
745
|
-
producer: "host",
|
|
746
|
-
threadIds: [],
|
|
747
|
-
chainCompletenessAffected: true,
|
|
748
|
-
};
|
|
749
|
-
current.lastLostIngressOrdinal = ingressOrdinal;
|
|
750
|
-
current.lastTimestamp = timestamp;
|
|
751
|
-
this.dropWindows[reason] = current;
|
|
752
|
-
if (emitOverflow && this.receiveOpen && !this.frozen)
|
|
753
|
-
void this.rawControlEvent("overflow", {
|
|
754
|
-
diff: {
|
|
755
|
-
reason,
|
|
756
|
-
truncation: reason === "max_bytes",
|
|
757
|
-
droppedEvents: this.droppedEvents,
|
|
758
|
-
droppedBytes: this.droppedBytes,
|
|
759
|
-
dropWindow: current,
|
|
760
|
-
},
|
|
761
|
-
});
|
|
762
|
-
return false;
|
|
763
|
-
}
|
|
764
|
-
async controlEvent(kind, extra = {}) {
|
|
765
|
-
return this.emit({
|
|
766
|
-
target: { kind: this.selector.kind },
|
|
767
|
-
trigger: { phase: this.phase, workloadId: this.plan.workloadId },
|
|
768
|
-
status: "dynamic",
|
|
769
|
-
event: { kind, depth: 0 },
|
|
770
|
-
...extra,
|
|
771
|
-
});
|
|
772
|
-
}
|
|
773
|
-
async rawControlEvent(kind, extra = {}) {
|
|
774
|
-
const ingressOrdinal = ++this.ingressOrdinal;
|
|
775
|
-
return this.enqueue(async () => {
|
|
776
|
-
if (this.frozen)
|
|
777
|
-
return;
|
|
778
|
-
const event = this.makeEvent(kind, extra, ingressOrdinal, this.seq + 1);
|
|
779
|
-
const size = Buffer.byteLength(JSON.stringify(event));
|
|
780
|
-
this.seq++;
|
|
781
|
-
this.bytes += size;
|
|
782
|
-
try {
|
|
783
|
-
await this.appendEvent(event);
|
|
784
|
-
}
|
|
785
|
-
catch (error) {
|
|
786
|
-
this.seq--;
|
|
787
|
-
this.bytes -= size;
|
|
788
|
-
this.drop(error instanceof Error && error.name === "AbortError"
|
|
789
|
-
? "append_aborted"
|
|
790
|
-
: "writer_failure", size, ingressOrdinal, false);
|
|
791
|
-
throw error;
|
|
792
|
-
}
|
|
793
|
-
});
|
|
794
|
-
}
|
|
795
|
-
markFailure(kind, error) {
|
|
796
|
-
this.failureClass = kind;
|
|
797
|
-
this.errorCode = {
|
|
798
|
-
capture_fatal: "CAPTURE_FATAL",
|
|
799
|
-
attachment_fatal: "ATTACHMENT_FATAL",
|
|
800
|
-
broker_fatal: "BROKER_FATAL",
|
|
801
|
-
}[kind];
|
|
74
|
+
async enter(threadId, input) { const d = (this.depth.get(threadId) ?? 0) + 1; this.depth.set(threadId, d); return this.emit({ ...input, event: { kind: "enter", threadId, depth: d } }); }
|
|
75
|
+
async leave(threadId, input) { const d = this.depth.get(threadId) ?? 1; const ok = await this.emit({ ...input, event: { kind: "leave", threadId, depth: d } }); if (d <= 1)
|
|
76
|
+
this.depth.delete(threadId);
|
|
77
|
+
else
|
|
78
|
+
this.depth.set(threadId, d - 1); return ok; }
|
|
79
|
+
async stop() { if (this.state === "stopped")
|
|
80
|
+
return this.manifest; this.state = "stopped"; this.stoppedAt = this.clock.now(); this.cleanupStatus = await this.options.transport.cleanup(this.hooks); await this.options.transport.detach?.(); await this.writeManifest(); return this.manifest; }
|
|
81
|
+
get clock() { return this.options.clock ?? systemClock; }
|
|
82
|
+
identity() { return { pid: this.options.pid, buildKey: this.options.buildKey, moduleBase: this.options.moduleBase, imageBase: this.options.imageBase }; }
|
|
83
|
+
async revalidate() { const actual = await this.options.transport.revalidateIdentity(this.identity()); if (actual.pid !== this.options.pid || actual.buildKey !== this.options.buildKey || actual.moduleBase.toLowerCase() !== this.options.moduleBase.toLowerCase()) {
|
|
802
84
|
this.state = "failed";
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
failureClass: kind,
|
|
808
|
-
errorCode: this.errorCode,
|
|
809
|
-
sessionId: this.options.sessionId,
|
|
810
|
-
pid: this.options.pid,
|
|
811
|
-
buildKey: this.options.buildKey,
|
|
812
|
-
error,
|
|
813
|
-
})).catch(() => undefined);
|
|
814
|
-
}
|
|
815
|
-
catch { /* reporting cannot mask the originating failure */ }
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
async cleanup(reason, requestedCleanupDeadline) {
|
|
819
|
-
if (this.cleanupTerminalComplete && this.cleanupPromise)
|
|
820
|
-
return this.cleanupPromise;
|
|
821
|
-
if (this.cleanupPromise)
|
|
822
|
-
return this.cleanupPromise;
|
|
823
|
-
this.receiveOpen = false;
|
|
824
|
-
this.draining = true;
|
|
825
|
-
this.cleanupPromise = (async () => {
|
|
826
|
-
if (!this.cleanupIntendedTerminal) {
|
|
827
|
-
this.cleanupIntendedTerminal =
|
|
828
|
-
this.state === "target_exited"
|
|
829
|
-
? "target_exited"
|
|
830
|
-
: this.state === "failed"
|
|
831
|
-
? "failed"
|
|
832
|
-
: "stopped";
|
|
833
|
-
}
|
|
834
|
-
const terminal = this.cleanupIntendedTerminal;
|
|
835
|
-
if (!this.cleanupCoreComplete) {
|
|
836
|
-
this.state = "stopping";
|
|
837
|
-
this.stoppedAt = this.clock.now();
|
|
838
|
-
}
|
|
839
|
-
const cleanupNow = () => this.clock.monotonicNow?.() ?? this.clock.now();
|
|
840
|
-
const cleanupStartedAt = cleanupNow();
|
|
841
|
-
const cleanupBudgetMs = Math.min(2_000, this.options.cleanupTimeoutMs ?? 2_000);
|
|
842
|
-
const localDeadline = cleanupStartedAt + cleanupBudgetMs;
|
|
843
|
-
const deadline = Math.min(localDeadline, requestedCleanupDeadline ?? localDeadline, this.options.cleanupDeadline ?? localDeadline);
|
|
844
|
-
const effectiveCleanupBudgetMs = Math.max(0, deadline - cleanupStartedAt);
|
|
845
|
-
const remainingCleanupMs = () => Math.max(0, deadline - cleanupNow());
|
|
846
|
-
const errors = [];
|
|
847
|
-
const steps = this.cleanupSteps;
|
|
848
|
-
if (!this.cleanupCoreComplete)
|
|
849
|
-
steps.push({
|
|
850
|
-
step: "stop_accepting_events",
|
|
851
|
-
status: "passed",
|
|
852
|
-
remainingMs: effectiveCleanupBudgetMs,
|
|
853
|
-
});
|
|
854
|
-
let bestEffortTail = Promise.resolve();
|
|
855
|
-
let tailDetached = false;
|
|
856
|
-
const detachedOperations = [];
|
|
857
|
-
const run = async (step, operation) => {
|
|
858
|
-
const remaining = remainingCleanupMs();
|
|
859
|
-
const pending = (tailDetached ? Promise.resolve() : bestEffortTail).then(operation);
|
|
860
|
-
let timedOut = false;
|
|
861
|
-
void pending.then(() => {
|
|
862
|
-
if (timedOut)
|
|
863
|
-
this.cleanupLateCompletions++;
|
|
864
|
-
}, () => {
|
|
865
|
-
if (timedOut)
|
|
866
|
-
this.cleanupLateCompletions++;
|
|
867
|
-
});
|
|
868
|
-
bestEffortTail = pending.then(() => undefined, () => undefined);
|
|
869
|
-
if (remaining === 0) {
|
|
870
|
-
// Invoke the remaining best-effort operation even when its wait budget is exhausted.
|
|
871
|
-
timedOut = true;
|
|
872
|
-
tailDetached = true;
|
|
873
|
-
detachedOperations.push({ step, promise: pending });
|
|
874
|
-
bestEffortTail = Promise.resolve();
|
|
875
|
-
this.cleanupDetachedOperations.push({
|
|
876
|
-
step,
|
|
877
|
-
status: "detached",
|
|
878
|
-
detachedAt: this.clock.now(),
|
|
879
|
-
});
|
|
880
|
-
const failure = {
|
|
881
|
-
step,
|
|
882
|
-
errorCode: "CLEANUP_TIMEOUT",
|
|
883
|
-
remainingMs: 0,
|
|
884
|
-
};
|
|
885
|
-
errors.push(failure);
|
|
886
|
-
steps.push({ ...failure, status: "failed" });
|
|
887
|
-
return undefined;
|
|
888
|
-
}
|
|
889
|
-
let timer;
|
|
890
|
-
try {
|
|
891
|
-
const deadlineWait = this.clock.sleep
|
|
892
|
-
? Promise.resolve()
|
|
893
|
-
// Give the queued operation its first microtask before a
|
|
894
|
-
// deterministic clock advances the aggregate deadline.
|
|
895
|
-
.then(() => this.clock.sleep(remaining))
|
|
896
|
-
.then(() => Promise.reject(new Error("CLEANUP_TIMEOUT")))
|
|
897
|
-
: new Promise((_, reject) => {
|
|
898
|
-
timer = setTimeout(() => reject(new Error("CLEANUP_TIMEOUT")), remaining);
|
|
899
|
-
});
|
|
900
|
-
const result = await Promise.race([
|
|
901
|
-
pending,
|
|
902
|
-
deadlineWait,
|
|
903
|
-
]);
|
|
904
|
-
steps.push({
|
|
905
|
-
step,
|
|
906
|
-
status: "passed",
|
|
907
|
-
remainingMs: remainingCleanupMs(),
|
|
908
|
-
});
|
|
909
|
-
return result;
|
|
910
|
-
}
|
|
911
|
-
catch (error) {
|
|
912
|
-
const code = String(error).includes("CLEANUP_TIMEOUT")
|
|
913
|
-
? "CLEANUP_TIMEOUT"
|
|
914
|
-
: "CLEANUP_STEP_FAILED";
|
|
915
|
-
if (code === "CLEANUP_TIMEOUT") {
|
|
916
|
-
timedOut = true;
|
|
917
|
-
tailDetached = true;
|
|
918
|
-
detachedOperations.push({ step, promise: pending });
|
|
919
|
-
bestEffortTail = Promise.resolve();
|
|
920
|
-
this.cleanupDetachedOperations.push({
|
|
921
|
-
step,
|
|
922
|
-
status: "detached",
|
|
923
|
-
detachedAt: this.clock.now(),
|
|
924
|
-
});
|
|
925
|
-
}
|
|
926
|
-
const failure = {
|
|
927
|
-
step,
|
|
928
|
-
errorCode: code,
|
|
929
|
-
error: String(error),
|
|
930
|
-
remainingMs: remainingCleanupMs(),
|
|
931
|
-
};
|
|
932
|
-
errors.push(failure);
|
|
933
|
-
steps.push({ ...failure, status: "failed" });
|
|
934
|
-
return undefined;
|
|
935
|
-
}
|
|
936
|
-
finally {
|
|
937
|
-
if (timer)
|
|
938
|
-
clearTimeout(timer);
|
|
939
|
-
}
|
|
940
|
-
};
|
|
941
|
-
if (!this.cleanupCoreComplete) {
|
|
942
|
-
const after = await run("after_snapshot", () => this.options.transport.snapshot?.("after"));
|
|
943
|
-
if (after !== undefined)
|
|
944
|
-
await run("append_after_snapshot", () => this.rawControlEvent("snapshot", {
|
|
945
|
-
trigger: {
|
|
946
|
-
phase: "after",
|
|
947
|
-
workloadId: this.plan.workloadId,
|
|
948
|
-
snapshotPhase: "after",
|
|
949
|
-
},
|
|
950
|
-
after,
|
|
951
|
-
status: "confirmed",
|
|
952
|
-
}));
|
|
953
|
-
const cleaned = await run("trace_hook_script_cleanup", () => this.options.transport.cleanup(this.hooks));
|
|
954
|
-
this.cleanupStatus = cleaned ?? {
|
|
955
|
-
hooks: this.hooks.length,
|
|
956
|
-
scripts: 1,
|
|
957
|
-
interceptors: this.hooks.length,
|
|
958
|
-
residualErrors: errors,
|
|
959
|
-
};
|
|
960
|
-
await run("append_cleanup_evidence", () => this.rawControlEvent("cleanup", {
|
|
961
|
-
diff: { reason, ...this.cleanupStatus, steps, errors },
|
|
962
|
-
}));
|
|
963
|
-
await run("drain_append_queue", () => this.appendTail);
|
|
964
|
-
if (this.appendInFlight > 0 &&
|
|
965
|
-
detachedOperations.some(operation => operation.step === "drain_append_queue")) {
|
|
966
|
-
this.abortPendingAppends();
|
|
967
|
-
void this.appendTail.then(() => { this.cleanupLateCompletions++; }, () => { this.cleanupLateCompletions++; });
|
|
968
|
-
errors.push({
|
|
969
|
-
step: "append_abort",
|
|
970
|
-
errorCode: "CLEANUP_TIMEOUT",
|
|
971
|
-
error: "append queue was aborted and detached at the aggregate cleanup deadline",
|
|
972
|
-
remainingMs: 0,
|
|
973
|
-
});
|
|
974
|
-
}
|
|
975
|
-
const residual = this.cleanupStatus.hooks !== 0 ||
|
|
976
|
-
this.cleanupStatus.scripts !== 0 ||
|
|
977
|
-
this.cleanupStatus.interceptors !== 0 ||
|
|
978
|
-
(this.cleanupStatus.residualErrors?.length ?? 0) > 0;
|
|
979
|
-
this.cleanupCoreFailed = residual || errors.length > 0;
|
|
980
|
-
if (this.cleanupCoreFailed)
|
|
981
|
-
this.markFailure("capture_fatal");
|
|
982
|
-
else
|
|
983
|
-
this.state = terminal;
|
|
984
|
-
this.cleanupStreamPrefixes =
|
|
985
|
-
(await run("stream_prefixes", () => this.options.eventSink.prefixes?.() ?? [])) ?? [];
|
|
986
|
-
this.cleanupFragment = (await run("write_manifest", () => this.writeManifest({
|
|
987
|
-
streamPrefixes: this.cleanupStreamPrefixes,
|
|
988
|
-
cleanupDeadlineMs: effectiveCleanupBudgetMs,
|
|
989
|
-
cleanupSteps: steps,
|
|
990
|
-
cleanupDetachedOperations: this.cleanupDetachedOperations,
|
|
991
|
-
cleanupLateCompletions: this.cleanupLateCompletions,
|
|
992
|
-
}))) ?? {
|
|
993
|
-
...this.manifest,
|
|
994
|
-
streamPrefixes: this.cleanupStreamPrefixes,
|
|
995
|
-
cleanupDeadlineMs: effectiveCleanupBudgetMs,
|
|
996
|
-
cleanupSteps: steps,
|
|
997
|
-
cleanupDetachedOperations: this.cleanupDetachedOperations,
|
|
998
|
-
cleanupLateCompletions: this.cleanupLateCompletions,
|
|
999
|
-
};
|
|
1000
|
-
this.cleanupCoreComplete = true;
|
|
1001
|
-
}
|
|
1002
|
-
const streamPrefixes = this.cleanupStreamPrefixes;
|
|
1003
|
-
const fragment = this.cleanupFragment ?? { ...this.manifest, streamPrefixes };
|
|
1004
|
-
let detachSucceeded = this.options.detachOnCleanup === false;
|
|
1005
|
-
if (this.options.detachOnCleanup === false) {
|
|
1006
|
-
if (!steps.some(entry => entry.step === "detach_deferred_to_broker"))
|
|
1007
|
-
steps.push({
|
|
1008
|
-
step: "detach_deferred_to_broker",
|
|
1009
|
-
status: "deferred",
|
|
1010
|
-
ownership: "broker",
|
|
1011
|
-
remainingMs: remainingCleanupMs(),
|
|
1012
|
-
});
|
|
1013
|
-
}
|
|
1014
|
-
else {
|
|
1015
|
-
const beforeDetachErrors = errors.length;
|
|
1016
|
-
await run("detach", () => this.options.transport.detach?.());
|
|
1017
|
-
detachSucceeded = errors.length === beforeDetachErrors;
|
|
1018
|
-
}
|
|
1019
|
-
if (!detachSucceeded) {
|
|
1020
|
-
const detachResidual = {
|
|
1021
|
-
step: "detach",
|
|
1022
|
-
errorCode: "CLEANUP_STEP_FAILED",
|
|
1023
|
-
residual: true,
|
|
1024
|
-
};
|
|
1025
|
-
this.cleanupStatus = {
|
|
1026
|
-
...this.cleanupStatus,
|
|
1027
|
-
residualErrors: [
|
|
1028
|
-
...(this.cleanupStatus.residualErrors ?? []),
|
|
1029
|
-
detachResidual,
|
|
1030
|
-
],
|
|
1031
|
-
};
|
|
1032
|
-
if (!steps.some(entry => entry.step === "detach" && entry.status === "failed")) {
|
|
1033
|
-
steps.push({
|
|
1034
|
-
step: "detach",
|
|
1035
|
-
status: "failed",
|
|
1036
|
-
errorCode: "CLEANUP_STEP_FAILED",
|
|
1037
|
-
residual: true,
|
|
1038
|
-
remainingMs: remainingCleanupMs(),
|
|
1039
|
-
});
|
|
1040
|
-
}
|
|
1041
|
-
this.cleanupDetachRetryable = !this.cleanupCoreFailed;
|
|
1042
|
-
this.markFailure("capture_fatal");
|
|
1043
|
-
}
|
|
1044
|
-
else {
|
|
1045
|
-
this.cleanupDetachRetryable = false;
|
|
1046
|
-
if (this.cleanupStatus.residualErrors)
|
|
1047
|
-
this.cleanupStatus = {
|
|
1048
|
-
...this.cleanupStatus,
|
|
1049
|
-
residualErrors: this.cleanupStatus.residualErrors.filter(entry => !isRecord(entry) || entry.step !== "detach"),
|
|
1050
|
-
};
|
|
1051
|
-
if (!this.cleanupCoreFailed) {
|
|
1052
|
-
this.failureClass = undefined;
|
|
1053
|
-
this.errorCode = undefined;
|
|
1054
|
-
this.state = terminal;
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
let freezeSucceeded = false;
|
|
1058
|
-
let freezeFailure;
|
|
1059
|
-
const freezeController = new AbortController();
|
|
1060
|
-
let finalPrefixes = streamPrefixes;
|
|
1061
|
-
if (detachSucceeded && !this.cleanupCoreFailed && errors.length === 0 && this.appendInFlight === 0) {
|
|
1062
|
-
finalPrefixes =
|
|
1063
|
-
(await run("final_stream_prefixes", () => this.options.eventSink.prefixes?.() ?? streamPrefixes)) ?? streamPrefixes;
|
|
1064
|
-
}
|
|
1065
|
-
const freezeRemaining = remainingCleanupMs();
|
|
1066
|
-
// Preserve part of the same aggregate deadline for publishing canonical
|
|
1067
|
-
// failure evidence if freeze hangs. Detach remains best-effort even if
|
|
1068
|
-
// both publication steps consume their bounded waits.
|
|
1069
|
-
// Reserve explicit time in the same absolute deadline for terminal
|
|
1070
|
-
// failure commit and publication quiescence. A freeze must not consume
|
|
1071
|
-
// the entire cleanup window.
|
|
1072
|
-
const FAILURE_COMMIT_RESERVE_MS = 500;
|
|
1073
|
-
const QUIESCENCE_RESERVE_MS = 100;
|
|
1074
|
-
const freezeWaitMs = Math.max(0, freezeRemaining - FAILURE_COMMIT_RESERVE_MS - QUIESCENCE_RESERVE_MS);
|
|
1075
|
-
if (detachSucceeded && !this.cleanupCoreFailed && errors.length === 0 && this.appendInFlight === 0 && freezeWaitMs > 0) {
|
|
1076
|
-
let timer;
|
|
1077
|
-
let freezeTimedOut = false;
|
|
1078
|
-
try {
|
|
1079
|
-
const frozenFragment = {
|
|
1080
|
-
...fragment,
|
|
1081
|
-
...this.manifest,
|
|
1082
|
-
streamPrefixes: finalPrefixes,
|
|
1083
|
-
};
|
|
1084
|
-
const pending = this.options.manifestSink?.freeze
|
|
1085
|
-
? Promise.resolve(this.options.manifestSink.freeze(frozenFragment, freezeController.signal))
|
|
1086
|
-
: Promise.resolve(this.options.manifestSink?.write(frozenFragment)).then(() => ({
|
|
1087
|
-
committed: true,
|
|
1088
|
-
terminalStatus: "frozen",
|
|
1089
|
-
}));
|
|
1090
|
-
void pending.then(() => { if (freezeTimedOut)
|
|
1091
|
-
this.cleanupLateCompletions++; }, () => { if (freezeTimedOut)
|
|
1092
|
-
this.cleanupLateCompletions++; });
|
|
1093
|
-
const timeout = this.clock.sleep
|
|
1094
|
-
? this.clock.sleep(freezeWaitMs).then(() => { throw new Error("FREEZE_TIMEOUT"); })
|
|
1095
|
-
: new Promise((_, reject) => {
|
|
1096
|
-
timer = setTimeout(() => reject(new Error("FREEZE_TIMEOUT")), freezeWaitMs);
|
|
1097
|
-
});
|
|
1098
|
-
const receipt = await Promise.race([pending, timeout]);
|
|
1099
|
-
if (!receipt?.committed || receipt.terminalStatus !== "frozen")
|
|
1100
|
-
throw new Error(`TERMINAL_FREEZE_NOT_COMMITTED:${receipt?.terminalStatus ?? "unknown"}`);
|
|
1101
|
-
freezeSucceeded = true;
|
|
1102
|
-
this.terminalStatus = "frozen";
|
|
1103
|
-
this.frozen = true;
|
|
1104
|
-
steps.push({ step: "freeze_artifacts", status: "passed", remainingMs: remainingCleanupMs() });
|
|
1105
|
-
}
|
|
1106
|
-
catch (error) {
|
|
1107
|
-
freezeFailure = error;
|
|
1108
|
-
if (String(error).includes("FREEZE_TIMEOUT")) {
|
|
1109
|
-
freezeTimedOut = true;
|
|
1110
|
-
this.cleanupDetachedOperations.push({
|
|
1111
|
-
step: "freeze_artifacts",
|
|
1112
|
-
status: "detached",
|
|
1113
|
-
detachedAt: this.clock.now(),
|
|
1114
|
-
});
|
|
1115
|
-
}
|
|
1116
|
-
freezeController.abort(error);
|
|
1117
|
-
steps.push({ step: "freeze_artifacts", status: "failed", errorCode: String(error).includes("FREEZE_TIMEOUT") ? "CLEANUP_TIMEOUT" : "CLEANUP_STEP_FAILED", error: String(error), remainingMs: remainingCleanupMs() });
|
|
1118
|
-
}
|
|
1119
|
-
finally {
|
|
1120
|
-
if (timer)
|
|
1121
|
-
clearTimeout(timer);
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
else {
|
|
1125
|
-
freezeFailure = new Error("FREEZE_PRECONDITION_FAILED");
|
|
1126
|
-
freezeController.abort(freezeFailure);
|
|
1127
|
-
steps.push({ step: "freeze_artifacts", status: "failed", errorCode: "FREEZE_PRECONDITION_FAILED", remainingMs: freezeRemaining });
|
|
1128
|
-
}
|
|
1129
|
-
if (!freezeSucceeded) {
|
|
1130
|
-
freezeFailure ??= new Error("CAPTURE_TERMINAL_FREEZE_FAILED");
|
|
1131
|
-
errors.push({ step: "freeze_artifacts", errorCode: String(freezeFailure).includes("FREEZE_TIMEOUT") ? "CLEANUP_TIMEOUT" : "CAPTURE_TERMINAL_FREEZE_FAILED", error: String(freezeFailure), remainingMs: remainingCleanupMs() });
|
|
1132
|
-
this.markFailure("capture_fatal");
|
|
1133
|
-
const terminalFailure = {
|
|
1134
|
-
...fragment,
|
|
1135
|
-
...this.manifest,
|
|
1136
|
-
frozen: false,
|
|
1137
|
-
terminal: true,
|
|
1138
|
-
terminalStatus: "failed",
|
|
1139
|
-
terminalFailure: {
|
|
1140
|
-
errorCode: "CAPTURE_TERMINAL_FREEZE_FAILED",
|
|
1141
|
-
error: String(freezeFailure),
|
|
1142
|
-
recoverable: true,
|
|
1143
|
-
nextAction: "retry artifact freeze from durable stream prefixes",
|
|
1144
|
-
},
|
|
1145
|
-
cleanupSteps: steps,
|
|
1146
|
-
cleanupDetachedOperations: this.cleanupDetachedOperations,
|
|
1147
|
-
cleanupLateCompletions: this.cleanupLateCompletions,
|
|
1148
|
-
};
|
|
1149
|
-
// Publishing failure evidence shares the aggregate cleanup deadline.
|
|
1150
|
-
// A stuck sink is detached so transport detach is still issued; the
|
|
1151
|
-
// frozen session state remains authoritative if that sink settles late.
|
|
1152
|
-
const failureController = new AbortController();
|
|
1153
|
-
const FAILURE_QUIESCENCE_RESERVE_MS = 100;
|
|
1154
|
-
const failureRemaining = remainingCleanupMs();
|
|
1155
|
-
const failurePublicationWaitMs = Math.max(0, failureRemaining - FAILURE_QUIESCENCE_RESERVE_MS);
|
|
1156
|
-
let failureTimer;
|
|
1157
|
-
let failureWaitActive = true;
|
|
1158
|
-
await run("publish_terminal_failure", async () => {
|
|
1159
|
-
const publication = this.options.manifestSink?.terminalFailure
|
|
1160
|
-
? Promise.resolve(this.options.manifestSink.terminalFailure(terminalFailure, failureController.signal))
|
|
1161
|
-
: Promise.resolve(this.options.manifestSink?.write(terminalFailure)).then(() => ({
|
|
1162
|
-
committed: true,
|
|
1163
|
-
terminalStatus: "failed",
|
|
1164
|
-
}));
|
|
1165
|
-
// Start the guarded writer before its abort timer. This gives the
|
|
1166
|
-
// publication a real commit opportunity while still reserving time
|
|
1167
|
-
// for its abort-aware unlink path before the aggregate deadline.
|
|
1168
|
-
if (failurePublicationWaitMs === 0) {
|
|
1169
|
-
failureController.abort(new Error("CLEANUP_TIMEOUT"));
|
|
1170
|
-
}
|
|
1171
|
-
else if (this.clock.sleep) {
|
|
1172
|
-
void this.clock.sleep(failurePublicationWaitMs).then(() => {
|
|
1173
|
-
if (failureWaitActive)
|
|
1174
|
-
failureController.abort(new Error("CLEANUP_TIMEOUT"));
|
|
1175
|
-
});
|
|
1176
|
-
}
|
|
1177
|
-
else {
|
|
1178
|
-
failureTimer = setTimeout(() => {
|
|
1179
|
-
if (failureWaitActive)
|
|
1180
|
-
failureController.abort(new Error("CLEANUP_TIMEOUT"));
|
|
1181
|
-
}, failurePublicationWaitMs);
|
|
1182
|
-
}
|
|
1183
|
-
const receipt = await publication;
|
|
1184
|
-
if (!receipt.committed || receipt.terminalStatus !== "failed")
|
|
1185
|
-
throw new Error(`TERMINAL_FAILURE_NOT_COMMITTED:${receipt.terminalStatus}`);
|
|
1186
|
-
this.terminalStatus = "failed";
|
|
1187
|
-
return receipt;
|
|
1188
|
-
});
|
|
1189
|
-
failureWaitActive = false;
|
|
1190
|
-
if (failureTimer)
|
|
1191
|
-
clearTimeout(failureTimer);
|
|
1192
|
-
}
|
|
1193
|
-
await run("quiesce_manifest_publications", () => this.options.manifestSink?.quiesce?.());
|
|
1194
|
-
this.draining = false;
|
|
1195
|
-
if (!this.cleanupDetachRetryable)
|
|
1196
|
-
this.cleanupTerminalComplete = true;
|
|
1197
|
-
if ((errors.length || this.cleanupCoreFailed || !freezeSucceeded) && !freezeSucceeded)
|
|
1198
|
-
throw new Error("CAPTURE_CLEANUP_INCOMPLETE");
|
|
1199
|
-
})();
|
|
1200
|
-
try {
|
|
1201
|
-
await this.cleanupPromise;
|
|
1202
|
-
}
|
|
1203
|
-
finally {
|
|
1204
|
-
if (this.cleanupDetachRetryable && !this.cleanupTerminalComplete)
|
|
1205
|
-
this.cleanupPromise = undefined;
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
enqueue(operation) {
|
|
1209
|
-
this.appendInFlight++;
|
|
1210
|
-
const result = this.appendTail.then(async () => {
|
|
1211
|
-
try {
|
|
1212
|
-
return await operation();
|
|
1213
|
-
}
|
|
1214
|
-
finally {
|
|
1215
|
-
this.appendInFlight--;
|
|
1216
|
-
}
|
|
1217
|
-
});
|
|
1218
|
-
this.appendTail = result.then(() => undefined, () => undefined);
|
|
1219
|
-
// The session owns the canonical append promise; detached callers may
|
|
1220
|
-
// observe the operation rejection, but it must never become an
|
|
1221
|
-
// unhandled rejection that escapes terminal cleanup.
|
|
1222
|
-
void result.catch(() => undefined);
|
|
1223
|
-
return result;
|
|
1224
|
-
}
|
|
1225
|
-
abortPendingAppends() {
|
|
1226
|
-
this.appendTransactionsAborted = true;
|
|
1227
|
-
for (const controller of this.appendControllers)
|
|
1228
|
-
controller.abort();
|
|
1229
|
-
}
|
|
1230
|
-
makeEvent(kind, input, ingressOrdinal, seq) {
|
|
1231
|
-
const target = isRecord(input.target) ? input.target : {};
|
|
1232
|
-
const trigger = isRecord(input.trigger) ? input.trigger : {};
|
|
1233
|
-
const rawEvent = isRecord(input.event) ? input.event : {};
|
|
1234
|
-
return {
|
|
1235
|
-
seq,
|
|
1236
|
-
ingressOrdinal,
|
|
1237
|
-
sessionId: this.options.sessionId,
|
|
1238
|
-
buildKey: this.options.buildKey,
|
|
1239
|
-
pid: this.options.pid,
|
|
1240
|
-
moduleBase: this.options.moduleBase,
|
|
1241
|
-
target: {
|
|
1242
|
-
kind: TargetKindSchema.parse(target.kind ?? this.selector.kind),
|
|
1243
|
-
name: typeof target.name === "string" ? target.name : null,
|
|
1244
|
-
rva: typeof target.rva === "string" ? target.rva : null,
|
|
1245
|
-
runtimeAddress: typeof target.runtimeAddress === "string"
|
|
1246
|
-
? target.runtimeAddress
|
|
1247
|
-
: null,
|
|
1248
|
-
},
|
|
1249
|
-
environment: this.environmentInfo.value,
|
|
1250
|
-
trigger: {
|
|
1251
|
-
workloadId: typeof trigger.workloadId === "string"
|
|
1252
|
-
? trigger.workloadId
|
|
1253
|
-
: this.plan.workloadId,
|
|
1254
|
-
phase: trigger.phase ?? this.phase,
|
|
1255
|
-
userAction: trigger.userAction ?? null,
|
|
1256
|
-
...trigger,
|
|
1257
|
-
},
|
|
1258
|
-
event: {
|
|
1259
|
-
kind,
|
|
1260
|
-
timestamp: new Date(this.clock.now()).toISOString(),
|
|
1261
|
-
threadId: typeof rawEvent.threadId === "number" ? rawEvent.threadId : null,
|
|
1262
|
-
depth: typeof rawEvent.depth === "number" ? rawEvent.depth : 0,
|
|
1263
|
-
invocationId: typeof rawEvent.invocationId === "string"
|
|
1264
|
-
? rawEvent.invocationId
|
|
1265
|
-
: null,
|
|
1266
|
-
parentInvocationId: typeof rawEvent.parentInvocationId === "string"
|
|
1267
|
-
? rawEvent.parentInvocationId
|
|
1268
|
-
: null,
|
|
1269
|
-
recursive: rawEvent.recursive === true,
|
|
1270
|
-
monotonicTimestamp: typeof rawEvent.monotonicTimestamp === "number"
|
|
1271
|
-
? rawEvent.monotonicTimestamp
|
|
1272
|
-
: this.clock.monotonicNow?.() ?? this.clock.now(),
|
|
1273
|
-
},
|
|
1274
|
-
arguments: input.arguments ?? null,
|
|
1275
|
-
returnValue: input.returnValue ?? null,
|
|
1276
|
-
before: input.before ?? null,
|
|
1277
|
-
after: input.after ?? null,
|
|
1278
|
-
diff: input.diff ?? null,
|
|
1279
|
-
callStack: Array.isArray(input.callStack) ? input.callStack : [],
|
|
1280
|
-
evidence: Array.isArray(input.evidence) ? input.evidence : [],
|
|
1281
|
-
status: input.status === "confirmed" ||
|
|
1282
|
-
input.status === "dynamic" ||
|
|
1283
|
-
input.status === "unresolved"
|
|
1284
|
-
? input.status
|
|
1285
|
-
: "partial",
|
|
1286
|
-
};
|
|
1287
|
-
}
|
|
1288
|
-
async writeManifest(extra = {}) {
|
|
1289
|
-
const fragment = { ...this.manifest, ...extra };
|
|
1290
|
-
if (this.options.manifestSink)
|
|
1291
|
-
await this.options.manifestSink.write(fragment);
|
|
1292
|
-
return fragment;
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
function phaseOf(trigger) {
|
|
1296
|
-
const phase = trigger.phase ?? "during";
|
|
1297
|
-
if (phase !== "before" && phase !== "during" && phase !== "after")
|
|
1298
|
-
throw new Error("TRIGGER_PHASE_INVALID");
|
|
1299
|
-
return phase;
|
|
1300
|
-
}
|
|
1301
|
-
function safeNonNegativeInteger(value) {
|
|
1302
|
-
return Number.isSafeInteger(value) && Number(value) >= 0 ? Number(value) : 0;
|
|
1303
|
-
}
|
|
1304
|
-
function isAbortError(value) {
|
|
1305
|
-
return !!value && typeof value === "object" &&
|
|
1306
|
-
("name" in value && value.name === "AbortError");
|
|
1307
|
-
}
|
|
1308
|
-
function isRecord(value) {
|
|
1309
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1310
|
-
}
|
|
1311
|
-
function stripSelectorMetadata(selector) {
|
|
1312
|
-
return {
|
|
1313
|
-
combine: selector.combine,
|
|
1314
|
-
...(selector.apis?.length ? { apis: selector.apis } : {}),
|
|
1315
|
-
...(selector.namespaces?.length || selector.namespace
|
|
1316
|
-
? {
|
|
1317
|
-
namespaces: [
|
|
1318
|
-
...(selector.namespaces ?? []),
|
|
1319
|
-
...(selector.namespace ? [selector.namespace] : []),
|
|
1320
|
-
],
|
|
1321
|
-
}
|
|
1322
|
-
: {}),
|
|
1323
|
-
...(selector.globs?.length || selector.glob
|
|
1324
|
-
? {
|
|
1325
|
-
globs: [
|
|
1326
|
-
...(selector.globs ?? []),
|
|
1327
|
-
...(selector.glob ? [selector.glob] : []),
|
|
1328
|
-
],
|
|
1329
|
-
}
|
|
1330
|
-
: {}),
|
|
1331
|
-
...(selector.rvas?.length || selector.rva
|
|
1332
|
-
? {
|
|
1333
|
-
rvas: [
|
|
1334
|
-
...(selector.rvas ?? []),
|
|
1335
|
-
...(selector.rva ? [selector.rva] : []),
|
|
1336
|
-
],
|
|
1337
|
-
}
|
|
1338
|
-
: {}),
|
|
1339
|
-
...(selector.dataSourceIds?.length
|
|
1340
|
-
? { dataSourceIds: selector.dataSourceIds }
|
|
1341
|
-
: {}),
|
|
1342
|
-
...(selector.objectIds?.length ? { objectIds: selector.objectIds } : {}),
|
|
1343
|
-
...(selector.ranges?.length ? { ranges: selector.ranges } : {}),
|
|
1344
|
-
maxTargets: selector.maxTargets,
|
|
1345
|
-
};
|
|
1346
|
-
}
|
|
1347
|
-
function selectorTargets(selector) {
|
|
1348
|
-
return [
|
|
1349
|
-
...(selector.apis ?? []).map((value) => ({ api: value })),
|
|
1350
|
-
...(selector.namespaces ?? []).map((value) => ({ namespace: value })),
|
|
1351
|
-
...(selector.globs ?? []).map((value) => ({ glob: value })),
|
|
1352
|
-
...(selector.rvas ?? []).map((value) => ({ rva: value })),
|
|
1353
|
-
...(selector.dataSourceIds ?? []).map((value) => ({ dataSourceId: value })),
|
|
1354
|
-
...(selector.objectIds ?? []).map((value) => ({ objectId: value })),
|
|
1355
|
-
...(selector.ranges ?? []).map((value) => ({ ...value })),
|
|
1356
|
-
];
|
|
85
|
+
throw new Error("TARGET_IDENTITY_CHANGED");
|
|
86
|
+
} }
|
|
87
|
+
async writeManifest(extra = {}) { if (this.options.manifestSink)
|
|
88
|
+
await this.options.manifestSink.write({ ...this.manifest, ...extra, manifestHash: createHash("sha256").update(JSON.stringify(this.manifest)).digest("hex") }); }
|
|
1357
89
|
}
|