socket-function 1.2.7 → 1.2.9
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/index.d.ts +20 -0
- package/package.json +1 -1
- package/src/forwardPort.d.ts +20 -0
- package/src/forwardPort.ts +131 -8
- package/src/webSocketServer.ts +88 -33
- package/test.ts +10 -154
package/index.d.ts
CHANGED
|
@@ -942,11 +942,31 @@ declare module "socket-function/src/formatting/logColors" {
|
|
|
942
942
|
}
|
|
943
943
|
|
|
944
944
|
declare module "socket-function/src/forwardPort" {
|
|
945
|
+
export interface PortMapping {
|
|
946
|
+
externalPort: number;
|
|
947
|
+
internalPort: number;
|
|
948
|
+
protocol: string;
|
|
949
|
+
/** The LAN client the mapping forwards to (NewInternalClient). */
|
|
950
|
+
internalClient: string;
|
|
951
|
+
/** Empty string means "any" remote host (the usual case). */
|
|
952
|
+
remoteHost: string;
|
|
953
|
+
enabled: boolean;
|
|
954
|
+
description: string;
|
|
955
|
+
/** Remaining lease in seconds; 0 means a permanent (static) mapping. */
|
|
956
|
+
leaseDuration: number;
|
|
957
|
+
}
|
|
958
|
+
/** Queries the router for every existing UPnP port mapping by walking
|
|
959
|
+
* GetGenericPortMappingEntry from index 0 until the gateway reports the index
|
|
960
|
+
* is out of range (SOAP error 713 / a non-200 response). */
|
|
961
|
+
export declare function listPortMappings(): Promise<PortMapping[]>;
|
|
945
962
|
export declare function forwardPort(config: {
|
|
946
963
|
externalPort: number;
|
|
947
964
|
internalPort: number;
|
|
948
965
|
duration?: number;
|
|
949
966
|
}): Promise<void>;
|
|
967
|
+
/** Our machine's LAN IP, as the router sees it — used to tell whether an existing port
|
|
968
|
+
* mapping points at us or at a different machine on the network. */
|
|
969
|
+
export declare function getLocalInternalIP(): string | undefined;
|
|
950
970
|
|
|
951
971
|
}
|
|
952
972
|
|
package/package.json
CHANGED
package/src/forwardPort.d.ts
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
|
+
export interface PortMapping {
|
|
2
|
+
externalPort: number;
|
|
3
|
+
internalPort: number;
|
|
4
|
+
protocol: string;
|
|
5
|
+
/** The LAN client the mapping forwards to (NewInternalClient). */
|
|
6
|
+
internalClient: string;
|
|
7
|
+
/** Empty string means "any" remote host (the usual case). */
|
|
8
|
+
remoteHost: string;
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
description: string;
|
|
11
|
+
/** Remaining lease in seconds; 0 means a permanent (static) mapping. */
|
|
12
|
+
leaseDuration: number;
|
|
13
|
+
}
|
|
14
|
+
/** Queries the router for every existing UPnP port mapping by walking
|
|
15
|
+
* GetGenericPortMappingEntry from index 0 until the gateway reports the index
|
|
16
|
+
* is out of range (SOAP error 713 / a non-200 response). */
|
|
17
|
+
export declare function listPortMappings(): Promise<PortMapping[]>;
|
|
1
18
|
export declare function forwardPort(config: {
|
|
2
19
|
externalPort: number;
|
|
3
20
|
internalPort: number;
|
|
4
21
|
duration?: number;
|
|
5
22
|
}): Promise<void>;
|
|
23
|
+
/** Our machine's LAN IP, as the router sees it — used to tell whether an existing port
|
|
24
|
+
* mapping points at us or at a different machine on the network. */
|
|
25
|
+
export declare function getLocalInternalIP(): string | undefined;
|
package/src/forwardPort.ts
CHANGED
|
@@ -6,6 +6,72 @@ import { timeInHour } from "./misc";
|
|
|
6
6
|
const SSDP_DISCOVER_MX = 2;
|
|
7
7
|
const SSDP_DISCOVER_MSG = `M-SEARCH * HTTP/1.1\r\nHOST: 239.255.255.250:1900\r\nMAN: "ssdp:discover"\r\nMX: ${SSDP_DISCOVER_MX}\r\nST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n\r\n`;
|
|
8
8
|
|
|
9
|
+
/** Resolves the UPnP Internet Gateway Device we can talk to, along with the local
|
|
10
|
+
* addressing needed to build SOAP requests against it. Shared by every operation
|
|
11
|
+
* that needs to reach the router's control endpoint. */
|
|
12
|
+
async function resolveGateway(): Promise<{
|
|
13
|
+
internalIP: string;
|
|
14
|
+
gatewayIP: string;
|
|
15
|
+
controlPort: number;
|
|
16
|
+
controlURLs: string[];
|
|
17
|
+
}> {
|
|
18
|
+
const localObj = getLocalInterfaceAddress();
|
|
19
|
+
if (!localObj) throw new Error("Could not find the local address / gateway");
|
|
20
|
+
|
|
21
|
+
const { internalIP, gatewayIP } = localObj;
|
|
22
|
+
console.log(`Local IP: ${internalIP}, Gateway IP: ${gatewayIP}`);
|
|
23
|
+
let gateway = await discoverGateway(internalIP);
|
|
24
|
+
let controlURLs = await getControlPaths(gateway);
|
|
25
|
+
let controlPort = Number(new URL(gateway).port);
|
|
26
|
+
|
|
27
|
+
return { internalIP, gatewayIP, controlPort, controlURLs };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PortMapping {
|
|
31
|
+
externalPort: number;
|
|
32
|
+
internalPort: number;
|
|
33
|
+
protocol: string;
|
|
34
|
+
/** The LAN client the mapping forwards to (NewInternalClient). */
|
|
35
|
+
internalClient: string;
|
|
36
|
+
/** Empty string means "any" remote host (the usual case). */
|
|
37
|
+
remoteHost: string;
|
|
38
|
+
enabled: boolean;
|
|
39
|
+
description: string;
|
|
40
|
+
/** Remaining lease in seconds; 0 means a permanent (static) mapping. */
|
|
41
|
+
leaseDuration: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Queries the router for every existing UPnP port mapping by walking
|
|
45
|
+
* GetGenericPortMappingEntry from index 0 until the gateway reports the index
|
|
46
|
+
* is out of range (SOAP error 713 / a non-200 response). */
|
|
47
|
+
export async function listPortMappings(): Promise<PortMapping[]> {
|
|
48
|
+
if (os.platform() === "linux") return [];
|
|
49
|
+
|
|
50
|
+
const { internalIP, gatewayIP, controlPort, controlURLs } = await resolveGateway();
|
|
51
|
+
|
|
52
|
+
let lastError: unknown;
|
|
53
|
+
for (let controlURL of controlURLs) {
|
|
54
|
+
try {
|
|
55
|
+
const mappings: PortMapping[] = [];
|
|
56
|
+
for (let index = 0; ; index++) {
|
|
57
|
+
const entry = await getGenericPortMappingEntry({
|
|
58
|
+
gatewayIP,
|
|
59
|
+
controlPort,
|
|
60
|
+
controlPath: controlURL,
|
|
61
|
+
index,
|
|
62
|
+
});
|
|
63
|
+
if (!entry) break;
|
|
64
|
+
mappings.push(entry);
|
|
65
|
+
}
|
|
66
|
+
return mappings;
|
|
67
|
+
} catch (e) {
|
|
68
|
+
lastError = e;
|
|
69
|
+
console.error(`Failed to list port mappings using controlURL ${controlURL}`, e);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`Failed to list port mappings, could not find a working controlURL. Last error: ${(lastError as Error)?.stack ?? lastError}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
9
75
|
export async function forwardPort(config: {
|
|
10
76
|
externalPort: number;
|
|
11
77
|
internalPort: number;
|
|
@@ -18,14 +84,7 @@ export async function forwardPort(config: {
|
|
|
18
84
|
const { externalPort, internalPort } = config;
|
|
19
85
|
let duration = config.duration ?? timeInHour;
|
|
20
86
|
|
|
21
|
-
const
|
|
22
|
-
if (!localObj) throw new Error("Could not find the local address / gateway");
|
|
23
|
-
|
|
24
|
-
const { internalIP, gatewayIP } = localObj;
|
|
25
|
-
console.log(`Local IP: ${internalIP}, Gateway IP: ${gatewayIP}`);
|
|
26
|
-
let gateway = await discoverGateway(internalIP);
|
|
27
|
-
let controlURLs = await getControlPaths(gateway);
|
|
28
|
-
let controlPort = Number(new URL(gateway).port);
|
|
87
|
+
const { internalIP, gatewayIP, controlPort, controlURLs } = await resolveGateway();
|
|
29
88
|
|
|
30
89
|
for (let controlURL of controlURLs) {
|
|
31
90
|
try {
|
|
@@ -49,6 +108,12 @@ export async function forwardPort(config: {
|
|
|
49
108
|
}
|
|
50
109
|
}
|
|
51
110
|
|
|
111
|
+
/** Our machine's LAN IP, as the router sees it — used to tell whether an existing port
|
|
112
|
+
* mapping points at us or at a different machine on the network. */
|
|
113
|
+
export function getLocalInternalIP(): string | undefined {
|
|
114
|
+
return getLocalInterfaceAddress()?.internalIP;
|
|
115
|
+
}
|
|
116
|
+
|
|
52
117
|
function getLocalInterfaceAddress(): { internalIP: string; gatewayIP: string; } | undefined {
|
|
53
118
|
let looksLikeRouter = (ip: string) => ip.startsWith("10.0.0") || ip.startsWith("10.0.1") || ip.startsWith("192.168.0");
|
|
54
119
|
|
|
@@ -216,3 +281,61 @@ async function createPortMapping(config: {
|
|
|
216
281
|
throw new Error(`Failed to create port mapping: ${data}`);
|
|
217
282
|
}
|
|
218
283
|
}
|
|
284
|
+
|
|
285
|
+
// UPnP returns this error code when we walk past the last mapping index, which is how
|
|
286
|
+
// we detect the end of the list rather than a real failure.
|
|
287
|
+
const UPNP_ARRAY_INDEX_INVALID = 713;
|
|
288
|
+
|
|
289
|
+
/** Fetches a single port mapping by its index. Returns undefined once the index is past
|
|
290
|
+
* the end of the table (the router's signal that there are no more entries). */
|
|
291
|
+
async function getGenericPortMappingEntry(config: {
|
|
292
|
+
gatewayIP: string;
|
|
293
|
+
controlPort: number;
|
|
294
|
+
controlPath: string;
|
|
295
|
+
index: number;
|
|
296
|
+
}): Promise<PortMapping | undefined> {
|
|
297
|
+
const { gatewayIP, controlPort, controlPath, index } = config;
|
|
298
|
+
const action = "\"urn:schemas-upnp-org:service:WANIPConnection:1#GetGenericPortMappingEntry\"";
|
|
299
|
+
|
|
300
|
+
const soapBody = `
|
|
301
|
+
<?xml version="1.0"?>
|
|
302
|
+
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
|
303
|
+
<s:Body>
|
|
304
|
+
<u:GetGenericPortMappingEntry xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
|
|
305
|
+
<NewPortMappingIndex>${index}</NewPortMappingIndex>
|
|
306
|
+
</u:GetGenericPortMappingEntry>
|
|
307
|
+
</s:Body>
|
|
308
|
+
</s:Envelope>
|
|
309
|
+
`;
|
|
310
|
+
|
|
311
|
+
const res = await fetch(`http://${gatewayIP}:${controlPort}${controlPath}`, {
|
|
312
|
+
method: "POST",
|
|
313
|
+
headers: {
|
|
314
|
+
"Content-Type": "text/xml; charset=\"utf-8\"",
|
|
315
|
+
"SOAPAction": action,
|
|
316
|
+
"Content-Length": Buffer.byteLength(soapBody) + "",
|
|
317
|
+
},
|
|
318
|
+
body: soapBody
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const data = await res.text();
|
|
322
|
+
if (res.status !== 200) {
|
|
323
|
+
let errorCode = Number(data.match(/<errorCode>(\d+)<\/errorCode>/)?.[1]);
|
|
324
|
+
if (errorCode === UPNP_ARRAY_INDEX_INVALID) {
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
throw new Error(`Failed to get port mapping entry ${index}: ${res.status} ${data}`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
let getField = (name: string) => data.match(new RegExp(`<${name}>(.*?)</${name}>`, "s"))?.[1] ?? "";
|
|
331
|
+
return {
|
|
332
|
+
externalPort: Number(getField("NewExternalPort")),
|
|
333
|
+
internalPort: Number(getField("NewInternalPort")),
|
|
334
|
+
protocol: getField("NewProtocol"),
|
|
335
|
+
internalClient: getField("NewInternalClient"),
|
|
336
|
+
remoteHost: getField("NewRemoteHost"),
|
|
337
|
+
enabled: getField("NewEnabled") === "1",
|
|
338
|
+
description: getField("NewPortMappingDescription"),
|
|
339
|
+
leaseDuration: Number(getField("NewLeaseDuration")),
|
|
340
|
+
};
|
|
341
|
+
}
|
package/src/webSocketServer.ts
CHANGED
|
@@ -12,13 +12,14 @@ import debugbreak from "debugbreak";
|
|
|
12
12
|
import { getNodeId } from "./nodeCache";
|
|
13
13
|
import crypto from "crypto";
|
|
14
14
|
import { Watchable, getRootDomain, timeInHour, timeInMinute } from "./misc";
|
|
15
|
-
import { delay, runInfinitePoll
|
|
15
|
+
import { delay, runInfinitePoll } from "./batching";
|
|
16
16
|
import { magenta, red } from "./formatting/logColors";
|
|
17
17
|
import { yellow } from "./formatting/logColors";
|
|
18
18
|
import { green } from "./formatting/logColors";
|
|
19
19
|
import { formatTime } from "./formatting/format";
|
|
20
20
|
import { getExternalIP, testTCPIsListening } from "./networking";
|
|
21
|
-
import { forwardPort } from "./forwardPort";
|
|
21
|
+
import { forwardPort, listPortMappings, getLocalInternalIP, PortMapping } from "./forwardPort";
|
|
22
|
+
import os from "os";
|
|
22
23
|
|
|
23
24
|
// When a requested port is taken and useAvailablePortIfPortInUse is set, we scan
|
|
24
25
|
// upwards from this base instead of binding a random OS-assigned port, so restarts
|
|
@@ -272,7 +273,7 @@ export async function startSocketServer(
|
|
|
272
273
|
}
|
|
273
274
|
}
|
|
274
275
|
|
|
275
|
-
if (!sniServers.has(sni)) {
|
|
276
|
+
if (!sniServers.has(sni) && sniServers.size > 0) {
|
|
276
277
|
console.warn(`No SNI server found for ${originalSNI}, using main server. SNI candidates ${Array.from(sniServers.keys()).join(", ")}`);
|
|
277
278
|
}
|
|
278
279
|
server = sniServers.get(sni) || mainHTTPSServer;
|
|
@@ -348,49 +349,103 @@ export async function startSocketServer(
|
|
|
348
349
|
});
|
|
349
350
|
}
|
|
350
351
|
|
|
351
|
-
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
352
|
+
// Frees the currently-bound listening socket so we can rebind on a different port
|
|
353
|
+
// (used when a locally-free port turns out to be unusable for forwarding).
|
|
354
|
+
async function releasePort(): Promise<void> {
|
|
355
|
+
await new Promise<void>((resolve, reject) => {
|
|
356
|
+
if (!realServer.listening) {
|
|
357
|
+
resolve();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
realServer.close(e => e ? reject(e) : resolve());
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Forwarding maps the external port to an equal internal port, so a candidate is only
|
|
365
|
+
// usable if we can also own its external mapping on the router. Skipped on linux,
|
|
366
|
+
// where forwardPort is a no-op anyway.
|
|
367
|
+
const doForward = !!(config.autoForwardPort && config.public && os.platform() !== "linux");
|
|
368
|
+
|
|
369
|
+
// Ensures the router's external mapping for `externalPort` belongs to us. Returns true
|
|
370
|
+
// if it's ours to keep (existing-and-ours → refresh the lease; free → create and
|
|
371
|
+
// confirm we won it), false if another machine owns it and we should try a new port.
|
|
372
|
+
async function claimPortForward(externalPort: number): Promise<boolean> {
|
|
373
|
+
const ourIP = getLocalInternalIP();
|
|
374
|
+
const matches = (m: PortMapping) => m.externalPort === externalPort && m.protocol.toUpperCase() === "TCP";
|
|
375
|
+
|
|
376
|
+
const existing = (await listPortMappings()).find(matches);
|
|
377
|
+
if (existing) {
|
|
378
|
+
if (ourIP && existing.internalClient === ourIP) {
|
|
379
|
+
await forwardPort({ externalPort, internalPort: externalPort });
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
console.log(magenta(`External port ${externalPort} is already forwarded to ${existing.internalClient}, trying another port`));
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Free right now — create the mapping, then re-read it to make sure another host
|
|
387
|
+
// didn't grab the same external port in the race between our list and our create.
|
|
388
|
+
await forwardPort({ externalPort, internalPort: externalPort });
|
|
389
|
+
const ours = (await listPortMappings()).find(matches);
|
|
390
|
+
if (!ours || (ourIP && ours.internalClient !== ourIP)) {
|
|
391
|
+
console.log(magenta(`Failed to claim external port ${externalPort} (now ${ours ? `forwarded to ${ours.internalClient}` : "still unmapped"}), trying another port`));
|
|
392
|
+
return false;
|
|
359
393
|
}
|
|
394
|
+
return true;
|
|
360
395
|
}
|
|
361
|
-
|
|
362
|
-
//
|
|
363
|
-
|
|
396
|
+
|
|
397
|
+
// Candidate ports: an explicitly requested port first (a falsy port, the default 0,
|
|
398
|
+
// means "no preference"), then a consistent upward scan so restarts are predictable.
|
|
399
|
+
function* candidatePorts(): Generator<number> {
|
|
400
|
+
if (port) yield port;
|
|
364
401
|
for (let candidate = PORT_SCAN_START; candidate < PORT_SCAN_START + PORT_SCAN_COUNT; candidate++) {
|
|
365
402
|
if (candidate === port) continue;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
403
|
+
yield candidate;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
let bound = false;
|
|
408
|
+
for (const candidate of candidatePorts()) {
|
|
409
|
+
if (!await tryListen(candidate)) {
|
|
410
|
+
if (candidate === config.port && !config.useAvailablePortIfPortInUse) {
|
|
411
|
+
throw new Error(`Port ${candidate} is already in use (set useAvailablePortIfPortInUse to fall back to another port)`);
|
|
370
412
|
}
|
|
413
|
+
continue;
|
|
371
414
|
}
|
|
372
|
-
|
|
373
|
-
|
|
415
|
+
// Locally bound. If we also forward, the external mapping must be ours too, or we
|
|
416
|
+
// release this port and keep scanning.
|
|
417
|
+
if (doForward) {
|
|
418
|
+
let claimed: boolean;
|
|
419
|
+
try {
|
|
420
|
+
claimed = await claimPortForward(candidate);
|
|
421
|
+
} catch (e) {
|
|
422
|
+
// UPnP unavailable (no gateway / discovery failed): fall back to best-effort
|
|
423
|
+
// forwarding rather than refusing to start.
|
|
424
|
+
console.error(red(`Could not verify forwarding for port ${candidate}, continuing best-effort: ${(e as Error).stack}`));
|
|
425
|
+
claimed = true;
|
|
426
|
+
}
|
|
427
|
+
if (!claimed) {
|
|
428
|
+
await releasePort();
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
374
431
|
}
|
|
432
|
+
port = candidate;
|
|
433
|
+
bound = true;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
if (!bound) {
|
|
437
|
+
throw new Error(`Could not find an available port in range ${PORT_SCAN_START}-${PORT_SCAN_START + PORT_SCAN_COUNT - 1} (requested ${config.port})`);
|
|
375
438
|
}
|
|
376
439
|
|
|
377
440
|
port = (realServer.address() as net.AddressInfo).port;
|
|
378
441
|
|
|
379
|
-
if (
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
// if (!isListening) {
|
|
383
|
-
// console.log(magenta(`Port ${port} is not externally reachable, trying to forward it`));
|
|
384
|
-
// await forwardPort({ externalPort: port, internalPort: port });
|
|
385
|
-
// }
|
|
386
|
-
// Even if they are listening, they might not stay listening. Forward every 8 hours
|
|
387
|
-
// (including at the start, in case the forward is about to expire).
|
|
388
|
-
async function forward() {
|
|
442
|
+
if (doForward) {
|
|
443
|
+
// The mapping is claimed above; keep refreshing the lease so it doesn't expire.
|
|
444
|
+
async function refreshForward() {
|
|
389
445
|
await forwardPort({ externalPort: port, internalPort: port });
|
|
390
|
-
console.log(magenta(`
|
|
446
|
+
console.log(magenta(`Refreshed port forward ${port} to our machine`));
|
|
391
447
|
}
|
|
392
|
-
|
|
393
|
-
runInfinitePollCallAtStart(timeInMinute * 30, forward).catch(e => console.error(red(`Error in port forwarding ${e.stack}`)));
|
|
448
|
+
runInfinitePoll(timeInMinute * 30, refreshForward);
|
|
394
449
|
}
|
|
395
450
|
|
|
396
451
|
let nodeId = getNodeId(getCommonName(config.cert), port);
|
package/test.ts
CHANGED
|
@@ -1,161 +1,17 @@
|
|
|
1
|
-
import { computeTweenedOffset, getTimeComponentsDetailed, waitForFirstTimeSync } from "./time/trueTimeShim";
|
|
2
1
|
import * as fs from "fs";
|
|
3
2
|
import * as path from "path";
|
|
3
|
+
import { listPortMappings } from "./src/forwardPort";
|
|
4
4
|
|
|
5
|
-
const SAMPLE_COUNT = 1000;
|
|
6
|
-
const SAMPLE_INTERVAL_MS = 10;
|
|
7
|
-
|
|
8
|
-
// Generate a simple numeric ID for this test run
|
|
9
|
-
const TEST_RUN_ID = Math.floor(Math.random() * 10000);
|
|
10
|
-
|
|
11
|
-
async function sampleMode() {
|
|
12
|
-
await waitForFirstTimeSync();
|
|
13
|
-
|
|
14
|
-
type Sample = {
|
|
15
|
-
id: number;
|
|
16
|
-
systemTime: number;
|
|
17
|
-
offset: number;
|
|
18
|
-
fromOffset: number;
|
|
19
|
-
toOffset: number;
|
|
20
|
-
fromTime: number;
|
|
21
|
-
toTime: number;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const samples: Sample[] = [];
|
|
25
|
-
|
|
26
|
-
console.log(`[Test ID: ${TEST_RUN_ID}] Sampling ${SAMPLE_COUNT} time components...`);
|
|
27
|
-
for (let i = 0; i < SAMPLE_COUNT; i++) {
|
|
28
|
-
const detailed = getTimeComponentsDetailed();
|
|
29
|
-
|
|
30
|
-
// Calculate the tween using the same systemTime
|
|
31
|
-
const offset = computeTweenedOffset(detailed);
|
|
32
|
-
|
|
33
|
-
samples.push({
|
|
34
|
-
id: TEST_RUN_ID,
|
|
35
|
-
systemTime: detailed.systemTime,
|
|
36
|
-
offset,
|
|
37
|
-
fromOffset: detailed.fromOffset,
|
|
38
|
-
toOffset: detailed.toOffset,
|
|
39
|
-
fromTime: detailed.fromTime,
|
|
40
|
-
toTime: detailed.toTime,
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
if (SAMPLE_INTERVAL_MS > 0) {
|
|
44
|
-
await new Promise(resolve => setTimeout(resolve, SAMPLE_INTERVAL_MS));
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Write to file with unique name
|
|
49
|
-
const distDir = path.join(__dirname, "dist");
|
|
50
|
-
if (!fs.existsSync(distDir)) {
|
|
51
|
-
fs.mkdirSync(distDir, { recursive: true });
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const filename = path.join(distDir, `time-samples-${Date.now()}-${process.pid}.json`);
|
|
55
|
-
fs.writeFileSync(filename, JSON.stringify(samples, null, 2));
|
|
56
|
-
|
|
57
|
-
console.log(`[Test ID: ${TEST_RUN_ID}] Wrote ${samples.length} samples to ${filename}`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function verifyMode() {
|
|
61
|
-
const distDir = path.join(__dirname, "dist");
|
|
62
|
-
if (!fs.existsSync(distDir)) {
|
|
63
|
-
console.error("No dist directory found. Run sampling mode first.");
|
|
64
|
-
process.exit(1);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const files = fs.readdirSync(distDir)
|
|
68
|
-
.filter(f => f.startsWith("time-samples-") && f.endsWith(".json"))
|
|
69
|
-
.map(f => path.join(distDir, f));
|
|
70
|
-
|
|
71
|
-
if (files.length === 0) {
|
|
72
|
-
console.error("No sample files found. Run sampling mode first.");
|
|
73
|
-
process.exit(1);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
console.log(`Found ${files.length} sample files`);
|
|
77
|
-
|
|
78
|
-
type Sample = {
|
|
79
|
-
id: number;
|
|
80
|
-
systemTime: number;
|
|
81
|
-
offset: number;
|
|
82
|
-
fromOffset: number;
|
|
83
|
-
toOffset: number;
|
|
84
|
-
fromTime: number;
|
|
85
|
-
toTime: number;
|
|
86
|
-
file: string;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
// Load all samples from all files
|
|
90
|
-
const allSamples: Sample[] = [];
|
|
91
|
-
|
|
92
|
-
for (const file of files) {
|
|
93
|
-
const content = fs.readFileSync(file, "utf-8");
|
|
94
|
-
const samples = JSON.parse(content);
|
|
95
|
-
for (const sample of samples) {
|
|
96
|
-
allSamples.push({ ...sample, file: path.basename(file) });
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
console.log(`Loaded ${allSamples.length} total samples`);
|
|
101
|
-
|
|
102
|
-
// Sort by systemTime
|
|
103
|
-
allSamples.sort((a, b) => a.systemTime - b.systemTime);
|
|
104
|
-
|
|
105
|
-
// Verify that systemTime + offset is monotonically increasing
|
|
106
|
-
let errors = 0;
|
|
107
|
-
let lastTrueTime = -Infinity;
|
|
108
|
-
|
|
109
|
-
for (let i = 0; i < allSamples.length; i++) {
|
|
110
|
-
const sample = allSamples[i];
|
|
111
|
-
const trueTime = sample.systemTime + sample.offset;
|
|
112
|
-
|
|
113
|
-
if (trueTime < lastTrueTime) {
|
|
114
|
-
errors++;
|
|
115
|
-
const prev = allSamples[i - 1];
|
|
116
|
-
const prevTrueTime = prev.systemTime + prev.offset;
|
|
117
|
-
|
|
118
|
-
console.error(`\nERROR at index ${i}: trueTime went backwards!`);
|
|
119
|
-
console.error(` Previous [ID: ${prev.id}, file: ${prev.file}]:`);
|
|
120
|
-
console.error(` systemTime: ${prev.systemTime}`);
|
|
121
|
-
console.error(` offset: ${prev.offset}`);
|
|
122
|
-
console.error(` trueTime: ${prevTrueTime}`);
|
|
123
|
-
console.error(` tween: ${prev.fromOffset} -> ${prev.toOffset}`);
|
|
124
|
-
console.error(` timeWindow: ${prev.fromTime} -> ${prev.toTime}`);
|
|
125
|
-
console.error(` Current [ID: ${sample.id}, file: ${sample.file}]:`);
|
|
126
|
-
console.error(` systemTime: ${sample.systemTime}`);
|
|
127
|
-
console.error(` offset: ${sample.offset}`);
|
|
128
|
-
console.error(` trueTime: ${trueTime}`);
|
|
129
|
-
console.error(` tween: ${sample.fromOffset} -> ${sample.toOffset}`);
|
|
130
|
-
console.error(` timeWindow: ${sample.fromTime} -> ${sample.toTime}`);
|
|
131
|
-
console.error(` Difference: ${trueTime - lastTrueTime}ms`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
lastTrueTime = trueTime;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (errors === 0) {
|
|
138
|
-
console.log(`✓ SUCCESS: All ${allSamples.length} samples are correctly ordered!`);
|
|
139
|
-
console.log(` Time range: ${allSamples[0].systemTime} to ${allSamples[allSamples.length - 1].systemTime}`);
|
|
140
|
-
console.log(` Duration: ${allSamples[allSamples.length - 1].systemTime - allSamples[0].systemTime}ms`);
|
|
141
|
-
} else {
|
|
142
|
-
console.error(`✗ FAILED: Found ${errors} ordering violations`);
|
|
143
|
-
process.exit(1);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
5
|
|
|
147
6
|
async function main() {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
} else {
|
|
157
|
-
await sampleMode();
|
|
158
|
-
}
|
|
7
|
+
let mappings = await listPortMappings();
|
|
8
|
+
console.log(`Found ${mappings.length} port mapping(s):`);
|
|
9
|
+
for (let mapping of mappings) {
|
|
10
|
+
let host = mapping.remoteHost || "*";
|
|
11
|
+
let lease = mapping.leaseDuration === 0 ? "permanent" : `${mapping.leaseDuration}s left`;
|
|
12
|
+
console.log(` ${mapping.protocol} ${host}:${mapping.externalPort} -> ${mapping.internalClient}:${mapping.internalPort} (${mapping.enabled ? "enabled" : "disabled"}, ${lease})${mapping.description ? ` "${mapping.description}"` : ""}`);
|
|
13
|
+
}
|
|
14
|
+
console.table(mappings);
|
|
159
15
|
}
|
|
160
16
|
|
|
161
|
-
main().catch(e => console.error(e)).finally(() => process.exit(0));
|
|
17
|
+
main().catch(e => console.error(e)).finally(() => process.exit(0));
|