@anthonyfree96/core 0.1.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/LICENSE +21 -0
- package/dist/index.d.ts +303 -0
- package/dist/index.js +722 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 comm-scope AndyFree96
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { Writable } from 'node:stream';
|
|
2
|
+
|
|
3
|
+
/** Direction of a piece of traffic relative to the monitored peer. */
|
|
4
|
+
type Direction = 'rx' | 'tx';
|
|
5
|
+
/** The concrete kind of transport a session is bound to. */
|
|
6
|
+
type TransportKind = 'serial' | 'tcp-client' | 'tcp-server' | 'udp' | 'udp-listen';
|
|
7
|
+
/** Static metadata describing the endpoint a session is attached to. */
|
|
8
|
+
interface TransportMeta {
|
|
9
|
+
kind: TransportKind;
|
|
10
|
+
/** Stable identifier for this endpoint within a session (e.g. a peer address). */
|
|
11
|
+
id: string;
|
|
12
|
+
/** Human-readable, reversible descriptor (e.g. `serial:COM3@115200`). */
|
|
13
|
+
desc: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A single unit of traffic observed (or replayed) by a {@link DataSource}.
|
|
17
|
+
* This is the lingua franca between every source and every sink.
|
|
18
|
+
*/
|
|
19
|
+
interface TrafficEvent {
|
|
20
|
+
/** Epoch milliseconds. */
|
|
21
|
+
ts: number;
|
|
22
|
+
dir: Direction;
|
|
23
|
+
data: Buffer;
|
|
24
|
+
transport: TransportMeta;
|
|
25
|
+
}
|
|
26
|
+
/** Lifecycle events emitted by a {@link DataSource}. */
|
|
27
|
+
interface SourceEventMap {
|
|
28
|
+
open: {
|
|
29
|
+
source: unknown;
|
|
30
|
+
meta: TransportMeta;
|
|
31
|
+
};
|
|
32
|
+
close: {
|
|
33
|
+
source: unknown;
|
|
34
|
+
meta: TransportMeta;
|
|
35
|
+
};
|
|
36
|
+
error: {
|
|
37
|
+
source: unknown;
|
|
38
|
+
error: Error;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Typed event dispatcher that satisfies the `on`/`off` surface required by
|
|
44
|
+
* {@link DataSource}. Live transports extend this and simply `emit('data', e)`
|
|
45
|
+
* when bytes arrive.
|
|
46
|
+
*/
|
|
47
|
+
declare class TrafficBus {
|
|
48
|
+
private readonly emitter;
|
|
49
|
+
on(event: 'data', fn: (e: TrafficEvent) => void): void;
|
|
50
|
+
on<K extends keyof SourceEventMap>(event: K, fn: (e: SourceEventMap[K]) => void): void;
|
|
51
|
+
off(event: string, fn: (...args: any[]) => void): void;
|
|
52
|
+
protected emitData(e: TrafficEvent): void;
|
|
53
|
+
protected emit<K extends keyof SourceEventMap>(event: K, payload: SourceEventMap[K]): void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A stream of {@link TrafficEvent}s. Implemented by both live transports
|
|
58
|
+
* (Serial/TCP/UDP) and offline sources (a recording file being played back),
|
|
59
|
+
* so the presentation and sink layers never need to know which they have.
|
|
60
|
+
*/
|
|
61
|
+
interface DataSource {
|
|
62
|
+
/** Begin producing events. */
|
|
63
|
+
start(): Promise<void>;
|
|
64
|
+
/** Stop producing events and release resources. */
|
|
65
|
+
stop(): Promise<void>;
|
|
66
|
+
/** Subscribe to traffic. */
|
|
67
|
+
on(event: 'data', fn: (e: TrafficEvent) => void): void;
|
|
68
|
+
/** Subscribe to a lifecycle event. */
|
|
69
|
+
on<K extends keyof SourceEventMap>(event: K, fn: (e: SourceEventMap[K]) => void): void;
|
|
70
|
+
/** Unsubscribe a previously registered listener. */
|
|
71
|
+
off(event: string, fn: (...args: any[]) => void): void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A live transport: a {@link DataSource} that additionally allows writing
|
|
76
|
+
* bytes to the peer (used by the online-replay path to re-send recorded data).
|
|
77
|
+
*/
|
|
78
|
+
interface Transport extends DataSource {
|
|
79
|
+
/** Write bytes to the peer. Resolves once handed to the underlying channel. */
|
|
80
|
+
send(data: Buffer): Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A consumer of the traffic stream. Sinks are composable: a single session can
|
|
85
|
+
* feed a live renderer, a recorder and an analyzer simultaneously, and future
|
|
86
|
+
* GUI frontends become just another sink implementation.
|
|
87
|
+
*/
|
|
88
|
+
interface Sink {
|
|
89
|
+
/** Called for every event that passes any upstream filter. */
|
|
90
|
+
onEvent(e: TrafficEvent): void;
|
|
91
|
+
/** Called once when the source stops or the session ends. */
|
|
92
|
+
onClose(): void;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Formatting helpers shared by the CLI renderers (and reusable by a future GUI).
|
|
97
|
+
* Everything here is pure: `Buffer` in, string out.
|
|
98
|
+
*/
|
|
99
|
+
/** Lowercase hex of every byte, space-separated. */
|
|
100
|
+
declare function toHex(buf: Buffer): string;
|
|
101
|
+
/** Printable ASCII where possible, `.` otherwise. */
|
|
102
|
+
declare function toAscii(buf: Buffer): string;
|
|
103
|
+
interface HexDumpLine {
|
|
104
|
+
/** 8-digit zero-padded hex offset. */
|
|
105
|
+
offset: string;
|
|
106
|
+
hex: string;
|
|
107
|
+
ascii: string;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Classic `xxd`-style dump, grouped 8 bytes / 16 bytes per row, with an
|
|
111
|
+
* ASCII gutter. Returns one object per row.
|
|
112
|
+
*/
|
|
113
|
+
declare function hexdump(buf: Buffer, bytesPerRow?: number): HexDumpLine[];
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Parsed form of a transport spec string such as `tcp-listen:9000`.
|
|
117
|
+
* Reversible: {@link toDesc} regenerates the canonical spec.
|
|
118
|
+
*/
|
|
119
|
+
type TransportSpec = {
|
|
120
|
+
kind: 'serial';
|
|
121
|
+
port: string;
|
|
122
|
+
baud: number;
|
|
123
|
+
} | {
|
|
124
|
+
kind: 'tcp-client';
|
|
125
|
+
host: string;
|
|
126
|
+
port: number;
|
|
127
|
+
} | {
|
|
128
|
+
kind: 'tcp-server';
|
|
129
|
+
host: string;
|
|
130
|
+
port: number;
|
|
131
|
+
} | {
|
|
132
|
+
kind: 'udp';
|
|
133
|
+
host: string;
|
|
134
|
+
port: number;
|
|
135
|
+
} | {
|
|
136
|
+
kind: 'udp-listen';
|
|
137
|
+
host: string;
|
|
138
|
+
port: number;
|
|
139
|
+
};
|
|
140
|
+
/** Parse a transport spec string into its structured form. */
|
|
141
|
+
declare function parseSpec(spec: string): TransportSpec;
|
|
142
|
+
/** Canonical, reversible descriptor for a spec. */
|
|
143
|
+
declare function toDesc(spec: TransportSpec): string;
|
|
144
|
+
/** Construct the concrete {@link Transport} for a parsed spec. */
|
|
145
|
+
declare function createTransport(spec: TransportSpec): Transport;
|
|
146
|
+
|
|
147
|
+
declare const DEFAULT_BAUD = 115200;
|
|
148
|
+
/** Enumerate attached serial ports. */
|
|
149
|
+
declare function listSerialPorts(): Promise<{
|
|
150
|
+
path: string;
|
|
151
|
+
manufacturer?: string;
|
|
152
|
+
}[]>;
|
|
153
|
+
|
|
154
|
+
/** Parse a hex string (spaces and `0x` tolerated) into a Buffer. */
|
|
155
|
+
declare function hexToBuffer(s: string): Buffer;
|
|
156
|
+
interface FilterOptions {
|
|
157
|
+
/** Only pass events in this direction. */
|
|
158
|
+
dir?: Direction;
|
|
159
|
+
/** Byte-sequence match against the raw payload. */
|
|
160
|
+
hex?: Buffer;
|
|
161
|
+
/** Literal (UTF-8) substring match against the raw payload. */
|
|
162
|
+
string?: Buffer;
|
|
163
|
+
/** Regex matched against the UTF-8-decoded payload. */
|
|
164
|
+
regex?: RegExp;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Per-event predicate. All specified criteria must pass for an event to be
|
|
168
|
+
* forwarded to sinks. Note this matches within a single event; stream-level
|
|
169
|
+
* patterns that span event boundaries belong to the search/analysis layer.
|
|
170
|
+
*/
|
|
171
|
+
declare class Matcher {
|
|
172
|
+
private readonly opts;
|
|
173
|
+
constructor(opts: FilterOptions);
|
|
174
|
+
matches(e: TrafficEvent): boolean;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
interface SessionOptions {
|
|
178
|
+
filter?: Matcher;
|
|
179
|
+
sinks?: Sink[];
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Wires a {@link DataSource} to a set of sinks, applying an optional filter.
|
|
183
|
+
* This is the shared glue behind `monitor`, `view` and `replay`: the source is
|
|
184
|
+
* swapped (live transport vs recording file), the sink graph stays the same.
|
|
185
|
+
*/
|
|
186
|
+
declare class Session {
|
|
187
|
+
private readonly source;
|
|
188
|
+
private readonly opts;
|
|
189
|
+
private readonly sinks;
|
|
190
|
+
private finished;
|
|
191
|
+
constructor(source: DataSource, opts?: SessionOptions);
|
|
192
|
+
run(): Promise<void>;
|
|
193
|
+
private dispatch;
|
|
194
|
+
private finish;
|
|
195
|
+
stop(): Promise<void>;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* JSON Lines recording format.
|
|
200
|
+
*
|
|
201
|
+
* First line is a header carrying a magic key (`comm-scope`) plus the schema
|
|
202
|
+
* version and session metadata. Every following line is a single traffic
|
|
203
|
+
* event, payload encoded as lowercase hex so binary data round-trips cleanly
|
|
204
|
+
* through JSON without escaping concerns.
|
|
205
|
+
*/
|
|
206
|
+
declare const SCHEMA_VERSION = 1;
|
|
207
|
+
declare const MAGIC_KEY = "comm-scope";
|
|
208
|
+
interface SessionHeader {
|
|
209
|
+
version: number;
|
|
210
|
+
kind: TransportKind;
|
|
211
|
+
id: string;
|
|
212
|
+
desc: string;
|
|
213
|
+
started: number;
|
|
214
|
+
}
|
|
215
|
+
declare function encodeHeader(h: SessionHeader): string;
|
|
216
|
+
declare function decodeHeader(line: string): SessionHeader;
|
|
217
|
+
declare function encodeEvent(e: TrafficEvent): string;
|
|
218
|
+
declare function decodeEvent(line: string): TrafficEvent;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Writes traffic as JSON Lines. Owns the target stream: it writes the header
|
|
222
|
+
* immediately and ends the stream when the session closes.
|
|
223
|
+
*/
|
|
224
|
+
declare class JsonlRecorder implements Sink {
|
|
225
|
+
private readonly stream;
|
|
226
|
+
private readonly header;
|
|
227
|
+
constructor(stream: Writable, header: SessionHeader);
|
|
228
|
+
onEvent(e: TrafficEvent): void;
|
|
229
|
+
onClose(): void;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
interface FileSourceOptions {
|
|
233
|
+
/** Replay speed multiplier. 1 = real-time, 0 = as fast as possible. */
|
|
234
|
+
speed?: number;
|
|
235
|
+
/** Optional direction filter applied while reading. */
|
|
236
|
+
dir?: Direction;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Offline data source: reads a recording and re-emits its events at the
|
|
240
|
+
* original timing (scaled by `speed`). Swappable for any live transport, which
|
|
241
|
+
* is what makes `view` behave identically to `monitor`.
|
|
242
|
+
*/
|
|
243
|
+
declare class FileSource extends TrafficBus implements DataSource {
|
|
244
|
+
private readonly file;
|
|
245
|
+
private readonly opts;
|
|
246
|
+
private stopped;
|
|
247
|
+
private rl;
|
|
248
|
+
private readonly meta;
|
|
249
|
+
constructor(file: string, opts?: FileSourceOptions);
|
|
250
|
+
start(): Promise<void>;
|
|
251
|
+
private headerMeta;
|
|
252
|
+
stop(): Promise<void>;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
interface TrafficStats {
|
|
256
|
+
firstTs: number;
|
|
257
|
+
lastTs: number;
|
|
258
|
+
rxBytes: number;
|
|
259
|
+
txBytes: number;
|
|
260
|
+
rxPackets: number;
|
|
261
|
+
txPackets: number;
|
|
262
|
+
}
|
|
263
|
+
interface TrafficSummary extends TrafficStats {
|
|
264
|
+
durationMs: number;
|
|
265
|
+
rxRate: number;
|
|
266
|
+
txRate: number;
|
|
267
|
+
totalRate: number;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Accumulates byte/packet counters and rates. Attach alongside a renderer or
|
|
271
|
+
* recorder; call {@link summarize} when the session ends.
|
|
272
|
+
*/
|
|
273
|
+
declare class Analyzer implements Sink {
|
|
274
|
+
private firstTs;
|
|
275
|
+
private lastTs;
|
|
276
|
+
private rxBytes;
|
|
277
|
+
private txBytes;
|
|
278
|
+
private rxPackets;
|
|
279
|
+
private txPackets;
|
|
280
|
+
onEvent(e: TrafficEvent): void;
|
|
281
|
+
onClose(): void;
|
|
282
|
+
summarize(): TrafficSummary;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Read a recording's events into memory, optionally filtering by direction. */
|
|
286
|
+
declare function readEvents(file: string, dir?: Direction): Promise<TrafficEvent[]>;
|
|
287
|
+
interface ReplayOptions {
|
|
288
|
+
/** Replay speed multiplier. 1 = real-time, 0 = as fast as possible. */
|
|
289
|
+
speed?: number;
|
|
290
|
+
/** Only replay events in this direction. Defaults to all. */
|
|
291
|
+
dir?: Direction;
|
|
292
|
+
/** Re-send continuously until interrupted. */
|
|
293
|
+
loop?: boolean;
|
|
294
|
+
/** Progress callback, fired once per event sent. */
|
|
295
|
+
onProgress?: (sent: number, total: number) => void;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Re-send a recording's bytes to a live transport at the original timing.
|
|
299
|
+
* Resolves once finished (never, when `loop` is set — interrupt externally).
|
|
300
|
+
*/
|
|
301
|
+
declare function replayFileToTransport(file: string, target: Transport, opts?: ReplayOptions): Promise<number>;
|
|
302
|
+
|
|
303
|
+
export { Analyzer, DEFAULT_BAUD, type DataSource, type Direction, FileSource, type FileSourceOptions, type FilterOptions, type HexDumpLine, JsonlRecorder, MAGIC_KEY, Matcher, type ReplayOptions, SCHEMA_VERSION, Session, type SessionHeader, type SessionOptions, type Sink, type SourceEventMap, TrafficBus, type TrafficEvent, type TrafficStats, type TrafficSummary, type Transport, type TransportKind, type TransportMeta, type TransportSpec, createTransport, decodeEvent, decodeHeader, encodeEvent, encodeHeader, hexToBuffer, hexdump, listSerialPorts, parseSpec, readEvents, replayFileToTransport, toAscii, toDesc, toHex };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
// src/event/bus.ts
|
|
2
|
+
import { EventEmitter } from "events";
|
|
3
|
+
var TrafficBus = class {
|
|
4
|
+
emitter = new EventEmitter();
|
|
5
|
+
on(event, fn) {
|
|
6
|
+
this.emitter.on(event, fn);
|
|
7
|
+
}
|
|
8
|
+
off(event, fn) {
|
|
9
|
+
this.emitter.off(event, fn);
|
|
10
|
+
}
|
|
11
|
+
emitData(e) {
|
|
12
|
+
this.emitter.emit("data", e);
|
|
13
|
+
}
|
|
14
|
+
emit(event, payload) {
|
|
15
|
+
this.emitter.emit(event, payload);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// src/format/hexdump.ts
|
|
20
|
+
var HEX = "0123456789abcdef";
|
|
21
|
+
function hexByte(b) {
|
|
22
|
+
return HEX[b >> 4 & 15] + HEX[b & 15];
|
|
23
|
+
}
|
|
24
|
+
function toHex(buf) {
|
|
25
|
+
let out = "";
|
|
26
|
+
for (let i = 0; i < buf.length; i++) {
|
|
27
|
+
if (i > 0) out += " ";
|
|
28
|
+
out += hexByte(buf[i]);
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
function toAscii(buf) {
|
|
33
|
+
let out = "";
|
|
34
|
+
for (let i = 0; i < buf.length; i++) {
|
|
35
|
+
const c = buf[i];
|
|
36
|
+
out += c >= 32 && c <= 126 ? String.fromCharCode(c) : ".";
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
function hexdump(buf, bytesPerRow = 16) {
|
|
41
|
+
const lines = [];
|
|
42
|
+
for (let base = 0; base < buf.length; base += bytesPerRow) {
|
|
43
|
+
const row = buf.subarray(base, base + bytesPerRow);
|
|
44
|
+
const hexParts = [];
|
|
45
|
+
let ascii = "";
|
|
46
|
+
for (let i = 0; i < row.length; i++) {
|
|
47
|
+
hexParts.push(hexByte(row[i]));
|
|
48
|
+
if (i === 7) hexParts.push(" ");
|
|
49
|
+
ascii += toAscii(row.subarray(i, i + 1));
|
|
50
|
+
}
|
|
51
|
+
const targetHex = bytesPerRow * 2 + Math.floor(bytesPerRow / 8);
|
|
52
|
+
const hex = hexParts.join(" ").padEnd(targetHex, " ");
|
|
53
|
+
lines.push({
|
|
54
|
+
offset: base.toString(16).padStart(8, "0"),
|
|
55
|
+
hex,
|
|
56
|
+
ascii
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return lines;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/transport/tcp.ts
|
|
63
|
+
import net from "net";
|
|
64
|
+
var TcpClientTransport = class extends TrafficBus {
|
|
65
|
+
constructor(host, port) {
|
|
66
|
+
super();
|
|
67
|
+
this.host = host;
|
|
68
|
+
this.port = port;
|
|
69
|
+
this.meta = {
|
|
70
|
+
kind: "tcp-client",
|
|
71
|
+
id: `${host}:${port}`,
|
|
72
|
+
desc: `tcp:${host}:${port}`
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
host;
|
|
76
|
+
port;
|
|
77
|
+
socket;
|
|
78
|
+
meta;
|
|
79
|
+
async start() {
|
|
80
|
+
const socket = net.createConnection({ host: this.host, port: this.port });
|
|
81
|
+
this.socket = socket;
|
|
82
|
+
await new Promise((resolve, reject) => {
|
|
83
|
+
socket.once("connect", resolve);
|
|
84
|
+
socket.once("error", reject);
|
|
85
|
+
});
|
|
86
|
+
socket.on("data", (data) => this.onRx(data));
|
|
87
|
+
socket.on("close", () => this.emit("close", { source: this, meta: this.meta }));
|
|
88
|
+
socket.on("error", (err) => this.emit("error", { source: this, error: err }));
|
|
89
|
+
this.emit("open", { source: this, meta: this.meta });
|
|
90
|
+
}
|
|
91
|
+
onRx(data) {
|
|
92
|
+
const e = { ts: Date.now(), dir: "rx", data, transport: this.meta };
|
|
93
|
+
this.emitData(e);
|
|
94
|
+
}
|
|
95
|
+
async send(data) {
|
|
96
|
+
const socket = this.socket;
|
|
97
|
+
if (!socket) throw new Error("tcp-client: not connected");
|
|
98
|
+
await new Promise((resolve, reject) => {
|
|
99
|
+
socket.write(data, (err) => err ? reject(err) : resolve());
|
|
100
|
+
});
|
|
101
|
+
const e = { ts: Date.now(), dir: "tx", data, transport: this.meta };
|
|
102
|
+
this.emitData(e);
|
|
103
|
+
}
|
|
104
|
+
async stop() {
|
|
105
|
+
this.socket?.destroy();
|
|
106
|
+
this.socket = void 0;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
var TcpServerTransport = class extends TrafficBus {
|
|
110
|
+
constructor(host, port) {
|
|
111
|
+
super();
|
|
112
|
+
this.host = host;
|
|
113
|
+
this.port = port;
|
|
114
|
+
}
|
|
115
|
+
host;
|
|
116
|
+
port;
|
|
117
|
+
server;
|
|
118
|
+
sockets = /* @__PURE__ */ new Map();
|
|
119
|
+
async start() {
|
|
120
|
+
await new Promise((resolve, reject) => {
|
|
121
|
+
const server = net.createServer((socket) => this.onConnection(socket));
|
|
122
|
+
server.once("error", reject);
|
|
123
|
+
server.listen(this.port, this.host || void 0, () => {
|
|
124
|
+
server.off("error", reject);
|
|
125
|
+
resolve();
|
|
126
|
+
});
|
|
127
|
+
this.server = server;
|
|
128
|
+
});
|
|
129
|
+
const meta = {
|
|
130
|
+
kind: "tcp-server",
|
|
131
|
+
id: `${this.host || "0.0.0.0"}:${this.port}`,
|
|
132
|
+
desc: `tcp-listen:${this.host ? `${this.host}:` : ""}${this.port}`
|
|
133
|
+
};
|
|
134
|
+
this.emit("open", { source: this, meta });
|
|
135
|
+
}
|
|
136
|
+
onConnection(socket) {
|
|
137
|
+
const remote = `${socket.remoteAddress ?? "?"}:${socket.remotePort ?? "?"}`;
|
|
138
|
+
const meta = {
|
|
139
|
+
kind: "tcp-server",
|
|
140
|
+
id: remote,
|
|
141
|
+
desc: `tcp-listen:${this.port}#${remote}`
|
|
142
|
+
};
|
|
143
|
+
this.sockets.set(remote, socket);
|
|
144
|
+
socket.on("data", (data) => {
|
|
145
|
+
const e = { ts: Date.now(), dir: "rx", data, transport: meta };
|
|
146
|
+
this.emitData(e);
|
|
147
|
+
});
|
|
148
|
+
socket.on("close", () => this.sockets.delete(remote));
|
|
149
|
+
socket.on("error", (err) => this.emit("error", { source: this, error: err }));
|
|
150
|
+
}
|
|
151
|
+
async send(data) {
|
|
152
|
+
const writes = [];
|
|
153
|
+
for (const socket of this.sockets.values()) {
|
|
154
|
+
writes.push(
|
|
155
|
+
new Promise((resolve, reject) => {
|
|
156
|
+
socket.write(data, (err) => err ? reject(err) : resolve());
|
|
157
|
+
})
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
await Promise.all(writes);
|
|
161
|
+
}
|
|
162
|
+
async stop() {
|
|
163
|
+
for (const socket of this.sockets.values()) socket.destroy();
|
|
164
|
+
this.sockets.clear();
|
|
165
|
+
await new Promise((resolve) => {
|
|
166
|
+
if (!this.server) return resolve();
|
|
167
|
+
this.server.close(() => resolve());
|
|
168
|
+
});
|
|
169
|
+
this.server = void 0;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// src/transport/udp.ts
|
|
174
|
+
import dgram from "dgram";
|
|
175
|
+
function udpTypeFor(host) {
|
|
176
|
+
return host.includes(":") ? "udp6" : "udp4";
|
|
177
|
+
}
|
|
178
|
+
var UdpTransport = class extends TrafficBus {
|
|
179
|
+
constructor(host, port) {
|
|
180
|
+
super();
|
|
181
|
+
this.host = host;
|
|
182
|
+
this.port = port;
|
|
183
|
+
this.meta = { kind: "udp", id: `${host}:${port}`, desc: `udp:${host}:${port}` };
|
|
184
|
+
}
|
|
185
|
+
host;
|
|
186
|
+
port;
|
|
187
|
+
socket;
|
|
188
|
+
meta;
|
|
189
|
+
async start() {
|
|
190
|
+
const socket = dgram.createSocket(udpTypeFor(this.host));
|
|
191
|
+
this.socket = socket;
|
|
192
|
+
await new Promise((resolve, reject) => {
|
|
193
|
+
socket.once("error", reject);
|
|
194
|
+
socket.bind(0, () => {
|
|
195
|
+
socket.off("error", reject);
|
|
196
|
+
resolve();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
socket.on("message", (msg, rinfo) => {
|
|
200
|
+
if (rinfo.address !== this.host || rinfo.port !== this.port) return;
|
|
201
|
+
const e = { ts: Date.now(), dir: "rx", data: msg, transport: this.meta };
|
|
202
|
+
this.emitData(e);
|
|
203
|
+
});
|
|
204
|
+
socket.on("error", (err) => this.emit("error", { source: this, error: err }));
|
|
205
|
+
this.emit("open", { source: this, meta: this.meta });
|
|
206
|
+
}
|
|
207
|
+
async send(data) {
|
|
208
|
+
const socket = this.socket;
|
|
209
|
+
if (!socket) throw new Error("udp: not bound");
|
|
210
|
+
await new Promise((resolve, reject) => {
|
|
211
|
+
socket.send(data, this.port, this.host, (err) => err ? reject(err) : resolve());
|
|
212
|
+
});
|
|
213
|
+
const e = { ts: Date.now(), dir: "tx", data, transport: this.meta };
|
|
214
|
+
this.emitData(e);
|
|
215
|
+
}
|
|
216
|
+
async stop() {
|
|
217
|
+
await new Promise((resolve) => {
|
|
218
|
+
if (!this.socket) return resolve();
|
|
219
|
+
this.socket.close(() => resolve());
|
|
220
|
+
});
|
|
221
|
+
this.socket = void 0;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
var UdpListenTransport = class extends TrafficBus {
|
|
225
|
+
constructor(host, port) {
|
|
226
|
+
super();
|
|
227
|
+
this.host = host;
|
|
228
|
+
this.port = port;
|
|
229
|
+
const label = `${this.host || "0.0.0.0"}:${this.port}`;
|
|
230
|
+
this.meta = { kind: "udp-listen", id: label, desc: `udp-listen:${label}` };
|
|
231
|
+
}
|
|
232
|
+
host;
|
|
233
|
+
port;
|
|
234
|
+
socket;
|
|
235
|
+
lastPeer;
|
|
236
|
+
meta;
|
|
237
|
+
async start() {
|
|
238
|
+
const socket = dgram.createSocket(udpTypeFor(this.host));
|
|
239
|
+
this.socket = socket;
|
|
240
|
+
await new Promise((resolve, reject) => {
|
|
241
|
+
socket.once("error", reject);
|
|
242
|
+
socket.bind(this.port, this.host || void 0, () => {
|
|
243
|
+
socket.off("error", reject);
|
|
244
|
+
resolve();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
socket.on("message", (msg, rinfo) => {
|
|
248
|
+
this.lastPeer = { address: rinfo.address, port: rinfo.port };
|
|
249
|
+
const meta = {
|
|
250
|
+
kind: "udp-listen",
|
|
251
|
+
id: `${rinfo.address}:${rinfo.port}`,
|
|
252
|
+
desc: this.meta.desc
|
|
253
|
+
};
|
|
254
|
+
const e = { ts: Date.now(), dir: "rx", data: msg, transport: meta };
|
|
255
|
+
this.emitData(e);
|
|
256
|
+
});
|
|
257
|
+
socket.on("error", (err) => this.emit("error", { source: this, error: err }));
|
|
258
|
+
this.emit("open", { source: this, meta: this.meta });
|
|
259
|
+
}
|
|
260
|
+
async send(data) {
|
|
261
|
+
const socket = this.socket;
|
|
262
|
+
const peer = this.lastPeer;
|
|
263
|
+
if (!socket) throw new Error("udp-listen: not bound");
|
|
264
|
+
if (!peer) throw new Error("udp-listen: no peer has sent yet");
|
|
265
|
+
await new Promise((resolve, reject) => {
|
|
266
|
+
socket.send(data, peer.port, peer.address, (err) => err ? reject(err) : resolve());
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
async stop() {
|
|
270
|
+
await new Promise((resolve) => {
|
|
271
|
+
if (!this.socket) return resolve();
|
|
272
|
+
this.socket.close(() => resolve());
|
|
273
|
+
});
|
|
274
|
+
this.socket = void 0;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// src/transport/serial.ts
|
|
279
|
+
import { SerialPort } from "serialport";
|
|
280
|
+
var DEFAULT_BAUD = 115200;
|
|
281
|
+
var SerialTransport = class extends TrafficBus {
|
|
282
|
+
constructor(path, baud) {
|
|
283
|
+
super();
|
|
284
|
+
this.path = path;
|
|
285
|
+
this.baud = baud;
|
|
286
|
+
this.meta = { kind: "serial", id: path, desc: `serial:${path}:${baud}` };
|
|
287
|
+
}
|
|
288
|
+
path;
|
|
289
|
+
baud;
|
|
290
|
+
port;
|
|
291
|
+
meta;
|
|
292
|
+
async start() {
|
|
293
|
+
const port = new SerialPort({ path: this.path, baudRate: this.baud });
|
|
294
|
+
this.port = port;
|
|
295
|
+
await new Promise((resolve, reject) => {
|
|
296
|
+
port.once("open", resolve);
|
|
297
|
+
port.once("error", reject);
|
|
298
|
+
});
|
|
299
|
+
port.on("data", (data) => {
|
|
300
|
+
const e = { ts: Date.now(), dir: "rx", data, transport: this.meta };
|
|
301
|
+
this.emitData(e);
|
|
302
|
+
});
|
|
303
|
+
port.on("close", () => this.emit("close", { source: this, meta: this.meta }));
|
|
304
|
+
port.on("error", (err) => this.emit("error", { source: this, error: err }));
|
|
305
|
+
this.emit("open", { source: this, meta: this.meta });
|
|
306
|
+
}
|
|
307
|
+
async send(data) {
|
|
308
|
+
if (!this.port) throw new Error("serial: not open");
|
|
309
|
+
await new Promise((resolve, reject) => {
|
|
310
|
+
this.port.write(data, (err) => err ? reject(err) : resolve());
|
|
311
|
+
});
|
|
312
|
+
const e = { ts: Date.now(), dir: "tx", data, transport: this.meta };
|
|
313
|
+
this.emitData(e);
|
|
314
|
+
}
|
|
315
|
+
async stop() {
|
|
316
|
+
if (!this.port) return;
|
|
317
|
+
const port = this.port;
|
|
318
|
+
this.port = void 0;
|
|
319
|
+
await new Promise((resolve) => port.close(() => resolve()));
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
async function listSerialPorts() {
|
|
323
|
+
const ports = await SerialPort.list();
|
|
324
|
+
return ports.map((p) => ({ path: p.path, manufacturer: p.manufacturer }));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/transport/factory.ts
|
|
328
|
+
function parsePort(s) {
|
|
329
|
+
if (!/^\d+$/.test(s)) throw new Error(`invalid port "${s}"`);
|
|
330
|
+
const p = Number(s);
|
|
331
|
+
if (p < 1 || p > 65535) throw new Error(`port out of range "${s}"`);
|
|
332
|
+
return p;
|
|
333
|
+
}
|
|
334
|
+
function parseBaud(s) {
|
|
335
|
+
if (!/^\d+$/.test(s)) throw new Error(`invalid baud "${s}"`);
|
|
336
|
+
const b = Number(s);
|
|
337
|
+
if (b < 1) throw new Error(`invalid baud "${s}"`);
|
|
338
|
+
return b;
|
|
339
|
+
}
|
|
340
|
+
function splitHostPort(s) {
|
|
341
|
+
let host;
|
|
342
|
+
let portStr;
|
|
343
|
+
if (s.startsWith("[")) {
|
|
344
|
+
const close = s.indexOf("]");
|
|
345
|
+
if (close < 0) throw new Error(`invalid host:port "${s}"`);
|
|
346
|
+
host = s.slice(1, close);
|
|
347
|
+
portStr = s.slice(close + 1);
|
|
348
|
+
if (!portStr.startsWith(":")) throw new Error(`invalid host:port "${s}"`);
|
|
349
|
+
portStr = portStr.slice(1);
|
|
350
|
+
} else {
|
|
351
|
+
const i = s.lastIndexOf(":");
|
|
352
|
+
if (i < 0) throw new Error(`missing port in "${s}"`);
|
|
353
|
+
host = s.slice(0, i);
|
|
354
|
+
portStr = s.slice(i + 1);
|
|
355
|
+
}
|
|
356
|
+
return { host, port: parsePort(portStr) };
|
|
357
|
+
}
|
|
358
|
+
function splitListen(s) {
|
|
359
|
+
if (!s.includes(":")) return { host: "", port: parsePort(s) };
|
|
360
|
+
return splitHostPort(s);
|
|
361
|
+
}
|
|
362
|
+
function splitSerial(s) {
|
|
363
|
+
const parts = s.split(":");
|
|
364
|
+
if (parts.length >= 2 && /^\d+$/.test(parts[1])) {
|
|
365
|
+
return { path: parts[0], baud: parseBaud(parts[1]) };
|
|
366
|
+
}
|
|
367
|
+
return { path: s, baud: DEFAULT_BAUD };
|
|
368
|
+
}
|
|
369
|
+
function parseSpec(spec) {
|
|
370
|
+
const idx = spec.indexOf(":");
|
|
371
|
+
if (idx < 0) throw new Error(`invalid transport spec "${spec}" (expected "scheme:...")`);
|
|
372
|
+
const scheme = spec.slice(0, idx);
|
|
373
|
+
const rest = spec.slice(idx + 1);
|
|
374
|
+
switch (scheme) {
|
|
375
|
+
case "serial": {
|
|
376
|
+
const { path, baud } = splitSerial(rest);
|
|
377
|
+
return { kind: "serial", port: path, baud };
|
|
378
|
+
}
|
|
379
|
+
case "tcp": {
|
|
380
|
+
const { host, port } = splitHostPort(rest);
|
|
381
|
+
return { kind: "tcp-client", host, port };
|
|
382
|
+
}
|
|
383
|
+
case "tcp-listen": {
|
|
384
|
+
const { host, port } = splitListen(rest);
|
|
385
|
+
return { kind: "tcp-server", host, port };
|
|
386
|
+
}
|
|
387
|
+
case "udp": {
|
|
388
|
+
const { host, port } = splitHostPort(rest);
|
|
389
|
+
return { kind: "udp", host, port };
|
|
390
|
+
}
|
|
391
|
+
case "udp-listen": {
|
|
392
|
+
const { host, port } = splitListen(rest);
|
|
393
|
+
return { kind: "udp-listen", host, port };
|
|
394
|
+
}
|
|
395
|
+
default:
|
|
396
|
+
throw new Error(`unknown transport scheme "${scheme}"`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function toDesc(spec) {
|
|
400
|
+
switch (spec.kind) {
|
|
401
|
+
case "serial":
|
|
402
|
+
return `serial:${spec.port}:${spec.baud}`;
|
|
403
|
+
case "tcp-client":
|
|
404
|
+
return `tcp:${spec.host}:${spec.port}`;
|
|
405
|
+
case "tcp-server":
|
|
406
|
+
return `tcp-listen:${spec.host ? `${spec.host}:` : ""}${spec.port}`;
|
|
407
|
+
case "udp":
|
|
408
|
+
return `udp:${spec.host}:${spec.port}`;
|
|
409
|
+
case "udp-listen":
|
|
410
|
+
return `udp-listen:${spec.host ? `${spec.host}:` : ""}${spec.port}`;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function createTransport(spec) {
|
|
414
|
+
switch (spec.kind) {
|
|
415
|
+
case "serial":
|
|
416
|
+
return new SerialTransport(spec.port, spec.baud);
|
|
417
|
+
case "tcp-client":
|
|
418
|
+
return new TcpClientTransport(spec.host, spec.port);
|
|
419
|
+
case "tcp-server":
|
|
420
|
+
return new TcpServerTransport(spec.host, spec.port);
|
|
421
|
+
case "udp":
|
|
422
|
+
return new UdpTransport(spec.host, spec.port);
|
|
423
|
+
case "udp-listen":
|
|
424
|
+
return new UdpListenTransport(spec.host, spec.port);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/sink/filter.ts
|
|
429
|
+
function hexToBuffer(s) {
|
|
430
|
+
const clean = s.replace(/0x/gi, "").replace(/[\s,]/g, "");
|
|
431
|
+
if (clean.length % 2 !== 0) throw new Error(`hex string has odd length: "${s}"`);
|
|
432
|
+
if (!/^[0-9a-f]*$/i.test(clean)) throw new Error(`invalid hex string: "${s}"`);
|
|
433
|
+
const buf = Buffer.alloc(clean.length / 2);
|
|
434
|
+
for (let i = 0; i < buf.length; i++) buf[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
435
|
+
return buf;
|
|
436
|
+
}
|
|
437
|
+
var Matcher = class {
|
|
438
|
+
constructor(opts) {
|
|
439
|
+
this.opts = opts;
|
|
440
|
+
}
|
|
441
|
+
opts;
|
|
442
|
+
matches(e) {
|
|
443
|
+
if (this.opts.dir && e.dir !== this.opts.dir) return false;
|
|
444
|
+
if (this.opts.hex && !e.data.includes(this.opts.hex)) return false;
|
|
445
|
+
if (this.opts.string && !e.data.includes(this.opts.string)) return false;
|
|
446
|
+
if (this.opts.regex && !this.opts.regex.test(e.data.toString("utf8"))) return false;
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
// src/session.ts
|
|
452
|
+
var Session = class {
|
|
453
|
+
constructor(source, opts = {}) {
|
|
454
|
+
this.source = source;
|
|
455
|
+
this.opts = opts;
|
|
456
|
+
this.sinks = opts.sinks ?? [];
|
|
457
|
+
}
|
|
458
|
+
source;
|
|
459
|
+
opts;
|
|
460
|
+
sinks;
|
|
461
|
+
finished = false;
|
|
462
|
+
async run() {
|
|
463
|
+
this.source.on("data", (e) => this.dispatch(e));
|
|
464
|
+
this.source.on("close", () => this.finish());
|
|
465
|
+
await this.source.start();
|
|
466
|
+
}
|
|
467
|
+
dispatch(e) {
|
|
468
|
+
if (this.opts.filter && !this.opts.filter.matches(e)) return;
|
|
469
|
+
for (const sink of this.sinks) sink.onEvent(e);
|
|
470
|
+
}
|
|
471
|
+
finish() {
|
|
472
|
+
if (this.finished) return;
|
|
473
|
+
this.finished = true;
|
|
474
|
+
for (const sink of this.sinks) sink.onClose();
|
|
475
|
+
}
|
|
476
|
+
async stop() {
|
|
477
|
+
await this.source.stop();
|
|
478
|
+
this.finish();
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
// src/format/jsonl.ts
|
|
483
|
+
var SCHEMA_VERSION = 1;
|
|
484
|
+
var MAGIC_KEY = "comm-scope";
|
|
485
|
+
function encodeHeader(h) {
|
|
486
|
+
return JSON.stringify({
|
|
487
|
+
[MAGIC_KEY]: h.version,
|
|
488
|
+
kind: h.kind,
|
|
489
|
+
id: h.id,
|
|
490
|
+
desc: h.desc,
|
|
491
|
+
started: h.started
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
function decodeHeader(line) {
|
|
495
|
+
const o = JSON.parse(line);
|
|
496
|
+
const version = o[MAGIC_KEY];
|
|
497
|
+
if (typeof version !== "number") {
|
|
498
|
+
throw new Error("not a comm-scope recording (missing header)");
|
|
499
|
+
}
|
|
500
|
+
if (version !== SCHEMA_VERSION) {
|
|
501
|
+
throw new Error(`unsupported recording schema version ${version}`);
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
version,
|
|
505
|
+
kind: o.kind,
|
|
506
|
+
id: String(o.id ?? ""),
|
|
507
|
+
desc: String(o.desc ?? ""),
|
|
508
|
+
started: Number(o.started ?? 0)
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function encodeEvent(e) {
|
|
512
|
+
return JSON.stringify({
|
|
513
|
+
t: e.ts,
|
|
514
|
+
dir: e.dir,
|
|
515
|
+
kind: e.transport.kind,
|
|
516
|
+
id: e.transport.id,
|
|
517
|
+
desc: e.transport.desc,
|
|
518
|
+
enc: "hex",
|
|
519
|
+
data: e.data.toString("hex")
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
function decodeEvent(line) {
|
|
523
|
+
const o = JSON.parse(line);
|
|
524
|
+
if (o.enc !== "hex") throw new Error(`unsupported payload encoding "${String(o.enc)}"`);
|
|
525
|
+
const dir = o.dir;
|
|
526
|
+
if (dir !== "rx" && dir !== "tx") throw new Error(`invalid direction "${String(o.dir)}"`);
|
|
527
|
+
return {
|
|
528
|
+
ts: Number(o.t),
|
|
529
|
+
dir,
|
|
530
|
+
data: Buffer.from(String(o.data), "hex"),
|
|
531
|
+
transport: {
|
|
532
|
+
kind: o.kind,
|
|
533
|
+
id: String(o.id ?? ""),
|
|
534
|
+
desc: String(o.desc ?? "")
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/sink/recorder.ts
|
|
540
|
+
var JsonlRecorder = class {
|
|
541
|
+
constructor(stream, header) {
|
|
542
|
+
this.stream = stream;
|
|
543
|
+
this.header = header;
|
|
544
|
+
this.stream.write(encodeHeader(header) + "\n");
|
|
545
|
+
}
|
|
546
|
+
stream;
|
|
547
|
+
header;
|
|
548
|
+
onEvent(e) {
|
|
549
|
+
this.stream.write(encodeEvent(e) + "\n");
|
|
550
|
+
}
|
|
551
|
+
onClose() {
|
|
552
|
+
this.stream.end();
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
// src/source/file-source.ts
|
|
557
|
+
import fs from "fs";
|
|
558
|
+
import readline from "readline";
|
|
559
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
560
|
+
var FileSource = class extends TrafficBus {
|
|
561
|
+
constructor(file, opts = {}) {
|
|
562
|
+
super();
|
|
563
|
+
this.file = file;
|
|
564
|
+
this.opts = opts;
|
|
565
|
+
this.meta = { kind: "serial", id: file, desc: file };
|
|
566
|
+
}
|
|
567
|
+
file;
|
|
568
|
+
opts;
|
|
569
|
+
stopped = false;
|
|
570
|
+
rl;
|
|
571
|
+
meta;
|
|
572
|
+
async start() {
|
|
573
|
+
this.stopped = false;
|
|
574
|
+
const rl = readline.createInterface({
|
|
575
|
+
input: fs.createReadStream(this.file),
|
|
576
|
+
crlfDelay: Infinity
|
|
577
|
+
});
|
|
578
|
+
this.rl = rl;
|
|
579
|
+
const speed = this.opts.speed ?? 1;
|
|
580
|
+
let prevT;
|
|
581
|
+
let header;
|
|
582
|
+
try {
|
|
583
|
+
for await (const line of rl) {
|
|
584
|
+
if (this.stopped) break;
|
|
585
|
+
if (header === void 0) {
|
|
586
|
+
header = decodeHeader(line);
|
|
587
|
+
this.emit("open", { source: this, meta: this.headerMeta(header) });
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
const e = decodeEvent(line);
|
|
591
|
+
if (this.opts.dir && e.dir !== this.opts.dir) continue;
|
|
592
|
+
if (prevT !== void 0 && speed > 0) {
|
|
593
|
+
const wait = (e.ts - prevT) / speed;
|
|
594
|
+
if (wait > 0) await sleep(wait);
|
|
595
|
+
}
|
|
596
|
+
prevT = e.ts;
|
|
597
|
+
this.emitData(e);
|
|
598
|
+
}
|
|
599
|
+
} finally {
|
|
600
|
+
if (!this.stopped) {
|
|
601
|
+
this.emit("close", { source: this, meta: this.meta });
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
headerMeta(h) {
|
|
606
|
+
return { kind: h.kind, id: h.id, desc: h.desc };
|
|
607
|
+
}
|
|
608
|
+
async stop() {
|
|
609
|
+
this.stopped = true;
|
|
610
|
+
this.rl?.close();
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
// src/sink/analyzer.ts
|
|
615
|
+
var Analyzer = class {
|
|
616
|
+
firstTs = 0;
|
|
617
|
+
lastTs = 0;
|
|
618
|
+
rxBytes = 0;
|
|
619
|
+
txBytes = 0;
|
|
620
|
+
rxPackets = 0;
|
|
621
|
+
txPackets = 0;
|
|
622
|
+
onEvent(e) {
|
|
623
|
+
if (this.firstTs === 0) this.firstTs = e.ts;
|
|
624
|
+
this.lastTs = e.ts;
|
|
625
|
+
if (e.dir === "rx") {
|
|
626
|
+
this.rxBytes += e.data.length;
|
|
627
|
+
this.rxPackets++;
|
|
628
|
+
} else {
|
|
629
|
+
this.txBytes += e.data.length;
|
|
630
|
+
this.txPackets++;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
onClose() {
|
|
634
|
+
}
|
|
635
|
+
summarize() {
|
|
636
|
+
const durationMs = Math.max(0, this.lastTs - this.firstTs);
|
|
637
|
+
const secs = durationMs / 1e3 || 1;
|
|
638
|
+
return {
|
|
639
|
+
firstTs: this.firstTs,
|
|
640
|
+
lastTs: this.lastTs,
|
|
641
|
+
rxBytes: this.rxBytes,
|
|
642
|
+
txBytes: this.txBytes,
|
|
643
|
+
rxPackets: this.rxPackets,
|
|
644
|
+
txPackets: this.txPackets,
|
|
645
|
+
durationMs,
|
|
646
|
+
rxRate: this.rxBytes / secs,
|
|
647
|
+
txRate: this.txBytes / secs,
|
|
648
|
+
totalRate: (this.rxBytes + this.txBytes) / secs
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
// src/replay/replay.ts
|
|
654
|
+
import fs2 from "fs";
|
|
655
|
+
import readline2 from "readline";
|
|
656
|
+
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
657
|
+
async function readEvents(file, dir) {
|
|
658
|
+
const events = [];
|
|
659
|
+
const rl = readline2.createInterface({
|
|
660
|
+
input: fs2.createReadStream(file),
|
|
661
|
+
crlfDelay: Infinity
|
|
662
|
+
});
|
|
663
|
+
let headerSeen = false;
|
|
664
|
+
for await (const line of rl) {
|
|
665
|
+
if (!headerSeen) {
|
|
666
|
+
decodeHeader(line);
|
|
667
|
+
headerSeen = true;
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
const e = decodeEvent(line);
|
|
671
|
+
if (dir && e.dir !== dir) continue;
|
|
672
|
+
events.push(e);
|
|
673
|
+
}
|
|
674
|
+
return events;
|
|
675
|
+
}
|
|
676
|
+
async function replayFileToTransport(file, target, opts = {}) {
|
|
677
|
+
const events = await readEvents(file, opts.dir);
|
|
678
|
+
const speed = opts.speed ?? 1;
|
|
679
|
+
const total = events.length;
|
|
680
|
+
let sent = 0;
|
|
681
|
+
for (; ; ) {
|
|
682
|
+
let prevT;
|
|
683
|
+
for (const e of events) {
|
|
684
|
+
if (prevT !== void 0 && speed > 0) {
|
|
685
|
+
const wait = (e.ts - prevT) / speed;
|
|
686
|
+
if (wait > 0) await sleep2(wait);
|
|
687
|
+
}
|
|
688
|
+
prevT = e.ts;
|
|
689
|
+
await target.send(e.data);
|
|
690
|
+
sent++;
|
|
691
|
+
opts.onProgress?.(sent, total);
|
|
692
|
+
}
|
|
693
|
+
if (!opts.loop) break;
|
|
694
|
+
}
|
|
695
|
+
return sent;
|
|
696
|
+
}
|
|
697
|
+
export {
|
|
698
|
+
Analyzer,
|
|
699
|
+
DEFAULT_BAUD,
|
|
700
|
+
FileSource,
|
|
701
|
+
JsonlRecorder,
|
|
702
|
+
MAGIC_KEY,
|
|
703
|
+
Matcher,
|
|
704
|
+
SCHEMA_VERSION,
|
|
705
|
+
Session,
|
|
706
|
+
TrafficBus,
|
|
707
|
+
createTransport,
|
|
708
|
+
decodeEvent,
|
|
709
|
+
decodeHeader,
|
|
710
|
+
encodeEvent,
|
|
711
|
+
encodeHeader,
|
|
712
|
+
hexToBuffer,
|
|
713
|
+
hexdump,
|
|
714
|
+
listSerialPorts,
|
|
715
|
+
parseSpec,
|
|
716
|
+
readEvents,
|
|
717
|
+
replayFileToTransport,
|
|
718
|
+
toAscii,
|
|
719
|
+
toDesc,
|
|
720
|
+
toHex
|
|
721
|
+
};
|
|
722
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/event/bus.ts","../src/format/hexdump.ts","../src/transport/tcp.ts","../src/transport/udp.ts","../src/transport/serial.ts","../src/transport/factory.ts","../src/sink/filter.ts","../src/session.ts","../src/format/jsonl.ts","../src/sink/recorder.ts","../src/source/file-source.ts","../src/sink/analyzer.ts","../src/replay/replay.ts"],"sourcesContent":["import { EventEmitter } from 'node:events';\nimport type { TrafficEvent, SourceEventMap } from './events.js';\n\n/**\n * Typed event dispatcher that satisfies the `on`/`off` surface required by\n * {@link DataSource}. Live transports extend this and simply `emit('data', e)`\n * when bytes arrive.\n */\nexport class TrafficBus {\n private readonly emitter = new EventEmitter();\n\n on(event: 'data', fn: (e: TrafficEvent) => void): void;\n on<K extends keyof SourceEventMap>(event: K, fn: (e: SourceEventMap[K]) => void): void;\n on(event: string, fn: (...args: any[]) => void): void {\n this.emitter.on(event, fn);\n }\n\n off(event: string, fn: (...args: any[]) => void): void {\n this.emitter.off(event, fn);\n }\n\n protected emitData(e: TrafficEvent): void {\n this.emitter.emit('data', e);\n }\n\n protected emit<K extends keyof SourceEventMap>(\n event: K,\n payload: SourceEventMap[K],\n ): void {\n this.emitter.emit(event, payload);\n }\n}\n","/**\n * Formatting helpers shared by the CLI renderers (and reusable by a future GUI).\n * Everything here is pure: `Buffer` in, string out.\n */\n\nconst HEX = '0123456789abcdef';\n\nfunction hexByte(b: number): string {\n return HEX[(b >> 4) & 0xf]! + HEX[b & 0xf]!;\n}\n\n/** Lowercase hex of every byte, space-separated. */\nexport function toHex(buf: Buffer): string {\n let out = '';\n for (let i = 0; i < buf.length; i++) {\n if (i > 0) out += ' ';\n out += hexByte(buf[i]!);\n }\n return out;\n}\n\n/** Printable ASCII where possible, `.` otherwise. */\nexport function toAscii(buf: Buffer): string {\n let out = '';\n for (let i = 0; i < buf.length; i++) {\n const c = buf[i]!;\n out += c >= 0x20 && c <= 0x7e ? String.fromCharCode(c) : '.';\n }\n return out;\n}\n\nexport interface HexDumpLine {\n /** 8-digit zero-padded hex offset. */\n offset: string;\n hex: string;\n ascii: string;\n}\n\n/**\n * Classic `xxd`-style dump, grouped 8 bytes / 16 bytes per row, with an\n * ASCII gutter. Returns one object per row.\n */\nexport function hexdump(buf: Buffer, bytesPerRow = 16): HexDumpLine[] {\n const lines: HexDumpLine[] = [];\n for (let base = 0; base < buf.length; base += bytesPerRow) {\n const row = buf.subarray(base, base + bytesPerRow);\n const hexParts: string[] = [];\n let ascii = '';\n for (let i = 0; i < row.length; i++) {\n hexParts.push(hexByte(row[i]!));\n if (i === 7) hexParts.push(' '); // visual gap after byte 7\n ascii += toAscii(row.subarray(i, i + 1));\n }\n // Pad the hex column so every row lines up.\n const targetHex = bytesPerRow * 2 + Math.floor(bytesPerRow / 8);\n const hex = hexParts.join(' ').padEnd(targetHex, ' ');\n lines.push({\n offset: base.toString(16).padStart(8, '0'),\n hex,\n ascii,\n });\n }\n return lines;\n}\n","import net from 'node:net';\nimport type { TrafficEvent, TransportMeta } from '../event/events.js';\nimport { TrafficBus } from '../event/bus.js';\nimport type { Transport } from './transport.js';\n\n/** Outbound TCP client: connects to `host:port` and monitors the stream. */\nexport class TcpClientTransport extends TrafficBus implements Transport {\n private socket: net.Socket | undefined;\n private readonly meta: TransportMeta;\n\n constructor(\n private readonly host: string,\n private readonly port: number,\n ) {\n super();\n this.meta = {\n kind: 'tcp-client',\n id: `${host}:${port}`,\n desc: `tcp:${host}:${port}`,\n };\n }\n\n async start(): Promise<void> {\n const socket = net.createConnection({ host: this.host, port: this.port });\n this.socket = socket;\n await new Promise<void>((resolve, reject) => {\n socket.once('connect', resolve);\n socket.once('error', reject);\n });\n socket.on('data', (data: Buffer) => this.onRx(data));\n socket.on('close', () => this.emit('close', { source: this, meta: this.meta }));\n socket.on('error', (err) => this.emit('error', { source: this, error: err }));\n this.emit('open', { source: this, meta: this.meta });\n }\n\n private onRx(data: Buffer): void {\n const e: TrafficEvent = { ts: Date.now(), dir: 'rx', data, transport: this.meta };\n this.emitData(e);\n }\n\n async send(data: Buffer): Promise<void> {\n const socket = this.socket;\n if (!socket) throw new Error('tcp-client: not connected');\n await new Promise<void>((resolve, reject) => {\n socket.write(data, (err) => (err ? reject(err) : resolve()));\n });\n const e: TrafficEvent = { ts: Date.now(), dir: 'tx', data, transport: this.meta };\n this.emitData(e);\n }\n\n async stop(): Promise<void> {\n this.socket?.destroy();\n this.socket = undefined;\n }\n}\n\n/** Inbound TCP server: listens and monitors every accepted client. */\nexport class TcpServerTransport extends TrafficBus implements Transport {\n private server: net.Server | undefined;\n private readonly sockets = new Map<string, net.Socket>();\n\n constructor(\n private readonly host: string,\n private readonly port: number,\n ) {\n super();\n }\n\n async start(): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n const server = net.createServer((socket) => this.onConnection(socket));\n server.once('error', reject);\n server.listen(this.port, this.host || undefined, () => {\n server.off('error', reject);\n resolve();\n });\n this.server = server;\n });\n const meta: TransportMeta = {\n kind: 'tcp-server',\n id: `${this.host || '0.0.0.0'}:${this.port}`,\n desc: `tcp-listen:${this.host ? `${this.host}:` : ''}${this.port}`,\n };\n this.emit('open', { source: this, meta });\n }\n\n private onConnection(socket: net.Socket): void {\n const remote = `${socket.remoteAddress ?? '?'}:${socket.remotePort ?? '?'}`;\n const meta: TransportMeta = {\n kind: 'tcp-server',\n id: remote,\n desc: `tcp-listen:${this.port}#${remote}`,\n };\n this.sockets.set(remote, socket);\n socket.on('data', (data: Buffer) => {\n const e: TrafficEvent = { ts: Date.now(), dir: 'rx', data, transport: meta };\n this.emitData(e);\n });\n socket.on('close', () => this.sockets.delete(remote));\n socket.on('error', (err) => this.emit('error', { source: this, error: err }));\n }\n\n async send(data: Buffer): Promise<void> {\n const writes: Promise<void>[] = [];\n for (const socket of this.sockets.values()) {\n writes.push(\n new Promise<void>((resolve, reject) => {\n socket.write(data, (err) => (err ? reject(err) : resolve()));\n }),\n );\n }\n await Promise.all(writes);\n }\n\n async stop(): Promise<void> {\n for (const socket of this.sockets.values()) socket.destroy();\n this.sockets.clear();\n await new Promise<void>((resolve) => {\n if (!this.server) return resolve();\n this.server.close(() => resolve());\n });\n this.server = undefined;\n }\n}\n","import dgram from 'node:dgram';\nimport type { TrafficEvent, TransportMeta } from '../event/events.js';\nimport { TrafficBus } from '../event/bus.js';\nimport type { Transport } from './transport.js';\n\ntype UdpType = 'udp4' | 'udp6';\n\nfunction udpTypeFor(host: string): UdpType {\n return host.includes(':') ? 'udp6' : 'udp4';\n}\n\n/** Point-to-point UDP: sends to `host:port`, receives replies from it only. */\nexport class UdpTransport extends TrafficBus implements Transport {\n private socket: dgram.Socket | undefined;\n private readonly meta: TransportMeta;\n\n constructor(\n private readonly host: string,\n private readonly port: number,\n ) {\n super();\n this.meta = { kind: 'udp', id: `${host}:${port}`, desc: `udp:${host}:${port}` };\n }\n\n async start(): Promise<void> {\n const socket = dgram.createSocket(udpTypeFor(this.host));\n this.socket = socket;\n await new Promise<void>((resolve, reject) => {\n socket.once('error', reject);\n socket.bind(0, () => {\n socket.off('error', reject);\n resolve();\n });\n });\n socket.on('message', (msg: Buffer, rinfo) => {\n // Only accept datagrams from the configured peer.\n if (rinfo.address !== this.host || rinfo.port !== this.port) return;\n const e: TrafficEvent = { ts: Date.now(), dir: 'rx', data: msg, transport: this.meta };\n this.emitData(e);\n });\n socket.on('error', (err) => this.emit('error', { source: this, error: err }));\n this.emit('open', { source: this, meta: this.meta });\n }\n\n async send(data: Buffer): Promise<void> {\n const socket = this.socket;\n if (!socket) throw new Error('udp: not bound');\n await new Promise<void>((resolve, reject) => {\n socket.send(data, this.port, this.host, (err) => (err ? reject(err) : resolve()));\n });\n const e: TrafficEvent = { ts: Date.now(), dir: 'tx', data, transport: this.meta };\n this.emitData(e);\n }\n\n async stop(): Promise<void> {\n await new Promise<void>((resolve) => {\n if (!this.socket) return resolve();\n this.socket.close(() => resolve());\n });\n this.socket = undefined;\n }\n}\n\n/** Passive UDP listener: binds a local port and monitors datagrams from anyone. */\nexport class UdpListenTransport extends TrafficBus implements Transport {\n private socket: dgram.Socket | undefined;\n private lastPeer: { address: string; port: number } | undefined;\n private readonly meta: TransportMeta;\n\n constructor(\n private readonly host: string,\n private readonly port: number,\n ) {\n super();\n const label = `${this.host || '0.0.0.0'}:${this.port}`;\n this.meta = { kind: 'udp-listen', id: label, desc: `udp-listen:${label}` };\n }\n\n async start(): Promise<void> {\n const socket = dgram.createSocket(udpTypeFor(this.host));\n this.socket = socket;\n await new Promise<void>((resolve, reject) => {\n socket.once('error', reject);\n socket.bind(this.port, this.host || undefined, () => {\n socket.off('error', reject);\n resolve();\n });\n });\n socket.on('message', (msg: Buffer, rinfo) => {\n this.lastPeer = { address: rinfo.address, port: rinfo.port };\n const meta: TransportMeta = {\n kind: 'udp-listen',\n id: `${rinfo.address}:${rinfo.port}`,\n desc: this.meta.desc,\n };\n const e: TrafficEvent = { ts: Date.now(), dir: 'rx', data: msg, transport: meta };\n this.emitData(e);\n });\n socket.on('error', (err) => this.emit('error', { source: this, error: err }));\n this.emit('open', { source: this, meta: this.meta });\n }\n\n async send(data: Buffer): Promise<void> {\n const socket = this.socket;\n const peer = this.lastPeer;\n if (!socket) throw new Error('udp-listen: not bound');\n if (!peer) throw new Error('udp-listen: no peer has sent yet');\n await new Promise<void>((resolve, reject) => {\n socket.send(data, peer.port, peer.address, (err) => (err ? reject(err) : resolve()));\n });\n }\n\n async stop(): Promise<void> {\n await new Promise<void>((resolve) => {\n if (!this.socket) return resolve();\n this.socket.close(() => resolve());\n });\n this.socket = undefined;\n }\n}\n","import { SerialPort } from 'serialport';\nimport type { TrafficEvent, TransportMeta } from '../event/events.js';\nimport { TrafficBus } from '../event/bus.js';\nimport type { Transport } from './transport.js';\n\nexport const DEFAULT_BAUD = 115200;\n\n/** Serial transport backed by the `serialport` native binding. */\nexport class SerialTransport extends TrafficBus implements Transport {\n private port: SerialPort | undefined;\n private readonly meta: TransportMeta;\n\n constructor(\n private readonly path: string,\n private readonly baud: number,\n ) {\n super();\n this.meta = { kind: 'serial', id: path, desc: `serial:${path}:${baud}` };\n }\n\n async start(): Promise<void> {\n const port = new SerialPort({ path: this.path, baudRate: this.baud });\n this.port = port;\n await new Promise<void>((resolve, reject) => {\n port.once('open', resolve);\n port.once('error', reject);\n });\n port.on('data', (data: Buffer) => {\n const e: TrafficEvent = { ts: Date.now(), dir: 'rx', data, transport: this.meta };\n this.emitData(e);\n });\n port.on('close', () => this.emit('close', { source: this, meta: this.meta }));\n port.on('error', (err) => this.emit('error', { source: this, error: err }));\n this.emit('open', { source: this, meta: this.meta });\n }\n\n async send(data: Buffer): Promise<void> {\n if (!this.port) throw new Error('serial: not open');\n await new Promise<void>((resolve, reject) => {\n this.port!.write(data, (err) => (err ? reject(err) : resolve()));\n });\n const e: TrafficEvent = { ts: Date.now(), dir: 'tx', data, transport: this.meta };\n this.emitData(e);\n }\n\n async stop(): Promise<void> {\n if (!this.port) return;\n const port = this.port;\n this.port = undefined;\n await new Promise<void>((resolve) => port.close(() => resolve()));\n }\n}\n\n/** Enumerate attached serial ports. */\nexport async function listSerialPorts(): Promise<{ path: string; manufacturer?: string }[]> {\n const ports = await SerialPort.list();\n return ports.map((p) => ({ path: p.path, manufacturer: p.manufacturer }));\n}\n","import type { Transport } from './transport.js';\nimport { TcpClientTransport, TcpServerTransport } from './tcp.js';\nimport { UdpTransport, UdpListenTransport } from './udp.js';\nimport { SerialTransport, DEFAULT_BAUD } from './serial.js';\n\n/**\n * Parsed form of a transport spec string such as `tcp-listen:9000`.\n * Reversible: {@link toDesc} regenerates the canonical spec.\n */\nexport type TransportSpec =\n | { kind: 'serial'; port: string; baud: number }\n | { kind: 'tcp-client'; host: string; port: number }\n | { kind: 'tcp-server'; host: string; port: number }\n | { kind: 'udp'; host: string; port: number }\n | { kind: 'udp-listen'; host: string; port: number };\n\nfunction parsePort(s: string): number {\n if (!/^\\d+$/.test(s)) throw new Error(`invalid port \"${s}\"`);\n const p = Number(s);\n if (p < 1 || p > 65535) throw new Error(`port out of range \"${s}\"`);\n return p;\n}\n\nfunction parseBaud(s: string): number {\n if (!/^\\d+$/.test(s)) throw new Error(`invalid baud \"${s}\"`);\n const b = Number(s);\n if (b < 1) throw new Error(`invalid baud \"${s}\"`);\n return b;\n}\n\n/** Split `host:port`, accepting bracketed IPv6 `[::1]:9000`. */\nfunction splitHostPort(s: string): { host: string; port: number } {\n let host: string;\n let portStr: string;\n if (s.startsWith('[')) {\n const close = s.indexOf(']');\n if (close < 0) throw new Error(`invalid host:port \"${s}\"`);\n host = s.slice(1, close);\n portStr = s.slice(close + 1);\n if (!portStr.startsWith(':')) throw new Error(`invalid host:port \"${s}\"`);\n portStr = portStr.slice(1);\n } else {\n const i = s.lastIndexOf(':');\n if (i < 0) throw new Error(`missing port in \"${s}\"`);\n host = s.slice(0, i);\n portStr = s.slice(i + 1);\n }\n return { host, port: parsePort(portStr) };\n}\n\n/** Split `port` or `host:port` for listener schemes (bind address optional). */\nfunction splitListen(s: string): { host: string; port: number } {\n if (!s.includes(':')) return { host: '', port: parsePort(s) };\n return splitHostPort(s);\n}\n\n/** Split `path` or `path:baud` for serial. */\nfunction splitSerial(s: string): { path: string; baud: number } {\n const parts = s.split(':');\n if (parts.length >= 2 && /^\\d+$/.test(parts[1]!)) {\n return { path: parts[0]!, baud: parseBaud(parts[1]!) };\n }\n return { path: s, baud: DEFAULT_BAUD };\n}\n\n/** Parse a transport spec string into its structured form. */\nexport function parseSpec(spec: string): TransportSpec {\n const idx = spec.indexOf(':');\n if (idx < 0) throw new Error(`invalid transport spec \"${spec}\" (expected \"scheme:...\")`);\n const scheme = spec.slice(0, idx);\n const rest = spec.slice(idx + 1);\n\n switch (scheme) {\n case 'serial': {\n const { path, baud } = splitSerial(rest);\n return { kind: 'serial', port: path, baud };\n }\n case 'tcp': {\n const { host, port } = splitHostPort(rest);\n return { kind: 'tcp-client', host, port };\n }\n case 'tcp-listen': {\n const { host, port } = splitListen(rest);\n return { kind: 'tcp-server', host, port };\n }\n case 'udp': {\n const { host, port } = splitHostPort(rest);\n return { kind: 'udp', host, port };\n }\n case 'udp-listen': {\n const { host, port } = splitListen(rest);\n return { kind: 'udp-listen', host, port };\n }\n default:\n throw new Error(`unknown transport scheme \"${scheme}\"`);\n }\n}\n\n/** Canonical, reversible descriptor for a spec. */\nexport function toDesc(spec: TransportSpec): string {\n switch (spec.kind) {\n case 'serial':\n return `serial:${spec.port}:${spec.baud}`;\n case 'tcp-client':\n return `tcp:${spec.host}:${spec.port}`;\n case 'tcp-server':\n return `tcp-listen:${spec.host ? `${spec.host}:` : ''}${spec.port}`;\n case 'udp':\n return `udp:${spec.host}:${spec.port}`;\n case 'udp-listen':\n return `udp-listen:${spec.host ? `${spec.host}:` : ''}${spec.port}`;\n }\n}\n\n/** Construct the concrete {@link Transport} for a parsed spec. */\nexport function createTransport(spec: TransportSpec): Transport {\n switch (spec.kind) {\n case 'serial':\n return new SerialTransport(spec.port, spec.baud);\n case 'tcp-client':\n return new TcpClientTransport(spec.host, spec.port);\n case 'tcp-server':\n return new TcpServerTransport(spec.host, spec.port);\n case 'udp':\n return new UdpTransport(spec.host, spec.port);\n case 'udp-listen':\n return new UdpListenTransport(spec.host, spec.port);\n }\n}\n","import type { Direction, TrafficEvent } from '../event/events.js';\n\n/** Parse a hex string (spaces and `0x` tolerated) into a Buffer. */\nexport function hexToBuffer(s: string): Buffer {\n const clean = s.replace(/0x/gi, '').replace(/[\\s,]/g, '');\n if (clean.length % 2 !== 0) throw new Error(`hex string has odd length: \"${s}\"`);\n if (!/^[0-9a-f]*$/i.test(clean)) throw new Error(`invalid hex string: \"${s}\"`);\n const buf = Buffer.alloc(clean.length / 2);\n for (let i = 0; i < buf.length; i++) buf[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);\n return buf;\n}\n\nexport interface FilterOptions {\n /** Only pass events in this direction. */\n dir?: Direction;\n /** Byte-sequence match against the raw payload. */\n hex?: Buffer;\n /** Literal (UTF-8) substring match against the raw payload. */\n string?: Buffer;\n /** Regex matched against the UTF-8-decoded payload. */\n regex?: RegExp;\n}\n\n/**\n * Per-event predicate. All specified criteria must pass for an event to be\n * forwarded to sinks. Note this matches within a single event; stream-level\n * patterns that span event boundaries belong to the search/analysis layer.\n */\nexport class Matcher {\n constructor(private readonly opts: FilterOptions) {}\n\n matches(e: TrafficEvent): boolean {\n if (this.opts.dir && e.dir !== this.opts.dir) return false;\n if (this.opts.hex && !e.data.includes(this.opts.hex)) return false;\n if (this.opts.string && !e.data.includes(this.opts.string)) return false;\n if (this.opts.regex && !this.opts.regex.test(e.data.toString('utf8'))) return false;\n return true;\n }\n}\n","import type { DataSource } from './source/source.js';\nimport type { Sink } from './sink/sink.js';\nimport type { Matcher } from './sink/filter.js';\nimport type { TrafficEvent } from './event/events.js';\n\nexport interface SessionOptions {\n filter?: Matcher;\n sinks?: Sink[];\n}\n\n/**\n * Wires a {@link DataSource} to a set of sinks, applying an optional filter.\n * This is the shared glue behind `monitor`, `view` and `replay`: the source is\n * swapped (live transport vs recording file), the sink graph stays the same.\n */\nexport class Session {\n private readonly sinks: Sink[];\n private finished = false;\n\n constructor(\n private readonly source: DataSource,\n private readonly opts: SessionOptions = {},\n ) {\n this.sinks = opts.sinks ?? [];\n }\n\n async run(): Promise<void> {\n this.source.on('data', (e) => this.dispatch(e));\n this.source.on('close', () => this.finish());\n await this.source.start();\n }\n\n private dispatch(e: TrafficEvent): void {\n if (this.opts.filter && !this.opts.filter.matches(e)) return;\n for (const sink of this.sinks) sink.onEvent(e);\n }\n\n private finish(): void {\n if (this.finished) return;\n this.finished = true;\n for (const sink of this.sinks) sink.onClose();\n }\n\n async stop(): Promise<void> {\n await this.source.stop();\n this.finish();\n }\n}\n","import type { Direction, TrafficEvent, TransportKind } from '../event/events.js';\n\n/**\n * JSON Lines recording format.\n *\n * First line is a header carrying a magic key (`comm-scope`) plus the schema\n * version and session metadata. Every following line is a single traffic\n * event, payload encoded as lowercase hex so binary data round-trips cleanly\n * through JSON without escaping concerns.\n */\n\nexport const SCHEMA_VERSION = 1;\nexport const MAGIC_KEY = 'comm-scope';\n\nexport interface SessionHeader {\n version: number;\n kind: TransportKind;\n id: string;\n desc: string;\n started: number;\n}\n\nexport function encodeHeader(h: SessionHeader): string {\n return JSON.stringify({\n [MAGIC_KEY]: h.version,\n kind: h.kind,\n id: h.id,\n desc: h.desc,\n started: h.started,\n });\n}\n\nexport function decodeHeader(line: string): SessionHeader {\n const o = JSON.parse(line) as Record<string, unknown>;\n const version = o[MAGIC_KEY];\n if (typeof version !== 'number') {\n throw new Error('not a comm-scope recording (missing header)');\n }\n if (version !== SCHEMA_VERSION) {\n throw new Error(`unsupported recording schema version ${version}`);\n }\n return {\n version,\n kind: o.kind as TransportKind,\n id: String(o.id ?? ''),\n desc: String(o.desc ?? ''),\n started: Number(o.started ?? 0),\n };\n}\n\nexport function encodeEvent(e: TrafficEvent): string {\n return JSON.stringify({\n t: e.ts,\n dir: e.dir,\n kind: e.transport.kind,\n id: e.transport.id,\n desc: e.transport.desc,\n enc: 'hex',\n data: e.data.toString('hex'),\n });\n}\n\nexport function decodeEvent(line: string): TrafficEvent {\n const o = JSON.parse(line) as Record<string, unknown>;\n if (o.enc !== 'hex') throw new Error(`unsupported payload encoding \"${String(o.enc)}\"`);\n const dir = o.dir as Direction;\n if (dir !== 'rx' && dir !== 'tx') throw new Error(`invalid direction \"${String(o.dir)}\"`);\n return {\n ts: Number(o.t),\n dir,\n data: Buffer.from(String(o.data), 'hex'),\n transport: {\n kind: o.kind as TransportKind,\n id: String(o.id ?? ''),\n desc: String(o.desc ?? ''),\n },\n };\n}\n","import type { Writable } from 'node:stream';\nimport type { TrafficEvent } from '../event/events.js';\nimport type { Sink } from './sink.js';\nimport { encodeEvent, encodeHeader } from '../format/jsonl.js';\nimport type { SessionHeader } from '../format/jsonl.js';\n\n/**\n * Writes traffic as JSON Lines. Owns the target stream: it writes the header\n * immediately and ends the stream when the session closes.\n */\nexport class JsonlRecorder implements Sink {\n private readonly header: SessionHeader;\n\n constructor(\n private readonly stream: Writable,\n header: SessionHeader,\n ) {\n this.header = header;\n this.stream.write(encodeHeader(header) + '\\n');\n }\n\n onEvent(e: TrafficEvent): void {\n this.stream.write(encodeEvent(e) + '\\n');\n }\n\n onClose(): void {\n this.stream.end();\n }\n}\n","import fs from 'node:fs';\nimport readline from 'node:readline';\nimport type { Direction, TransportMeta } from '../event/events.js';\nimport { TrafficBus } from '../event/bus.js';\nimport type { DataSource } from './source.js';\nimport { decodeEvent, decodeHeader } from '../format/jsonl.js';\nimport type { SessionHeader } from '../format/jsonl.js';\n\nconst sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));\n\nexport interface FileSourceOptions {\n /** Replay speed multiplier. 1 = real-time, 0 = as fast as possible. */\n speed?: number;\n /** Optional direction filter applied while reading. */\n dir?: Direction;\n}\n\n/**\n * Offline data source: reads a recording and re-emits its events at the\n * original timing (scaled by `speed`). Swappable for any live transport, which\n * is what makes `view` behave identically to `monitor`.\n */\nexport class FileSource extends TrafficBus implements DataSource {\n private stopped = false;\n private rl: readline.Interface | undefined;\n private readonly meta: TransportMeta;\n\n constructor(\n private readonly file: string,\n private readonly opts: FileSourceOptions = {},\n ) {\n super();\n // Placeholder meta; replaced by the header's metadata once read.\n this.meta = { kind: 'serial', id: file, desc: file };\n }\n\n async start(): Promise<void> {\n this.stopped = false;\n const rl = readline.createInterface({\n input: fs.createReadStream(this.file),\n crlfDelay: Infinity,\n });\n this.rl = rl;\n\n const speed = this.opts.speed ?? 1;\n let prevT: number | undefined;\n let header: SessionHeader | undefined;\n\n try {\n for await (const line of rl) {\n if (this.stopped) break;\n if (header === undefined) {\n header = decodeHeader(line);\n this.emit('open', { source: this, meta: this.headerMeta(header) });\n continue;\n }\n const e = decodeEvent(line);\n if (this.opts.dir && e.dir !== this.opts.dir) continue;\n if (prevT !== undefined && speed > 0) {\n const wait = (e.ts - prevT) / speed;\n if (wait > 0) await sleep(wait);\n }\n prevT = e.ts;\n this.emitData(e);\n }\n } finally {\n if (!this.stopped) {\n this.emit('close', { source: this, meta: this.meta });\n }\n }\n }\n\n private headerMeta(h: SessionHeader): TransportMeta {\n return { kind: h.kind, id: h.id, desc: h.desc };\n }\n\n async stop(): Promise<void> {\n this.stopped = true;\n this.rl?.close();\n }\n}\n","import type { TrafficEvent } from '../event/events.js';\nimport type { Sink } from './sink.js';\n\nexport interface TrafficStats {\n firstTs: number;\n lastTs: number;\n rxBytes: number;\n txBytes: number;\n rxPackets: number;\n txPackets: number;\n}\n\nexport interface TrafficSummary extends TrafficStats {\n durationMs: number;\n rxRate: number;\n txRate: number;\n totalRate: number;\n}\n\n/**\n * Accumulates byte/packet counters and rates. Attach alongside a renderer or\n * recorder; call {@link summarize} when the session ends.\n */\nexport class Analyzer implements Sink {\n private firstTs = 0;\n private lastTs = 0;\n private rxBytes = 0;\n private txBytes = 0;\n private rxPackets = 0;\n private txPackets = 0;\n\n onEvent(e: TrafficEvent): void {\n if (this.firstTs === 0) this.firstTs = e.ts;\n this.lastTs = e.ts;\n if (e.dir === 'rx') {\n this.rxBytes += e.data.length;\n this.rxPackets++;\n } else {\n this.txBytes += e.data.length;\n this.txPackets++;\n }\n }\n\n onClose(): void {}\n\n summarize(): TrafficSummary {\n const durationMs = Math.max(0, this.lastTs - this.firstTs);\n const secs = durationMs / 1000 || 1;\n return {\n firstTs: this.firstTs,\n lastTs: this.lastTs,\n rxBytes: this.rxBytes,\n txBytes: this.txBytes,\n rxPackets: this.rxPackets,\n txPackets: this.txPackets,\n durationMs,\n rxRate: this.rxBytes / secs,\n txRate: this.txBytes / secs,\n totalRate: (this.rxBytes + this.txBytes) / secs,\n };\n }\n}\n","import fs from 'node:fs';\nimport readline from 'node:readline';\nimport type { Direction, TrafficEvent } from '../event/events.js';\nimport type { Transport } from '../transport/transport.js';\nimport { decodeHeader, decodeEvent } from '../format/jsonl.js';\n\nconst sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));\n\n/** Read a recording's events into memory, optionally filtering by direction. */\nexport async function readEvents(file: string, dir?: Direction): Promise<TrafficEvent[]> {\n const events: TrafficEvent[] = [];\n const rl = readline.createInterface({\n input: fs.createReadStream(file),\n crlfDelay: Infinity,\n });\n let headerSeen = false;\n for await (const line of rl) {\n if (!headerSeen) {\n decodeHeader(line);\n headerSeen = true;\n continue;\n }\n const e = decodeEvent(line);\n if (dir && e.dir !== dir) continue;\n events.push(e);\n }\n return events;\n}\n\nexport interface ReplayOptions {\n /** Replay speed multiplier. 1 = real-time, 0 = as fast as possible. */\n speed?: number;\n /** Only replay events in this direction. Defaults to all. */\n dir?: Direction;\n /** Re-send continuously until interrupted. */\n loop?: boolean;\n /** Progress callback, fired once per event sent. */\n onProgress?: (sent: number, total: number) => void;\n}\n\n/**\n * Re-send a recording's bytes to a live transport at the original timing.\n * Resolves once finished (never, when `loop` is set — interrupt externally).\n */\nexport async function replayFileToTransport(\n file: string,\n target: Transport,\n opts: ReplayOptions = {},\n): Promise<number> {\n const events = await readEvents(file, opts.dir);\n const speed = opts.speed ?? 1;\n const total = events.length;\n let sent = 0;\n\n for (;;) {\n let prevT: number | undefined;\n for (const e of events) {\n if (prevT !== undefined && speed > 0) {\n const wait = (e.ts - prevT) / speed;\n if (wait > 0) await sleep(wait);\n }\n prevT = e.ts;\n await target.send(e.data);\n sent++;\n opts.onProgress?.(sent, total);\n }\n if (!opts.loop) break;\n }\n\n return sent;\n}\n"],"mappings":"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","names":["fs","readline","sleep","readline","fs"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@anthonyfree96/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Engine for comm-scope: transport capture, recording, replay and analysis",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"serial",
|
|
23
|
+
"tcp",
|
|
24
|
+
"udp",
|
|
25
|
+
"traffic",
|
|
26
|
+
"replay",
|
|
27
|
+
"transport"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/AndyFree96/comm-scope"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup",
|
|
38
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
39
|
+
"test": "vitest run"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"serialport": "^13.0.0"
|
|
43
|
+
}
|
|
44
|
+
}
|