pyre-cli 1.3.0 → 2.0.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/README.md +278 -17
- package/dist/index.js +1763 -350
- package/package.json +6 -3
- package/parity.md +542 -0
- package/src/formatters/index.ts +17 -0
- package/src/formatters/output.ts +165 -0
- package/src/formatters/render.ts +659 -0
- package/src/formatters/themes.ts +115 -0
- package/src/formatters/types.ts +62 -0
- package/src/history.ts +153 -0
- package/src/index.ts +201 -0
- package/src/live/export.ts +76 -0
- package/src/live/index.ts +16 -0
- package/src/live/input.ts +443 -0
- package/src/live/render.ts +146 -0
- package/src/live/state.ts +103 -0
- package/src/live/types.ts +23 -0
- package/src/monitors/collectors.ts +687 -0
- package/src/monitors/index.ts +14 -0
- package/src/monitors/run.ts +18 -0
- package/src/monitors/smc.ts +107 -0
- package/src/monitors/types.ts +148 -0
- package/src/p2p/client.ts +197 -0
- package/src/p2p/index.ts +3 -0
- package/src/p2p/protocol.ts +71 -0
- package/src/p2p/server.ts +219 -0
- package/src/p2p/types.ts +71 -0
- package/src/pyre_img +0 -0
- package/src/sparkline.ts +34 -0
- package/src/splash.ts +102 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared shell-execution helper for the monitors module.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `child_process.execSync` with a timeout and a
|
|
5
|
+
* fallback return value so that a failing command never
|
|
6
|
+
* crashes the entire collection pipeline.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
async function run(cmd: string, fallback: string = ''): Promise<string> {
|
|
10
|
+
try {
|
|
11
|
+
const { execSync } = await import('node:child_process');
|
|
12
|
+
return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 }).trim();
|
|
13
|
+
} catch {
|
|
14
|
+
return fallback;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { run };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMC and powermetrics sensor reader.
|
|
3
|
+
*
|
|
4
|
+
* Modern macOS requires `powermetrics` (root) to read CPU die
|
|
5
|
+
* temperature, GPU die temperature, SMC die temperature, and
|
|
6
|
+
* power draw (CPU/GPU/combined watts). This module wraps that
|
|
7
|
+
* call with a short-lived cache so that multiple collectors can
|
|
8
|
+
* reuse the same data within a single tick without invoking
|
|
9
|
+
* `powermetrics` repeatedly.
|
|
10
|
+
*
|
|
11
|
+
* It also provides the `parseSuffix` helper used when decoding
|
|
12
|
+
* `vm.swapusage` output.
|
|
13
|
+
*/
|
|
14
|
+
import { run } from './run.js';
|
|
15
|
+
|
|
16
|
+
export interface SmcMetrics {
|
|
17
|
+
temps: Record<string, number>;
|
|
18
|
+
power: { cpu?: number; gpu?: number; combined?: number };
|
|
19
|
+
freq: Record<string, number>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let smcCache: { data: SmcMetrics; ts: number } | null = null;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Query `powermetrics` for SMC temperatures, power draw, and
|
|
26
|
+
* CPU frequency. Results are cached for 1.5 s so that
|
|
27
|
+
* concurrent collectors (CPU temp, power, thermal) share a
|
|
28
|
+
* single invocation.
|
|
29
|
+
*
|
|
30
|
+
* Falls back to an empty object on any failure (missing
|
|
31
|
+
* sudoers entry, unavailable binary, etc.) so the rest of the
|
|
32
|
+
* application degrades gracefully.
|
|
33
|
+
*/
|
|
34
|
+
export async function getSmcMetrics(): Promise<SmcMetrics> {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
if (smcCache && now - smcCache.ts < 1500) return smcCache.data;
|
|
37
|
+
|
|
38
|
+
const result: SmcMetrics = { temps: {}, power: {}, freq: {} };
|
|
39
|
+
try {
|
|
40
|
+
// `sudo -n` (non-interactive) so we never hang waiting on a
|
|
41
|
+
// password prompt that can't be answered from a piped child
|
|
42
|
+
// process; if the user hasn't set up passwordless sudo for
|
|
43
|
+
// powermetrics, this just fails fast and we fall back to the
|
|
44
|
+
// thermal-state string instead of a temperature.
|
|
45
|
+
const pm = (
|
|
46
|
+
await run(
|
|
47
|
+
'sudo -n powermetrics --samplers smc,cpu_power -n 1 --format text 2>/dev/null',
|
|
48
|
+
''
|
|
49
|
+
)
|
|
50
|
+
).trim();
|
|
51
|
+
|
|
52
|
+
if (pm) {
|
|
53
|
+
const cpuTemp = pm.match(/CPU die temperature:\s*([\d.]+)/i);
|
|
54
|
+
if (cpuTemp) result.temps['cpu_die'] = parseFloat(cpuTemp[1]);
|
|
55
|
+
|
|
56
|
+
const gpuTemp = pm.match(/GPU die temperature:\s*([\d.]+)/i);
|
|
57
|
+
if (gpuTemp) result.temps['gpu_die'] = parseFloat(gpuTemp[1]);
|
|
58
|
+
|
|
59
|
+
const smcTemp = pm.match(/SMC die temperature:\s*([\d.]+)/i);
|
|
60
|
+
if (smcTemp) result.temps['smc_die'] = parseFloat(smcTemp[1]);
|
|
61
|
+
|
|
62
|
+
const cpuPower = pm.match(/CPU Power:\s*([\d.]+)\s*mW/i);
|
|
63
|
+
if (cpuPower) result.power.cpu = parseFloat(cpuPower[1]) / 1000;
|
|
64
|
+
|
|
65
|
+
const gpuPower = pm.match(/GPU Power:\s*([\d.]+)\s*mW/i);
|
|
66
|
+
if (gpuPower) result.power.gpu = parseFloat(gpuPower[1]) / 1000;
|
|
67
|
+
|
|
68
|
+
const combinedPower = pm.match(/Combined Power \(CPU \+ GPU.*?\):\s*([\d.]+)\s*mW/i);
|
|
69
|
+
if (combinedPower) result.power.combined = parseFloat(combinedPower[1]) / 1000;
|
|
70
|
+
|
|
71
|
+
// Apple Silicon reports active clock speed per core cluster
|
|
72
|
+
// rather than a single number — grab whichever cluster(s)
|
|
73
|
+
// this chip has.
|
|
74
|
+
const eFreq = pm.match(/E-Cluster HW active frequency:\s*([\d.]+)/i);
|
|
75
|
+
if (eFreq) result.freq['e_cluster'] = parseFloat(eFreq[1]);
|
|
76
|
+
|
|
77
|
+
const pFreq = pm.match(/P-Cluster HW active frequency:\s*([\d.]+)/i);
|
|
78
|
+
if (pFreq) result.freq['p_cluster'] = parseFloat(pFreq[1]);
|
|
79
|
+
|
|
80
|
+
// Some single-cluster/older chips report just "CPU HW active frequency".
|
|
81
|
+
const cpuFreq = pm.match(/CPU HW active frequency:\s*([\d.]+)/i);
|
|
82
|
+
if (cpuFreq) result.freq['cpu'] = parseFloat(cpuFreq[1]);
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// powermetrics unavailable/unauthorized — leave temps/power/freq empty,
|
|
86
|
+
// the rest of the app degrades gracefully (thermal state string still
|
|
87
|
+
// shows, and CPU frequency falls back to 0/"unavailable").
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
smcCache = { data: result, ts: now };
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Parse a size suffix (K, M, G) from `vm.swapusage` output into
|
|
96
|
+
* its byte multiplier. Case-insensitive; defaults to 1 (no
|
|
97
|
+
* suffix).
|
|
98
|
+
*/
|
|
99
|
+
export function parseSuffix(s: string): number {
|
|
100
|
+
return s.toUpperCase() === 'K'
|
|
101
|
+
? 1024
|
|
102
|
+
: s.toUpperCase() === 'M'
|
|
103
|
+
? 1024 * 1024
|
|
104
|
+
: s.toUpperCase() === 'G'
|
|
105
|
+
? 1024 * 1024 * 1024
|
|
106
|
+
: 1;
|
|
107
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core data types for system metric collection.
|
|
3
|
+
*
|
|
4
|
+
* Every interface in this file represents a distinct category of
|
|
5
|
+
* system information gathered by the monitors module. The
|
|
6
|
+
* {@link StatsData} aggregate ties them together for downstream
|
|
7
|
+
* formatting and export.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface StatsData {
|
|
11
|
+
header: { title: string; hostname: string; os: string; uptime: string };
|
|
12
|
+
cpu: CpuData;
|
|
13
|
+
gpu: GpuData | null;
|
|
14
|
+
memory: MemoryData;
|
|
15
|
+
disk: DiskData[];
|
|
16
|
+
battery: BatteryData | null;
|
|
17
|
+
thermal: ThermalData;
|
|
18
|
+
network: NetworkData;
|
|
19
|
+
processes: ProcessData[];
|
|
20
|
+
power: PowerData | null;
|
|
21
|
+
timestamp: string;
|
|
22
|
+
packets: PacketData | null;
|
|
23
|
+
tasks: TaskData[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface GpuData {
|
|
27
|
+
model: string;
|
|
28
|
+
memory: number;
|
|
29
|
+
utilization: number;
|
|
30
|
+
temperature?: number;
|
|
31
|
+
processes: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PowerData {
|
|
35
|
+
cpuWatts?: number;
|
|
36
|
+
gpuWatts?: number;
|
|
37
|
+
combinedWatts?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface PacketData {
|
|
41
|
+
totalPackets: number;
|
|
42
|
+
rxPackets: number;
|
|
43
|
+
txPackets: number;
|
|
44
|
+
rxRate: number;
|
|
45
|
+
txRate: number;
|
|
46
|
+
connections: number;
|
|
47
|
+
topProcesses: { pid: number; command: string; rxBytes: number; txBytes: number }[];
|
|
48
|
+
/** Every process with an open network connection (unlike topProcesses, not truncated). */
|
|
49
|
+
allProcesses: { pid: number; command: string; rxBytes: number; txBytes: number; rxPackets?: number; txPackets?: number }[];
|
|
50
|
+
interfaces: { iface: string; rxPackets: number; txPackets: number; rxBytes: number; txBytes: number }[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface TaskData {
|
|
54
|
+
pid: number;
|
|
55
|
+
user: string;
|
|
56
|
+
cpu: number;
|
|
57
|
+
mem: number;
|
|
58
|
+
command: string;
|
|
59
|
+
state: string;
|
|
60
|
+
runtime: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface CpuData {
|
|
64
|
+
brand: string;
|
|
65
|
+
cores: number;
|
|
66
|
+
physicalCores: number;
|
|
67
|
+
frequency: number;
|
|
68
|
+
usage: number;
|
|
69
|
+
loadAvg: number[];
|
|
70
|
+
temperature?: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface MemoryData {
|
|
74
|
+
total: number;
|
|
75
|
+
used: number;
|
|
76
|
+
free: number;
|
|
77
|
+
swapTotal: number;
|
|
78
|
+
swapUsed: number;
|
|
79
|
+
swapFree: number;
|
|
80
|
+
pageSize: number;
|
|
81
|
+
usagePercent: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ThermalData {
|
|
85
|
+
state: string;
|
|
86
|
+
detail?: string;
|
|
87
|
+
/** Normalized 0-3 scale (nominal/fair/serious/critical) derived from `state`, for graphing/coloring */
|
|
88
|
+
pressureLevel: number;
|
|
89
|
+
temperatures?: Record<string, number | null>;
|
|
90
|
+
error?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface BatteryData {
|
|
94
|
+
level: number;
|
|
95
|
+
state: string;
|
|
96
|
+
timeRemaining: string;
|
|
97
|
+
health: string;
|
|
98
|
+
powerSource: string;
|
|
99
|
+
cycles?: number;
|
|
100
|
+
condition?: string;
|
|
101
|
+
maxCapacityPercent?: number;
|
|
102
|
+
estimatedTimeToEmpty?: string;
|
|
103
|
+
dischargeRatePerHour?: number;
|
|
104
|
+
powerWatts?: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface DiskData {
|
|
108
|
+
filesystem: string;
|
|
109
|
+
size: string;
|
|
110
|
+
used: string;
|
|
111
|
+
available: string;
|
|
112
|
+
capacity: string;
|
|
113
|
+
mountpoint: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface NetworkData {
|
|
117
|
+
interface: string;
|
|
118
|
+
ip: string;
|
|
119
|
+
rxBytes: number;
|
|
120
|
+
txBytes: number;
|
|
121
|
+
rxPackets: number;
|
|
122
|
+
txPackets: number;
|
|
123
|
+
rxPacketsPerSec?: number;
|
|
124
|
+
txPacketsPerSec?: number;
|
|
125
|
+
connections?: number;
|
|
126
|
+
topProcesses?: NetworkProcess[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface NetworkProcess {
|
|
130
|
+
pid: number;
|
|
131
|
+
command: string;
|
|
132
|
+
rxBytes: number;
|
|
133
|
+
txBytes: number;
|
|
134
|
+
rxPackets: number;
|
|
135
|
+
txPackets: number;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface ProcessData {
|
|
139
|
+
pid: number;
|
|
140
|
+
ppid: number;
|
|
141
|
+
user: string;
|
|
142
|
+
cpu: number;
|
|
143
|
+
mem: number;
|
|
144
|
+
command: string;
|
|
145
|
+
state: string;
|
|
146
|
+
threads: number;
|
|
147
|
+
runtime: number;
|
|
148
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { encodeAuth, encodePing, encodePong, encodeDisconnect } from './protocol.js';
|
|
4
|
+
import type { P2PClientOptions } from './types.js';
|
|
5
|
+
import type { StatsData } from '../monitors/types.js';
|
|
6
|
+
|
|
7
|
+
interface ReadState {
|
|
8
|
+
buffer: Buffer;
|
|
9
|
+
expectingHeader: boolean;
|
|
10
|
+
messageLength: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class P2PClient {
|
|
14
|
+
private socket: net.Socket | null = null;
|
|
15
|
+
private options: P2PClientOptions;
|
|
16
|
+
private running = false;
|
|
17
|
+
private readState: ReadState = { buffer: Buffer.alloc(0), expectingHeader: true, messageLength: 0 };
|
|
18
|
+
private onDataCallback: ((data: StatsData) => void) | null = null;
|
|
19
|
+
private onStatusCallback: ((msg: string) => void) | null = null;
|
|
20
|
+
private reconnectTimer: NodeJS.Timeout | null = null;
|
|
21
|
+
private pingHandle: NodeJS.Timeout | null = null;
|
|
22
|
+
|
|
23
|
+
constructor(options: P2PClientOptions) {
|
|
24
|
+
this.options = options;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
setOnData(cb: (data: StatsData) => void): void {
|
|
28
|
+
this.onDataCallback = cb;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
setOnStatus(cb: (msg: string) => void): void {
|
|
32
|
+
this.onStatusCallback = cb;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async connect(): Promise<void> {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
this.socket = new net.Socket();
|
|
38
|
+
|
|
39
|
+
this.socket.on('data', (data) => this.onData(data));
|
|
40
|
+
this.socket.on('close', () => this.onClose());
|
|
41
|
+
this.socket.on('error', (err) => this.onError(err));
|
|
42
|
+
this.socket.on('timeout', () => this.onTimeout());
|
|
43
|
+
|
|
44
|
+
this.socket.setTimeout(10000);
|
|
45
|
+
|
|
46
|
+
this.socket.connect(this.options.port, this.options.host, () => {
|
|
47
|
+
this.running = true;
|
|
48
|
+
this.socket?.setTimeout(0);
|
|
49
|
+
this.socket?.write(encodeAuth(this.options.password));
|
|
50
|
+
this.status('Connecting...');
|
|
51
|
+
resolve();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
this.socket.once('error', (err) => {
|
|
55
|
+
reject(err);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
disconnect(): void {
|
|
61
|
+
this.running = false;
|
|
62
|
+
if (this.reconnectTimer) {
|
|
63
|
+
clearTimeout(this.reconnectTimer);
|
|
64
|
+
this.reconnectTimer = null;
|
|
65
|
+
}
|
|
66
|
+
if (this.pingHandle) {
|
|
67
|
+
clearInterval(this.pingHandle);
|
|
68
|
+
this.pingHandle = null;
|
|
69
|
+
}
|
|
70
|
+
if (this.socket) {
|
|
71
|
+
try {
|
|
72
|
+
this.socket.write(encodeDisconnect('Client closing'));
|
|
73
|
+
} catch {
|
|
74
|
+
// ignore
|
|
75
|
+
}
|
|
76
|
+
this.socket.destroy();
|
|
77
|
+
this.socket = null;
|
|
78
|
+
}
|
|
79
|
+
this.status('Disconnected');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private onData(data: Buffer): void {
|
|
83
|
+
this.readState.buffer = Buffer.concat([this.readState.buffer, data]);
|
|
84
|
+
|
|
85
|
+
while (this.readState.buffer.length > 0) {
|
|
86
|
+
if (this.readState.expectingHeader) {
|
|
87
|
+
if (this.readState.buffer.length >= 4) {
|
|
88
|
+
this.readState.messageLength = this.readState.buffer.readUInt32BE(0);
|
|
89
|
+
this.readState.buffer = this.readState.buffer.subarray(4);
|
|
90
|
+
this.readState.expectingHeader = false;
|
|
91
|
+
} else {
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!this.readState.expectingHeader) {
|
|
97
|
+
if (this.readState.buffer.length >= this.readState.messageLength) {
|
|
98
|
+
const body = this.readState.buffer.subarray(0, this.readState.messageLength);
|
|
99
|
+
this.readState.buffer = this.readState.buffer.subarray(this.readState.messageLength);
|
|
100
|
+
this.readState.expectingHeader = true;
|
|
101
|
+
this.handleMessage(body);
|
|
102
|
+
} else {
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private handleMessage(body: Buffer): void {
|
|
110
|
+
try {
|
|
111
|
+
const msg = JSON.parse(body.toString('utf-8'));
|
|
112
|
+
|
|
113
|
+
switch (msg.kind) {
|
|
114
|
+
case 'auth-ok':
|
|
115
|
+
this.status('Authenticated — receiving live data');
|
|
116
|
+
this.startPing();
|
|
117
|
+
break;
|
|
118
|
+
|
|
119
|
+
case 'auth-fail':
|
|
120
|
+
this.status(`Authentication failed: ${(msg.payload as { reason: string }).reason}`);
|
|
121
|
+
this.disconnect();
|
|
122
|
+
break;
|
|
123
|
+
|
|
124
|
+
case 'data':
|
|
125
|
+
if (this.onDataCallback && msg.payload?.stats) {
|
|
126
|
+
this.onDataCallback(msg.payload.stats as StatsData);
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
|
|
130
|
+
case 'ping':
|
|
131
|
+
if (this.socket && !this.socket.destroyed) {
|
|
132
|
+
const ts = (msg.payload as { ts: number }).ts;
|
|
133
|
+
this.socket.write(encodePong(ts));
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
case 'pong':
|
|
138
|
+
break;
|
|
139
|
+
|
|
140
|
+
case 'disconnect':
|
|
141
|
+
this.status(`Server disconnected: ${(msg.payload as { reason: string }).reason}`);
|
|
142
|
+
this.disconnect();
|
|
143
|
+
break;
|
|
144
|
+
|
|
145
|
+
default:
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
} catch {
|
|
149
|
+
// ignore malformed messages
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private startPing(): void {
|
|
154
|
+
this.pingHandle = setInterval(() => {
|
|
155
|
+
if (this.socket && !this.socket.destroyed) {
|
|
156
|
+
this.socket.write(encodePing());
|
|
157
|
+
}
|
|
158
|
+
}, 15000);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private onClose(): void {
|
|
162
|
+
if (this.running) {
|
|
163
|
+
this.status('Connection closed — reconnecting in 3s');
|
|
164
|
+
this.reconnectTimer = setTimeout(() => {
|
|
165
|
+
if (this.running) this.connect().catch(() => {});
|
|
166
|
+
}, 3000);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private onError(err: Error): void {
|
|
171
|
+
const code = (err as any).code;
|
|
172
|
+
if (code === 'ECONNREFUSED') {
|
|
173
|
+
this.status('Connection refused — is the server running?');
|
|
174
|
+
} else if (code === 'ETIMEDOUT') {
|
|
175
|
+
this.status('Connection timed out');
|
|
176
|
+
} else {
|
|
177
|
+
this.status(`Connection error: ${err.message}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private onTimeout(): void {
|
|
182
|
+
this.status('Connection timed out');
|
|
183
|
+
this.disconnect();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private status(msg: string): void {
|
|
187
|
+
if (this.onStatusCallback) {
|
|
188
|
+
this.onStatusCallback(msg);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function connectP2PClient(options: P2PClientOptions): Promise<P2PClient> {
|
|
194
|
+
const client = new P2PClient(options);
|
|
195
|
+
await client.connect();
|
|
196
|
+
return client;
|
|
197
|
+
}
|
package/src/p2p/index.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { P2PMessage, P2PMessageKind, P2PPayload } from './types.js';
|
|
2
|
+
import type { StatsData } from '../monitors/types.js';
|
|
3
|
+
|
|
4
|
+
const HEADER_SIZE = 4;
|
|
5
|
+
const MAX_MESSAGE_SIZE = 16 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
function buildMessage(kind: P2PMessageKind, payload: P2PPayload): Buffer {
|
|
8
|
+
const envelope: P2PMessage = { kind, payload };
|
|
9
|
+
const body = Buffer.from(JSON.stringify(envelope), 'utf-8');
|
|
10
|
+
const header = Buffer.alloc(HEADER_SIZE);
|
|
11
|
+
header.writeUInt32BE(body.length, 0);
|
|
12
|
+
return Buffer.concat([header, body]);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function encodeAuth(password: string): Buffer {
|
|
16
|
+
return buildMessage('auth', { password });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function encodeAuthOk(): Buffer {
|
|
20
|
+
return buildMessage('auth-ok', { ok: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function encodeAuthFail(reason: string): Buffer {
|
|
24
|
+
return buildMessage('auth-fail', { ok: false, reason });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function encodeData(stats: StatsData): Buffer {
|
|
28
|
+
return buildMessage('data', { stats });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function encodePing(): Buffer {
|
|
32
|
+
return buildMessage('ping', { ts: Date.now() });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function encodePong(ts: number): Buffer {
|
|
36
|
+
return buildMessage('pong', { ts });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function encodeDisconnect(reason: string): Buffer {
|
|
40
|
+
return buildMessage('disconnect', { reason });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function decodeMessage(buf: Buffer): P2PMessage {
|
|
44
|
+
const str = buf.toString('utf-8');
|
|
45
|
+
const msg: P2PMessage = JSON.parse(str);
|
|
46
|
+
return msg;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function readMessage(stream: { read(n: number): Buffer | null }): Buffer | null {
|
|
50
|
+
const header = stream.read(HEADER_SIZE);
|
|
51
|
+
if (!header || header.length < HEADER_SIZE) return null;
|
|
52
|
+
const length = header.readUInt32BE(0);
|
|
53
|
+
if (length > MAX_MESSAGE_SIZE) {
|
|
54
|
+
throw new Error(`Message too large: ${length} bytes`);
|
|
55
|
+
}
|
|
56
|
+
const body = stream.read(length);
|
|
57
|
+
if (!body || body.length < length) return null;
|
|
58
|
+
return body;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function readAll(stream: { read(n: number): Buffer | null }, total: number): Buffer {
|
|
62
|
+
const chunks: Buffer[] = [];
|
|
63
|
+
let remaining = total;
|
|
64
|
+
while (remaining > 0) {
|
|
65
|
+
const chunk = stream.read(remaining);
|
|
66
|
+
if (!chunk) break;
|
|
67
|
+
chunks.push(chunk);
|
|
68
|
+
remaining -= chunk.length;
|
|
69
|
+
}
|
|
70
|
+
return Buffer.concat(chunks);
|
|
71
|
+
}
|