100 lines
3.3 KiB
TypeScript
100 lines
3.3 KiB
TypeScript
import type { IPty } from "@lydell/node-pty";
|
|
|
|
import type {
|
|
InteractionEvent,
|
|
ProcessCaptureEventJournalEntry,
|
|
ProcessScenario,
|
|
RecordProcessCaptureEvent,
|
|
} from "../domain/processCapture.js";
|
|
import {
|
|
normalizeProcessElapsedTime,
|
|
normalizeProcessText,
|
|
} from "./ProcessNormalization.js";
|
|
import type { TerminalRenderer } from "./TerminalRenderer.js";
|
|
|
|
/** Mutable observation-order ledger whose writer is shared by capture producers. */
|
|
export interface ProcessCaptureJournal {
|
|
readonly entries: readonly ProcessCaptureEventJournalEntry[];
|
|
readonly recordEvent: RecordProcessCaptureEvent;
|
|
}
|
|
|
|
/** Create one monotonic event journal for a process capture. */
|
|
export const createProcessCaptureJournal = (): ProcessCaptureJournal => {
|
|
const entries: ProcessCaptureEventJournalEntry[] = [];
|
|
return {
|
|
entries,
|
|
recordEvent: (collection, index) => {
|
|
entries.push({ capture_order: entries.length, collection, index });
|
|
},
|
|
};
|
|
};
|
|
|
|
interface ScenarioInteractionOptions {
|
|
readonly scenario: ProcessScenario;
|
|
readonly getTerminal: () => IPty | undefined;
|
|
readonly timers: Set<NodeJS.Timeout>;
|
|
readonly interactions: InteractionEvent[];
|
|
readonly renderer: TerminalRenderer;
|
|
readonly started: number;
|
|
readonly dispatchedEventIndexes: Set<number>;
|
|
readonly recordEvent: RecordProcessCaptureEvent;
|
|
}
|
|
|
|
/** Schedule scenario interactions and record each observed dispatch atomically. */
|
|
export const scheduleScenarioInteractions = (
|
|
options: ScenarioInteractionOptions,
|
|
): void => {
|
|
const { scenario, getTerminal, timers, interactions, renderer, started } =
|
|
options;
|
|
for (const [eventIndex, event] of scenario.events.entries()) {
|
|
const timer = setTimeout(() => {
|
|
options.dispatchedEventIndexes.add(eventIndex);
|
|
const terminal = getTerminal();
|
|
const dispatchedAt = Math.max(0, Date.now() - started);
|
|
let outcome: InteractionEvent["outcome"] = "dispatched";
|
|
if (terminal === undefined) outcome = "target_exited";
|
|
else {
|
|
try {
|
|
if (event.type === "input") terminal.write(event.data);
|
|
else if (event.type === "resize") {
|
|
terminal.resize(event.columns, event.rows);
|
|
renderer.resize(
|
|
event.columns,
|
|
event.rows,
|
|
normalizeProcessElapsedTime(
|
|
dispatchedAt,
|
|
scenario.normalization.time_bucket_ms,
|
|
),
|
|
);
|
|
} else terminal.kill(event.signal);
|
|
} catch {
|
|
outcome = "failed";
|
|
}
|
|
}
|
|
const sequence = interactions.length;
|
|
interactions.push({
|
|
sequence,
|
|
scheduled_at_ms: event.at_ms,
|
|
dispatched_at_ms: dispatchedAt,
|
|
type: event.type,
|
|
data:
|
|
event.type === "input"
|
|
? event.sensitive
|
|
? `<redacted-input:${String(Buffer.byteLength(event.data))}-bytes>`
|
|
: normalizeProcessText(
|
|
event.data,
|
|
scenario,
|
|
"<no-temporary-root>",
|
|
-1,
|
|
)
|
|
: event.type === "resize"
|
|
? `${String(event.columns)}x${String(event.rows)}`
|
|
: event.signal,
|
|
outcome,
|
|
});
|
|
options.recordEvent("interaction_events", sequence);
|
|
}, event.at_ms);
|
|
timers.add(timer);
|
|
}
|
|
};
|