pinggy 0.3.5 → 0.3.7
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 +1 -1
- package/dist/chunk-65R2GMKQ.js +2101 -0
- package/dist/index.cjs +1798 -1349
- package/dist/index.d.cts +616 -0
- package/dist/index.d.ts +616 -0
- package/dist/index.js +24 -2
- package/dist/{main-K44C44NW.js → main-2QDG7PWL.js} +166 -1705
- package/package.json +2 -3
- package/.github/workflows/npm-publish-github-packages.yml +0 -34
- package/.github/workflows/publish-binaries.yml +0 -223
- package/Makefile +0 -4
- package/caxa_build.js +0 -24
- package/dist/chunk-T5ESYDJY.js +0 -121
- package/ent.plist +0 -14
- package/jest.config.js +0 -19
- package/src/_tests_/build_config.test.ts +0 -91
- package/src/cli/buildConfig.ts +0 -535
- package/src/cli/defaults.ts +0 -20
- package/src/cli/extendedOptions.ts +0 -153
- package/src/cli/help.ts +0 -43
- package/src/cli/options.ts +0 -50
- package/src/cli/starCli.ts +0 -229
- package/src/index.ts +0 -31
- package/src/logger.ts +0 -138
- package/src/main.ts +0 -87
- package/src/remote_management/handler.ts +0 -244
- package/src/remote_management/remoteManagement.ts +0 -226
- package/src/remote_management/remote_schema.ts +0 -176
- package/src/remote_management/websocket_handlers.ts +0 -180
- package/src/tui/blessed/TunnelTui.ts +0 -340
- package/src/tui/blessed/components/DisplayUpdaters.ts +0 -189
- package/src/tui/blessed/components/KeyBindings.ts +0 -236
- package/src/tui/blessed/components/Modals.ts +0 -302
- package/src/tui/blessed/components/UIComponents.ts +0 -306
- package/src/tui/blessed/components/index.ts +0 -4
- package/src/tui/blessed/config.ts +0 -53
- package/src/tui/blessed/headerFetcher.ts +0 -42
- package/src/tui/blessed/index.ts +0 -2
- package/src/tui/blessed/qrCodeGenerator.ts +0 -20
- package/src/tui/blessed/webDebuggerConnection.ts +0 -128
- package/src/tui/ink/asciArt.ts +0 -7
- package/src/tui/ink/hooks/useQrCodes.ts +0 -27
- package/src/tui/ink/hooks/useReqResHeaders.ts +0 -27
- package/src/tui/ink/hooks/useTerminalSize.ts +0 -26
- package/src/tui/ink/hooks/useTerminalStats.ts +0 -24
- package/src/tui/ink/hooks/useWebDebugger.ts +0 -98
- package/src/tui/ink/index.tsx +0 -243
- package/src/tui/ink/layout/Borders.tsx +0 -15
- package/src/tui/ink/layout/Container.tsx +0 -15
- package/src/tui/ink/sections/DebuggerDetailModal.tsx +0 -53
- package/src/tui/ink/sections/KeyBindings.tsx +0 -58
- package/src/tui/ink/sections/QrCodeSection.tsx +0 -28
- package/src/tui/ink/sections/StatsSection.tsx +0 -20
- package/src/tui/ink/sections/URLsSection.tsx +0 -53
- package/src/tui/ink/utils/utils.ts +0 -35
- package/src/tui/spinner/spinner.ts +0 -64
- package/src/tunnel_manager/TunnelManager.ts +0 -1212
- package/src/types.ts +0 -255
- package/src/utils/FileServer.ts +0 -112
- package/src/utils/detect_vc_redist_on_windows.ts +0 -111
- package/src/utils/getFreePort.ts +0 -41
- package/src/utils/htmlTemplates.ts +0 -146
- package/src/utils/parseArgs.ts +0 -79
- package/src/utils/printer.ts +0 -81
- package/src/utils/util.ts +0 -18
- package/src/workers/file_serve_worker.ts +0 -33
- package/tsconfig.json +0 -17
- package/tsup.config.ts +0 -12
|
@@ -1,1212 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Manages the lifecycle and state of multiple tunnel instances.
|
|
3
|
-
* Implements the Singleton pattern to ensure only one tunnel manager exists in the application.
|
|
4
|
-
*
|
|
5
|
-
* @remarks
|
|
6
|
-
* This class provides comprehensive tunnel management capabilities including:
|
|
7
|
-
* - Creation and initialization of tunnels
|
|
8
|
-
* - Starting and stopping tunnels
|
|
9
|
-
* - Managing tunnel configurations and states
|
|
10
|
-
* - Handling additional forwarding rules
|
|
11
|
-
* - Monitoring tunnel status
|
|
12
|
-
*
|
|
13
|
-
* @sealed
|
|
14
|
-
* @singleton
|
|
15
|
-
*/
|
|
16
|
-
import { ForwardingEntry, pinggy, TunnelType, type PinggyOptions, type TunnelInstance, type TunnelUsageType } from "@pinggy/pinggy";
|
|
17
|
-
import { logger } from "../logger.js";
|
|
18
|
-
import { AdditionalForwarding, TunnelWarningCode, Warning } from "../types.js";
|
|
19
|
-
import path from "node:path";
|
|
20
|
-
import { Worker } from "node:worker_threads";
|
|
21
|
-
import { fileURLToPath } from "node:url";
|
|
22
|
-
import CLIPrinter from "../utils/printer.js";
|
|
23
|
-
import { getRandomId, isValidPort } from "../utils/util.js";
|
|
24
|
-
|
|
25
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
26
|
-
const __dirname = path.dirname(__filename);
|
|
27
|
-
|
|
28
|
-
export interface ManagedTunnel {
|
|
29
|
-
tunnelid: string;
|
|
30
|
-
configid: string;
|
|
31
|
-
tunnelName?: string;
|
|
32
|
-
instance: TunnelInstance;
|
|
33
|
-
tunnelConfig?: PinggyOptions;
|
|
34
|
-
configWithForwarding?: PinggyOptions;
|
|
35
|
-
additionalForwarding?: AdditionalForwarding[];
|
|
36
|
-
serveWorker?: Worker | null;
|
|
37
|
-
warnings?: Warning[];
|
|
38
|
-
serve?: string;
|
|
39
|
-
isStopped?: boolean;
|
|
40
|
-
createdAt?: string;
|
|
41
|
-
startedAt?: string | null;
|
|
42
|
-
stoppedAt?: string | null;
|
|
43
|
-
autoReconnect?: boolean;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface TunnelList {
|
|
47
|
-
tunnelid: string;
|
|
48
|
-
configid: string;
|
|
49
|
-
tunnelName?: string;
|
|
50
|
-
tunnelConfig: PinggyOptions;
|
|
51
|
-
remoteurls: string[];
|
|
52
|
-
additionalForwarding?: AdditionalForwarding[];
|
|
53
|
-
serve?: string;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export type StatsListener = (tunnelId: string, stats: TunnelUsageType) => void;
|
|
57
|
-
export type ErrorListener = (tunnelId: string, errorMsg: string, isFatal: boolean) => void;
|
|
58
|
-
export type DisconnectListener = (tunnelId: string, error: string, messages: string[]) => void;
|
|
59
|
-
export type TunnelWorkerErrorListner = (tunnelid: string, error: Error) => void;
|
|
60
|
-
export type StartListener = (tunnelId: string, urls: string[]) => void;
|
|
61
|
-
|
|
62
|
-
export interface ITunnelManager {
|
|
63
|
-
createTunnel(config: (PinggyOptions & { configid: string; tunnelid?: string; tunnelName?: string }) & { additionalForwarding?: AdditionalForwarding[] }): Promise<ManagedTunnel>;
|
|
64
|
-
startTunnel(tunnelId: string): Promise<string[]>;
|
|
65
|
-
stopTunnel(tunnelId: string): { configid: string; tunnelid: string };
|
|
66
|
-
stopAllTunnels(): void;
|
|
67
|
-
getTunnelUrls(tunnelId: string): Promise<string[]>;
|
|
68
|
-
getAllTunnels(): Promise<TunnelList[]>;
|
|
69
|
-
getTunnelStatus(tunnelId: string): Promise<string>;
|
|
70
|
-
getTunnelInstance(configId?: string, tunnelId?: string): TunnelInstance;
|
|
71
|
-
getTunnelConfig(configId?: string, tunnelId?: string): Promise<PinggyOptions>;
|
|
72
|
-
restartTunnel(tunnelId: string): Promise<void>;
|
|
73
|
-
updateConfig(
|
|
74
|
-
newConfig: PinggyOptions & { configid: string; additionalForwarding?: AdditionalForwarding[], tunnelName?: string },
|
|
75
|
-
): Promise<ManagedTunnel>;
|
|
76
|
-
getManagedTunnel(configId?: string, tunnelId?: string): ManagedTunnel;
|
|
77
|
-
getTunnelGreetMessage(tunnelId: string): Promise<string | null>;
|
|
78
|
-
getTunnelStats(tunnelId: string): TunnelUsageType[] | null;
|
|
79
|
-
getLatestTunnelStats(tunnelId: string): TunnelUsageType | null;
|
|
80
|
-
registerStatsListener(tunnelId: string, listener: StatsListener): Promise<[string, string]>;
|
|
81
|
-
registerErrorListener(tunnelId: string, listener: ErrorListener): Promise<string>;
|
|
82
|
-
registerWorkerErrorListner(tunnelId: string, listener: TunnelWorkerErrorListner): void;
|
|
83
|
-
registerStartListener(tunnelId: string, listener: StartListener): Promise<string>;
|
|
84
|
-
deregisterErrorListener(tunnelId: string, listenerId: string): void;
|
|
85
|
-
registerDisconnectListener(tunnelId: string, listener: DisconnectListener): Promise<string>;
|
|
86
|
-
deregisterDisconnectListener(tunnelId: string, listenerId: string): void;
|
|
87
|
-
deregisterStatsListener(tunnelId: string, listenerId: string): void;
|
|
88
|
-
getLocalserverTlsInfo(tunnelId: string): Promise<string | boolean>;
|
|
89
|
-
removeStoppedTunnelByTunnelId(tunnelId: string): boolean;
|
|
90
|
-
removeStoppedTunnelByConfigId(configId: string): boolean;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export class TunnelManager implements ITunnelManager {
|
|
94
|
-
|
|
95
|
-
private static instance: TunnelManager;
|
|
96
|
-
private tunnelsByTunnelId: Map<string, ManagedTunnel> = new Map();
|
|
97
|
-
private tunnelsByConfigId: Map<string, ManagedTunnel> = new Map();
|
|
98
|
-
private tunnelStats: Map<string, TunnelUsageType[]> = new Map();
|
|
99
|
-
private tunnelStatsListeners: Map<string, Map<string, StatsListener>> = new Map();
|
|
100
|
-
private tunnelErrorListeners: Map<string, Map<string, ErrorListener>> = new Map();
|
|
101
|
-
private tunnelDisconnectListeners: Map<string, Map<string, DisconnectListener>> = new Map();
|
|
102
|
-
private tunnelWorkerErrorListeners: Map<string, Map<string, TunnelWorkerErrorListner>> = new Map();
|
|
103
|
-
private tunnelStartListeners: Map<string, Map<string, StartListener>> = new Map();
|
|
104
|
-
|
|
105
|
-
private constructor() { }
|
|
106
|
-
|
|
107
|
-
public static getInstance(): TunnelManager {
|
|
108
|
-
if (!TunnelManager.instance) {
|
|
109
|
-
TunnelManager.instance = new TunnelManager();
|
|
110
|
-
}
|
|
111
|
-
return TunnelManager.instance;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Creates a new managed tunnel instance with the given configuration.
|
|
116
|
-
* Builds the config with forwarding rules and creates the tunnel instance.
|
|
117
|
-
*
|
|
118
|
-
* @param config - The tunnel configuration options
|
|
119
|
-
* @param config.configid - Unique identifier for the tunnel configuration
|
|
120
|
-
* @param config.tunnelid - Optional custom tunnel identifier. If not provided, a random UUID will be generated
|
|
121
|
-
* @param config.additionalForwarding - Optional array of additional forwarding configurations
|
|
122
|
-
*
|
|
123
|
-
* @throws {Error} When configId is invalid or empty
|
|
124
|
-
* @throws {Error} When a tunnel with the given configId already exists
|
|
125
|
-
*
|
|
126
|
-
* @returns {ManagedTunnel} A new managed tunnel instance containing the tunnel details,
|
|
127
|
-
* status information, and statistics
|
|
128
|
-
*/
|
|
129
|
-
async createTunnel(config: (PinggyOptions & { tunnelType: string[] | undefined } & { configid: string; tunnelid?: string; tunnelName?: string }) & { additionalForwarding?: AdditionalForwarding[] } & { serve?: string }): Promise<ManagedTunnel> {
|
|
130
|
-
const { configid, additionalForwarding, tunnelName } = config;
|
|
131
|
-
if (configid === undefined || configid.trim().length === 0) {
|
|
132
|
-
throw new Error(`Invalid configId: "${configid}"`);
|
|
133
|
-
}
|
|
134
|
-
if (this.tunnelsByConfigId.has(configid)) {
|
|
135
|
-
throw new Error(`Tunnel with configId "${configid}" already exists`);
|
|
136
|
-
}
|
|
137
|
-
const tunnelid = config.tunnelid || getRandomId();
|
|
138
|
-
|
|
139
|
-
// Build the config with forwarding rules
|
|
140
|
-
const configWithForwarding = this.buildPinggyConfig(config, additionalForwarding);
|
|
141
|
-
|
|
142
|
-
// Create the tunnel
|
|
143
|
-
return this._createTunnelWithProcessedConfig({
|
|
144
|
-
configid,
|
|
145
|
-
tunnelid,
|
|
146
|
-
tunnelName,
|
|
147
|
-
originalConfig: config,
|
|
148
|
-
configWithForwarding,
|
|
149
|
-
additionalForwarding,
|
|
150
|
-
serve: config.serve,
|
|
151
|
-
autoReconnect: config.autoReconnect !== undefined ? config.autoReconnect : false,
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Internal method to create a tunnel with an already-processed configuration.
|
|
157
|
-
* This is used by createTunnel, restartTunnel, and updateConfig to avoid config processing.
|
|
158
|
-
*
|
|
159
|
-
* @param params - Configuration parameters with already-processed forwarding rules
|
|
160
|
-
* @returns The created ManagedTunnel instance
|
|
161
|
-
* @private
|
|
162
|
-
*/
|
|
163
|
-
private async _createTunnelWithProcessedConfig(params: {
|
|
164
|
-
configid: string;
|
|
165
|
-
tunnelid: string;
|
|
166
|
-
tunnelName?: string;
|
|
167
|
-
originalConfig: PinggyOptions;
|
|
168
|
-
configWithForwarding: PinggyOptions;
|
|
169
|
-
additionalForwarding?: AdditionalForwarding[];
|
|
170
|
-
serve?: string;
|
|
171
|
-
autoReconnect: boolean;
|
|
172
|
-
}): Promise<ManagedTunnel> {
|
|
173
|
-
let instance;
|
|
174
|
-
try {
|
|
175
|
-
instance = await pinggy.createTunnel(params.configWithForwarding);
|
|
176
|
-
} catch (e) {
|
|
177
|
-
logger.error("Error creating tunnel instance:", e);
|
|
178
|
-
throw e;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const now = new Date().toISOString();
|
|
182
|
-
|
|
183
|
-
const managed: ManagedTunnel = {
|
|
184
|
-
tunnelid: params.tunnelid,
|
|
185
|
-
configid: params.configid,
|
|
186
|
-
tunnelName: params.tunnelName,
|
|
187
|
-
instance,
|
|
188
|
-
tunnelConfig: params.originalConfig,
|
|
189
|
-
configWithForwarding: params.configWithForwarding,
|
|
190
|
-
additionalForwarding: params.additionalForwarding,
|
|
191
|
-
serve: params.serve,
|
|
192
|
-
warnings: [],
|
|
193
|
-
isStopped: false,
|
|
194
|
-
createdAt: now,
|
|
195
|
-
startedAt: null,
|
|
196
|
-
stoppedAt: null,
|
|
197
|
-
autoReconnect: params.autoReconnect,
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Register stats & error callback for this tunnel
|
|
201
|
-
instance.setTunnelEstablishedCallback(({ }) => {
|
|
202
|
-
managed.startedAt = new Date().toISOString();
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
this.setupStatsCallback(params.tunnelid, managed);
|
|
206
|
-
this.setupErrorCallback(params.tunnelid, managed);
|
|
207
|
-
this.setupDisconnectCallback(params.tunnelid, managed);
|
|
208
|
-
this.setUpTunnelWorkerErrorCallback(params.tunnelid, managed)
|
|
209
|
-
|
|
210
|
-
this.tunnelsByTunnelId.set(params.tunnelid, managed);
|
|
211
|
-
this.tunnelsByConfigId.set(params.configid, managed);
|
|
212
|
-
logger.info("Tunnel created", { configid: params.configid, tunnelid: params.tunnelid });
|
|
213
|
-
return managed;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Builds the Pinggy configuration by merging the default forwarding rule
|
|
218
|
-
* with additional forwarding rules from additionalForwarding array.
|
|
219
|
-
*
|
|
220
|
-
* @param config - The base Pinggy configuration
|
|
221
|
-
* @param additionalForwarding - Optional array of additional forwarding rules
|
|
222
|
-
* @returns Modified PinggyOptions
|
|
223
|
-
*/
|
|
224
|
-
private buildPinggyConfig(
|
|
225
|
-
config: PinggyOptions & { tunnelType: string[] | undefined } & { configid: string; tunnelid?: string; tunnelName?: string } & { serve?: string },
|
|
226
|
-
additionalForwarding?: AdditionalForwarding[]
|
|
227
|
-
): PinggyOptions {
|
|
228
|
-
const forwardingRules: any[] = [];
|
|
229
|
-
|
|
230
|
-
// Add the default forwarding rule
|
|
231
|
-
if (config.forwarding) {
|
|
232
|
-
forwardingRules.push({
|
|
233
|
-
type: (config.tunnelType && config.tunnelType[0]) || "http",
|
|
234
|
-
address: config.forwarding,
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Add additional forwarding rules
|
|
239
|
-
if (Array.isArray(additionalForwarding) && additionalForwarding.length > 0) {
|
|
240
|
-
for (const rule of additionalForwarding) {
|
|
241
|
-
if (rule && rule.localDomain && rule.localPort && rule.remoteDomain && isValidPort(rule.localPort)) {
|
|
242
|
-
const forwardingRule: ForwardingEntry = {
|
|
243
|
-
type: rule.protocol as TunnelType, // In Future we can make this dynamic based on user input
|
|
244
|
-
address:`${rule.localDomain}:${rule.localPort}`,
|
|
245
|
-
listenAddress: rule.remotePort && isValidPort(rule.remotePort) ? `${rule.remoteDomain}:${rule.remotePort}` : rule.remoteDomain,
|
|
246
|
-
};
|
|
247
|
-
forwardingRules.push(forwardingRule);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return {
|
|
253
|
-
...config,
|
|
254
|
-
forwarding: forwardingRules.length > 0 ? forwardingRules : config.forwarding,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Start a tunnel that was created but not yet started
|
|
260
|
-
*/
|
|
261
|
-
async startTunnel(tunnelId: string): Promise<string[]> {
|
|
262
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
263
|
-
if (!managed) throw new Error(`Tunnel with id "${tunnelId}" not found`);
|
|
264
|
-
|
|
265
|
-
logger.info("Starting tunnel", { tunnelId });
|
|
266
|
-
let urls: string[];
|
|
267
|
-
try {
|
|
268
|
-
urls = await managed.instance.start();
|
|
269
|
-
} catch (error) {
|
|
270
|
-
logger.error("Failed to start tunnel", { tunnelId, error });
|
|
271
|
-
throw error;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
logger.info("Tunnel started", { tunnelId, urls });
|
|
275
|
-
|
|
276
|
-
if (managed.serve) {
|
|
277
|
-
this.startStaticFileServer(managed)
|
|
278
|
-
}
|
|
279
|
-
// Notify start listeners( now used to render tui again)
|
|
280
|
-
try {
|
|
281
|
-
const startListeners = this.tunnelStartListeners.get(tunnelId);
|
|
282
|
-
if (startListeners) {
|
|
283
|
-
for (const [id, listener] of startListeners) {
|
|
284
|
-
try {
|
|
285
|
-
listener(tunnelId, urls);
|
|
286
|
-
} catch (err) {
|
|
287
|
-
logger.debug("Error in start-listener callback", { listenerId: id, tunnelId, err });
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
} catch (e) {
|
|
292
|
-
logger.warn("Failed to notify start listeners", { tunnelId, e });
|
|
293
|
-
}
|
|
294
|
-
return urls;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Stops a running tunnel and updates its status.
|
|
299
|
-
*
|
|
300
|
-
* @param tunnelId - The unique identifier of the tunnel to stop
|
|
301
|
-
* @throws {Error} If the tunnel with the given tunnelId is not found
|
|
302
|
-
* @remarks
|
|
303
|
-
* - Clears the tunnel's remote URLs
|
|
304
|
-
* - Updates the tunnel's state to Exited if stopped successfully
|
|
305
|
-
* - Logs the stop operation with tunnelId and configId
|
|
306
|
-
*/
|
|
307
|
-
stopTunnel(tunnelId: string): { configid: string; tunnelid: string } {
|
|
308
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
309
|
-
if (!managed) throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
310
|
-
|
|
311
|
-
logger.info("Stopping tunnel", { tunnelId, configId: managed.configid });
|
|
312
|
-
try {
|
|
313
|
-
managed.instance.stop();
|
|
314
|
-
if (managed.serveWorker) {
|
|
315
|
-
logger.info("terminating serveWorker");
|
|
316
|
-
managed.serveWorker.terminate();
|
|
317
|
-
}
|
|
318
|
-
this.tunnelStats.delete(tunnelId);
|
|
319
|
-
this.tunnelStatsListeners.delete(tunnelId);
|
|
320
|
-
// Remove runtime-only tracking data
|
|
321
|
-
this.tunnelStats.delete(tunnelId);
|
|
322
|
-
this.tunnelStatsListeners.delete(tunnelId);
|
|
323
|
-
this.tunnelErrorListeners.delete(tunnelId);
|
|
324
|
-
this.tunnelDisconnectListeners.delete(tunnelId);
|
|
325
|
-
this.tunnelWorkerErrorListeners.delete(tunnelId);
|
|
326
|
-
this.tunnelStartListeners.delete(tunnelId);
|
|
327
|
-
managed.serveWorker = null;
|
|
328
|
-
managed.warnings = managed.warnings ?? [];
|
|
329
|
-
managed.isStopped = true;
|
|
330
|
-
managed.stoppedAt = new Date().toISOString();
|
|
331
|
-
logger.info("Tunnel stopped", { tunnelId, configId: managed.configid });
|
|
332
|
-
return { configid: managed.configid, tunnelid: managed.tunnelid };
|
|
333
|
-
} catch (error) {
|
|
334
|
-
logger.error("Failed to stop tunnel", { tunnelId, error });
|
|
335
|
-
throw error;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Get all public URLs for a tunnel
|
|
341
|
-
*/
|
|
342
|
-
async getTunnelUrls(tunnelId: string): Promise<string[]> {
|
|
343
|
-
try {
|
|
344
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
345
|
-
if (!managed || managed.isStopped) {
|
|
346
|
-
logger.error(`Tunnel "${tunnelId}" not found when fetching URLs`);
|
|
347
|
-
return [];
|
|
348
|
-
}
|
|
349
|
-
const urls = await managed.instance.urls();
|
|
350
|
-
logger.debug("Queried tunnel URLs", { tunnelId, urls });
|
|
351
|
-
return urls;
|
|
352
|
-
} catch (error) {
|
|
353
|
-
logger.error("Error fetching tunnel URLs", { tunnelId, error });
|
|
354
|
-
throw error;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Get all TunnelStatus currently managed by this TunnelManager
|
|
360
|
-
* @returns An array of all TunnelStatus objects
|
|
361
|
-
*/
|
|
362
|
-
async getAllTunnels(): Promise<TunnelList[]> {
|
|
363
|
-
|
|
364
|
-
try {
|
|
365
|
-
const tunnelList = await Promise.all(Array.from(this.tunnelsByTunnelId.values()).map(async (tunnel) => {
|
|
366
|
-
return {
|
|
367
|
-
tunnelid: tunnel.tunnelid,
|
|
368
|
-
configid: tunnel.configid,
|
|
369
|
-
tunnelName: tunnel.tunnelName,
|
|
370
|
-
tunnelConfig: tunnel.tunnelConfig!,
|
|
371
|
-
remoteurls: !tunnel.isStopped ? await this.getTunnelUrls(tunnel.tunnelid) : [],
|
|
372
|
-
additionalForwarding: tunnel.additionalForwarding,
|
|
373
|
-
serve: tunnel.serve,
|
|
374
|
-
};
|
|
375
|
-
}));
|
|
376
|
-
return tunnelList;
|
|
377
|
-
} catch (err) {
|
|
378
|
-
logger.error("Error fetching tunnels", { error: err });
|
|
379
|
-
return [];
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* Get status of a tunnel
|
|
385
|
-
*/
|
|
386
|
-
async getTunnelStatus(tunnelId: string): Promise<string> {
|
|
387
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
388
|
-
if (!managed) {
|
|
389
|
-
logger.error(`Tunnel "${tunnelId}" not found when fetching status`);
|
|
390
|
-
throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
391
|
-
}
|
|
392
|
-
if (managed.isStopped) {
|
|
393
|
-
return 'exited';
|
|
394
|
-
}
|
|
395
|
-
const status = await managed.instance.getStatus();
|
|
396
|
-
logger.debug("Queried tunnel status", { tunnelId, status });
|
|
397
|
-
return status;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Stop all tunnels
|
|
402
|
-
*/
|
|
403
|
-
stopAllTunnels(): void {
|
|
404
|
-
for (const { instance } of this.tunnelsByTunnelId.values()) {
|
|
405
|
-
try {
|
|
406
|
-
instance.stop();
|
|
407
|
-
} catch (e) {
|
|
408
|
-
logger.warn("Error stopping tunnel instance", e);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
this.tunnelsByTunnelId.clear();
|
|
412
|
-
this.tunnelsByConfigId.clear();
|
|
413
|
-
this.tunnelStats.clear();
|
|
414
|
-
this.tunnelStatsListeners.clear();
|
|
415
|
-
logger.info("All tunnels stopped and cleared");
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* Remove a stopped tunnel's records so it will no longer be returned by list methods.
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
* @param tunnelId - the tunnel id to remove
|
|
424
|
-
* @returns true if the record was removed, false otherwise
|
|
425
|
-
*/
|
|
426
|
-
removeStoppedTunnelByTunnelId(tunnelId: string): boolean {
|
|
427
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
428
|
-
if (!managed) {
|
|
429
|
-
logger.debug("Attempted to remove non-existent tunnel", { tunnelId });
|
|
430
|
-
return false;
|
|
431
|
-
}
|
|
432
|
-
if (!managed.isStopped) {
|
|
433
|
-
logger.warn("Attempted to remove tunnel that is not stopped", { tunnelId });
|
|
434
|
-
return false;
|
|
435
|
-
}
|
|
436
|
-
this._cleanupTunnelRecords(managed);
|
|
437
|
-
logger.info("Removed stopped tunnel records", { tunnelId, configId: managed.configid });
|
|
438
|
-
return true;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Remove a stopped tunnel by its config id.
|
|
443
|
-
* @param configId - the config id to remove
|
|
444
|
-
* @returns true if the record was removed, false otherwise
|
|
445
|
-
*/
|
|
446
|
-
removeStoppedTunnelByConfigId(configId: string): boolean {
|
|
447
|
-
const managed = this.tunnelsByConfigId.get(configId);
|
|
448
|
-
if (!managed) {
|
|
449
|
-
logger.debug("Attempted to remove non-existent tunnel by configId", { configId });
|
|
450
|
-
return false;
|
|
451
|
-
}
|
|
452
|
-
return this.removeStoppedTunnelByTunnelId(managed.tunnelid);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
private _cleanupTunnelRecords(managed: ManagedTunnel): void {
|
|
457
|
-
if (!managed.isStopped) {
|
|
458
|
-
throw new Error(`Active tunnel "${managed.tunnelid}" cannot be removed`);
|
|
459
|
-
}
|
|
460
|
-
try {
|
|
461
|
-
// If serveWorker exists and tunnel is already stopped, do NOT attempt to terminate it;
|
|
462
|
-
// just clear the reference to allow GC.
|
|
463
|
-
if (managed.serveWorker) {
|
|
464
|
-
managed.serveWorker = null;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// remove stored maps and listeners
|
|
468
|
-
this.tunnelStats.delete(managed.tunnelid);
|
|
469
|
-
this.tunnelStatsListeners.delete(managed.tunnelid);
|
|
470
|
-
this.tunnelErrorListeners.delete(managed.tunnelid);
|
|
471
|
-
this.tunnelDisconnectListeners.delete(managed.tunnelid);
|
|
472
|
-
this.tunnelWorkerErrorListeners.delete(managed.tunnelid);
|
|
473
|
-
this.tunnelStartListeners.delete(managed.tunnelid);
|
|
474
|
-
this.tunnelsByTunnelId.delete(managed.tunnelid);
|
|
475
|
-
this.tunnelsByConfigId.delete(managed.configid);
|
|
476
|
-
} catch (e) {
|
|
477
|
-
logger.warn("Failed cleaning up tunnel records", { tunnelId: managed.tunnelid, error: e });
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
/**
|
|
483
|
-
* Get tunnel instance by either configId or tunnelId
|
|
484
|
-
* @param configId - The configuration ID of the tunnel
|
|
485
|
-
* @param tunnelId - The tunnel ID
|
|
486
|
-
* @returns The tunnel instance
|
|
487
|
-
* @throws Error if neither configId nor tunnelId is provided, or if tunnel is not found
|
|
488
|
-
*/
|
|
489
|
-
getTunnelInstance(configId?: string, tunnelId?: string): TunnelInstance {
|
|
490
|
-
if (configId) {
|
|
491
|
-
const managed = this.tunnelsByConfigId.get(configId);
|
|
492
|
-
if (!managed) throw new Error(`Tunnel "${configId}" not found`);
|
|
493
|
-
return managed.instance;
|
|
494
|
-
}
|
|
495
|
-
if (tunnelId) {
|
|
496
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
497
|
-
if (!managed) throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
498
|
-
return managed.instance;
|
|
499
|
-
}
|
|
500
|
-
throw new Error(`Either configId or tunnelId must be provided`);
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
/**
|
|
504
|
-
* Get tunnel config by either configId or tunnelId
|
|
505
|
-
* @param configId - The configuration ID of the tunnel
|
|
506
|
-
* @param tunnelId - The tunnel ID
|
|
507
|
-
* @returns The tunnel config
|
|
508
|
-
* @throws Error if neither configId nor tunnelId is provided, or if tunnel is not found
|
|
509
|
-
*/
|
|
510
|
-
async getTunnelConfig(configId?: string, tunnelId?: string): Promise<PinggyOptions> {
|
|
511
|
-
if (configId) {
|
|
512
|
-
const managed = this.tunnelsByConfigId.get(configId);
|
|
513
|
-
if (!managed) {
|
|
514
|
-
throw new Error(`Tunnel with configId "${configId}" not found`);
|
|
515
|
-
}
|
|
516
|
-
return <PinggyOptions>managed.instance.getConfig();
|
|
517
|
-
}
|
|
518
|
-
if (tunnelId) {
|
|
519
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
520
|
-
if (!managed) {
|
|
521
|
-
throw new Error(`Tunnel with tunnelId "${tunnelId}" not found`);
|
|
522
|
-
}
|
|
523
|
-
return <PinggyOptions>managed.instance.getConfig();
|
|
524
|
-
}
|
|
525
|
-
throw new Error(`Either configId or tunnelId must be provided`);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
/**
|
|
529
|
-
* Restarts a tunnel with its current configuration.
|
|
530
|
-
* This function will stop the tunnel if it's running and start it again.
|
|
531
|
-
* All configurations including additional forwarding rules are preserved.
|
|
532
|
-
*/
|
|
533
|
-
async restartTunnel(tunnelid: string): Promise<void> {
|
|
534
|
-
// Get the existing tunnel
|
|
535
|
-
const existingTunnel = this.tunnelsByTunnelId.get(tunnelid);
|
|
536
|
-
if (!existingTunnel) {
|
|
537
|
-
throw new Error(`Tunnel "${tunnelid}" not found`);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
logger.info("Initiating tunnel restart", {
|
|
541
|
-
tunnelId: tunnelid,
|
|
542
|
-
configId: existingTunnel.configid
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
try {
|
|
546
|
-
// Store the current configuration
|
|
547
|
-
const tunnelName = existingTunnel.tunnelName;
|
|
548
|
-
const currentConfigId = existingTunnel.configid;
|
|
549
|
-
const currentConfig = existingTunnel.tunnelConfig;
|
|
550
|
-
const configWithForwarding = existingTunnel.configWithForwarding;
|
|
551
|
-
const additionalForwarding = existingTunnel.additionalForwarding;
|
|
552
|
-
const currentServe = existingTunnel.serve;
|
|
553
|
-
const autoReconnect = existingTunnel.autoReconnect || false;
|
|
554
|
-
|
|
555
|
-
// Remove the existing tunnel
|
|
556
|
-
this.tunnelsByTunnelId.delete(tunnelid);
|
|
557
|
-
this.tunnelsByConfigId.delete(existingTunnel.configid);
|
|
558
|
-
this.tunnelStats.delete(tunnelid);
|
|
559
|
-
this.tunnelStatsListeners.delete(tunnelid);
|
|
560
|
-
this.tunnelErrorListeners.delete(tunnelid);
|
|
561
|
-
this.tunnelDisconnectListeners.delete(tunnelid);
|
|
562
|
-
this.tunnelWorkerErrorListeners.delete(tunnelid);
|
|
563
|
-
this.tunnelStartListeners.delete(tunnelid);
|
|
564
|
-
|
|
565
|
-
// Create a new tunnel with the same configuration
|
|
566
|
-
const newTunnel = await this._createTunnelWithProcessedConfig({
|
|
567
|
-
configid: currentConfigId,
|
|
568
|
-
tunnelid,
|
|
569
|
-
tunnelName,
|
|
570
|
-
originalConfig: currentConfig!,
|
|
571
|
-
configWithForwarding: configWithForwarding!,
|
|
572
|
-
additionalForwarding,
|
|
573
|
-
serve: currentServe,
|
|
574
|
-
autoReconnect,
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
//preserve the createdAt timestamp
|
|
578
|
-
if (existingTunnel.createdAt) {
|
|
579
|
-
newTunnel.createdAt = existingTunnel.createdAt;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// Start the new tunnel
|
|
583
|
-
await this.startTunnel(newTunnel.tunnelid);
|
|
584
|
-
|
|
585
|
-
} catch (error) {
|
|
586
|
-
logger.error("Failed to restart tunnel", {
|
|
587
|
-
tunnelid,
|
|
588
|
-
error: error instanceof Error ? error.message : 'Unknown error'
|
|
589
|
-
});
|
|
590
|
-
throw new Error(`Failed to restart tunnel: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
* Updates the configuration of an existing tunnel.
|
|
596
|
-
*
|
|
597
|
-
* This method handles the process of updating a tunnel's configuration while preserving
|
|
598
|
-
* its state. If the tunnel is running, it will be stopped, updated, and restarted.
|
|
599
|
-
* In case of failure, it attempts to restore the original configuration.
|
|
600
|
-
*
|
|
601
|
-
* @param newConfig - The new configuration to apply, including configid and optional additional forwarding
|
|
602
|
-
*
|
|
603
|
-
* @returns Promise resolving to the updated ManagedTunnel
|
|
604
|
-
* @throws Error if the tunnel is not found or if the update process fails
|
|
605
|
-
*/
|
|
606
|
-
async updateConfig(
|
|
607
|
-
newConfig: PinggyOptions & { tunnelType: string[] | undefined } & { configid: string; additionalForwarding?: AdditionalForwarding[], tunnelName?: string, serve?: string },
|
|
608
|
-
): Promise<ManagedTunnel> {
|
|
609
|
-
const { configid, tunnelName: newTunnelName, additionalForwarding } = newConfig;
|
|
610
|
-
|
|
611
|
-
if (!configid || configid.trim().length === 0) {
|
|
612
|
-
throw new Error(`Invalid configid: "${configid}"`);
|
|
613
|
-
}
|
|
614
|
-
// Get the existing tunnel
|
|
615
|
-
const existingTunnel = this.tunnelsByConfigId.get(configid);
|
|
616
|
-
if (!existingTunnel) {
|
|
617
|
-
throw new Error(`Tunnel with config id "${configid}" not found`);
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// Store the current state
|
|
621
|
-
const isStopped = existingTunnel.isStopped;
|
|
622
|
-
const currentTunnelConfig = existingTunnel.tunnelConfig!;
|
|
623
|
-
const currentConfigWithForwarding = existingTunnel.configWithForwarding!;
|
|
624
|
-
const currentTunnelId = existingTunnel.tunnelid;
|
|
625
|
-
const currentTunnelConfigId = existingTunnel.configid;
|
|
626
|
-
const currentAdditionalForwarding = existingTunnel.additionalForwarding;
|
|
627
|
-
const currentTunnelName = existingTunnel.tunnelName;
|
|
628
|
-
const currentServe = existingTunnel.serve;
|
|
629
|
-
const currentAutoReconnect = existingTunnel.autoReconnect || false;
|
|
630
|
-
|
|
631
|
-
try {
|
|
632
|
-
// Stop the existing tunnel if running
|
|
633
|
-
if (!isStopped) {
|
|
634
|
-
existingTunnel.instance.stop();
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// Remove the old tunnel
|
|
638
|
-
this.tunnelsByTunnelId.delete(currentTunnelId);
|
|
639
|
-
this.tunnelsByConfigId.delete(currentTunnelConfigId);
|
|
640
|
-
|
|
641
|
-
// Build config for the new configuration
|
|
642
|
-
const mergedBaseConfig = {
|
|
643
|
-
...newConfig,
|
|
644
|
-
configid: configid,
|
|
645
|
-
tunnelName: newTunnelName !== undefined ? newTunnelName : currentTunnelName,
|
|
646
|
-
serve: newConfig.serve !== undefined ? newConfig.serve : currentServe
|
|
647
|
-
};
|
|
648
|
-
|
|
649
|
-
// Build the config with forwarding rules (only for new config)
|
|
650
|
-
const newConfigWithForwarding = this.buildPinggyConfig(
|
|
651
|
-
mergedBaseConfig,
|
|
652
|
-
additionalForwarding !== undefined ? additionalForwarding : currentAdditionalForwarding
|
|
653
|
-
);
|
|
654
|
-
|
|
655
|
-
// Create the new tunnel with the built config
|
|
656
|
-
const newTunnel = await this._createTunnelWithProcessedConfig({
|
|
657
|
-
configid: configid,
|
|
658
|
-
tunnelid: currentTunnelId,
|
|
659
|
-
tunnelName: newTunnelName !== undefined ? newTunnelName : currentTunnelName,
|
|
660
|
-
originalConfig: mergedBaseConfig,
|
|
661
|
-
configWithForwarding: newConfigWithForwarding,
|
|
662
|
-
additionalForwarding: additionalForwarding !== undefined ? additionalForwarding : currentAdditionalForwarding,
|
|
663
|
-
serve: newConfig.serve !== undefined ? newConfig.serve : currentServe,
|
|
664
|
-
autoReconnect: currentAutoReconnect,
|
|
665
|
-
});
|
|
666
|
-
|
|
667
|
-
// Start the tunnel if it was running before
|
|
668
|
-
if (!isStopped) {
|
|
669
|
-
await this.startTunnel(newTunnel.tunnelid);
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
logger.info("Tunnel configuration updated", {
|
|
673
|
-
tunnelId: newTunnel.tunnelid,
|
|
674
|
-
configId: newTunnel.configid,
|
|
675
|
-
isStopped: isStopped
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
return newTunnel;
|
|
679
|
-
|
|
680
|
-
} catch (error: any) {
|
|
681
|
-
logger.error("Error updating tunnel configuration", {
|
|
682
|
-
configId: configid,
|
|
683
|
-
error: error instanceof Error ? error.message : String(error)
|
|
684
|
-
});
|
|
685
|
-
// If anything fails during the update, try to restore the previous state
|
|
686
|
-
try {
|
|
687
|
-
const originalTunnel = await this._createTunnelWithProcessedConfig({
|
|
688
|
-
configid: currentTunnelConfigId,
|
|
689
|
-
tunnelid: currentTunnelId,
|
|
690
|
-
tunnelName: currentTunnelName,
|
|
691
|
-
originalConfig: currentTunnelConfig,
|
|
692
|
-
configWithForwarding: currentConfigWithForwarding,
|
|
693
|
-
additionalForwarding: currentAdditionalForwarding,
|
|
694
|
-
serve: currentServe,
|
|
695
|
-
autoReconnect: currentAutoReconnect,
|
|
696
|
-
});
|
|
697
|
-
if (!isStopped) {
|
|
698
|
-
await this.startTunnel(originalTunnel.tunnelid);
|
|
699
|
-
}
|
|
700
|
-
logger.warn("Restored original tunnel configuration after update failure", {
|
|
701
|
-
currentTunnelId,
|
|
702
|
-
error: error instanceof Error ? error.message : 'Unknown error'
|
|
703
|
-
});
|
|
704
|
-
} catch (restoreError: any) {
|
|
705
|
-
logger.error("Failed to restore original tunnel configuration", {
|
|
706
|
-
currentTunnelId,
|
|
707
|
-
error: restoreError instanceof Error ? restoreError.message : 'Unknown error'
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
// Re-throw the original error
|
|
711
|
-
throw error;
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
/**
|
|
716
|
-
* Retrieve the ManagedTunnel object by either configId or tunnelId.
|
|
717
|
-
* Throws an error if neither id is provided or the tunnel is not found.
|
|
718
|
-
*/
|
|
719
|
-
getManagedTunnel(configId?: string, tunnelId?: string): ManagedTunnel {
|
|
720
|
-
if (configId) {
|
|
721
|
-
const managed = this.tunnelsByConfigId.get(configId);
|
|
722
|
-
if (!managed) throw new Error(`Tunnel "${configId}" not found`);
|
|
723
|
-
return managed;
|
|
724
|
-
}
|
|
725
|
-
if (tunnelId) {
|
|
726
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
727
|
-
if (!managed) throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
728
|
-
return managed;
|
|
729
|
-
}
|
|
730
|
-
throw new Error(`Either configId or tunnelId must be provided`);
|
|
731
|
-
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
async getTunnelGreetMessage(tunnelId: string): Promise<string | null> {
|
|
735
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
736
|
-
if (!managed) {
|
|
737
|
-
logger.error(`Tunnel "${tunnelId}" not found when fetching greet message`);
|
|
738
|
-
return null;
|
|
739
|
-
}
|
|
740
|
-
try {
|
|
741
|
-
if (managed.isStopped) {
|
|
742
|
-
|
|
743
|
-
return null;
|
|
744
|
-
}
|
|
745
|
-
const messages = await managed.instance.getGreetMessage();
|
|
746
|
-
if (Array.isArray(messages)) {
|
|
747
|
-
return messages.join(" ");
|
|
748
|
-
}
|
|
749
|
-
return messages ?? null;
|
|
750
|
-
} catch (e) {
|
|
751
|
-
logger.error(
|
|
752
|
-
`Error fetching greet message for tunnel "${tunnelId}": ${e instanceof Error ? e.message : String(e)
|
|
753
|
-
}`
|
|
754
|
-
);
|
|
755
|
-
return null;
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
getTunnelStats(tunnelId: string): TunnelUsageType[] | null {
|
|
762
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
763
|
-
if (!managed) {
|
|
764
|
-
return null;
|
|
765
|
-
}
|
|
766
|
-
// Return the latest stats or null if none available yet
|
|
767
|
-
const stats = this.tunnelStats.get(tunnelId);
|
|
768
|
-
return stats || null;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
getLatestTunnelStats(tunnelId: string): TunnelUsageType | null {
|
|
772
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
773
|
-
if (!managed) {
|
|
774
|
-
return null;
|
|
775
|
-
}
|
|
776
|
-
const stats = this.tunnelStats.get(tunnelId);
|
|
777
|
-
if (stats && stats.length > 0) {
|
|
778
|
-
return stats[stats.length - 1];
|
|
779
|
-
}
|
|
780
|
-
return null;
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
/**
|
|
784
|
-
* Registers a listener function to receive tunnel statistics updates.
|
|
785
|
-
* The listener will be called whenever any tunnel's stats are updated.
|
|
786
|
-
*
|
|
787
|
-
* @param tunnelId - The tunnel ID to listen to stats for
|
|
788
|
-
* @param listener - Function that receives tunnelId and stats when updates occur
|
|
789
|
-
* @returns A unique listener ID that can be used to deregister the listener and tunnelId
|
|
790
|
-
*
|
|
791
|
-
* @throws {Error} When the specified tunnelId does not exist
|
|
792
|
-
*/
|
|
793
|
-
async registerStatsListener(tunnelId: string, listener: StatsListener): Promise<[string, string]> {
|
|
794
|
-
// Verify tunnel exists
|
|
795
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
796
|
-
if (!managed) {
|
|
797
|
-
throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// Initialize listeners map for this tunnel if it doesn't exist
|
|
801
|
-
if (!this.tunnelStatsListeners.has(tunnelId)) {
|
|
802
|
-
this.tunnelStatsListeners.set(tunnelId, new Map());
|
|
803
|
-
}
|
|
804
|
-
const listenerId = getRandomId();
|
|
805
|
-
const tunnelListeners = this.tunnelStatsListeners.get(tunnelId)!;
|
|
806
|
-
tunnelListeners.set(listenerId, listener);
|
|
807
|
-
|
|
808
|
-
logger.info("Stats listener registered for tunnel", { tunnelId, listenerId });
|
|
809
|
-
return [listenerId, tunnelId];
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
async registerErrorListener(tunnelId: string, listener: ErrorListener): Promise<string> {
|
|
813
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
814
|
-
if (!managed) {
|
|
815
|
-
throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
if (!this.tunnelErrorListeners.has(tunnelId)) {
|
|
819
|
-
this.tunnelErrorListeners.set(tunnelId, new Map());
|
|
820
|
-
}
|
|
821
|
-
const listenerId = getRandomId();
|
|
822
|
-
const tunnelErrorListeners = this.tunnelErrorListeners.get(tunnelId)!;
|
|
823
|
-
tunnelErrorListeners.set(listenerId, listener);
|
|
824
|
-
|
|
825
|
-
logger.info("Error listener registered for tunnel", { tunnelId, listenerId });
|
|
826
|
-
return listenerId;
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
async registerDisconnectListener(tunnelId: string, listener: DisconnectListener): Promise<string> {
|
|
830
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
831
|
-
if (!managed) {
|
|
832
|
-
throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
if (!this.tunnelDisconnectListeners.has(tunnelId)) {
|
|
836
|
-
this.tunnelDisconnectListeners.set(tunnelId, new Map());
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
const listenerId = getRandomId();
|
|
840
|
-
const tunnelDisconnectListeners = this.tunnelDisconnectListeners.get(tunnelId)!;
|
|
841
|
-
tunnelDisconnectListeners.set(listenerId, listener);
|
|
842
|
-
|
|
843
|
-
logger.info("Disconnect listener registered for tunnel", { tunnelId, listenerId });
|
|
844
|
-
return listenerId;
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
async registerWorkerErrorListner(tunnelId: string, listener: TunnelWorkerErrorListner): Promise<void> {
|
|
848
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
849
|
-
if (!managed) {
|
|
850
|
-
throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
if (!this.tunnelWorkerErrorListeners.has(tunnelId)) {
|
|
854
|
-
this.tunnelWorkerErrorListeners.set(tunnelId, new Map());
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
const listenerId = getRandomId();
|
|
858
|
-
const tunnelWorkerErrorListner = this.tunnelWorkerErrorListeners.get(tunnelId);
|
|
859
|
-
tunnelWorkerErrorListner?.set(listenerId, listener);
|
|
860
|
-
logger.info("TunnelWorker error listener registered for tunnel", { tunnelId, listenerId });
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
async registerStartListener(tunnelId: string, listener: StartListener): Promise<string> {
|
|
864
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
865
|
-
if (!managed) {
|
|
866
|
-
throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
if (!this.tunnelStartListeners.has(tunnelId)) {
|
|
870
|
-
this.tunnelStartListeners.set(tunnelId, new Map());
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
const listenerId = getRandomId();
|
|
874
|
-
const listeners = this.tunnelStartListeners.get(tunnelId)!;
|
|
875
|
-
listeners.set(listenerId, listener);
|
|
876
|
-
|
|
877
|
-
logger.info("Start listener registered for tunnel", { tunnelId, listenerId });
|
|
878
|
-
return listenerId;
|
|
879
|
-
}
|
|
880
|
-
/**
|
|
881
|
-
* Removes a previously registered stats listener.
|
|
882
|
-
*
|
|
883
|
-
* @param tunnelId - The tunnel ID the listener was registered for
|
|
884
|
-
* @param listenerId - The unique ID returned when the listener was registered
|
|
885
|
-
*/
|
|
886
|
-
deregisterStatsListener(tunnelId: string, listenerId: string): void {
|
|
887
|
-
const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
|
|
888
|
-
if (!tunnelListeners) {
|
|
889
|
-
logger.warn("No listeners found for tunnel", { tunnelId });
|
|
890
|
-
return;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
const removed = tunnelListeners.delete(listenerId);
|
|
894
|
-
if (removed) {
|
|
895
|
-
logger.info("Stats listener deregistered", { tunnelId, listenerId });
|
|
896
|
-
|
|
897
|
-
// Clean up empty listener map
|
|
898
|
-
if (tunnelListeners.size === 0) {
|
|
899
|
-
this.tunnelStatsListeners.delete(tunnelId);
|
|
900
|
-
}
|
|
901
|
-
} else {
|
|
902
|
-
logger.warn("Attempted to deregister non-existent stats listener", { tunnelId, listenerId });
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
deregisterErrorListener(tunnelId: string, listenerId: string): void {
|
|
907
|
-
const listeners = this.tunnelErrorListeners.get(tunnelId);
|
|
908
|
-
if (!listeners) {
|
|
909
|
-
logger.warn("No error listeners found for tunnel", { tunnelId });
|
|
910
|
-
return;
|
|
911
|
-
}
|
|
912
|
-
const removed = listeners.delete(listenerId);
|
|
913
|
-
if (removed) {
|
|
914
|
-
logger.info("Error listener deregistered", { tunnelId, listenerId });
|
|
915
|
-
if (listeners.size === 0) {
|
|
916
|
-
this.tunnelErrorListeners.delete(tunnelId);
|
|
917
|
-
}
|
|
918
|
-
} else {
|
|
919
|
-
logger.warn("Attempted to deregister non-existent error listener", { tunnelId, listenerId });
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
deregisterDisconnectListener(tunnelId: string, listenerId: string): void {
|
|
924
|
-
const listeners = this.tunnelDisconnectListeners.get(tunnelId);
|
|
925
|
-
if (!listeners) {
|
|
926
|
-
logger.warn("No disconnect listeners found for tunnel", { tunnelId });
|
|
927
|
-
return;
|
|
928
|
-
}
|
|
929
|
-
const removed = listeners.delete(listenerId);
|
|
930
|
-
if (removed) {
|
|
931
|
-
logger.info("Disconnect listener deregistered", { tunnelId, listenerId });
|
|
932
|
-
if (listeners.size === 0) {
|
|
933
|
-
this.tunnelDisconnectListeners.delete(tunnelId);
|
|
934
|
-
}
|
|
935
|
-
} else {
|
|
936
|
-
logger.warn("Attempted to deregister non-existent disconnect listener", { tunnelId, listenerId });
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
async getLocalserverTlsInfo(tunnelId: string): Promise<string | false> {
|
|
941
|
-
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
942
|
-
if (!managed) {
|
|
943
|
-
logger.error(`Tunnel "${tunnelId}" not found when fetching local server TLS info`);
|
|
944
|
-
return false;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
try {
|
|
948
|
-
if (managed.isStopped) {
|
|
949
|
-
|
|
950
|
-
return false;
|
|
951
|
-
}
|
|
952
|
-
const tlsInfo = await managed.instance.getLocalServerTls();
|
|
953
|
-
if (tlsInfo) {
|
|
954
|
-
return tlsInfo;
|
|
955
|
-
}
|
|
956
|
-
return false;
|
|
957
|
-
} catch (e) {
|
|
958
|
-
logger.error(`Error fetching TLS info for tunnel "${tunnelId}": ${e instanceof Error ? e.message : e}`);
|
|
959
|
-
return false;
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
/**
|
|
965
|
-
* Sets up the stats callback for a tunnel during creation.
|
|
966
|
-
* This callback will update stored stats and notify all registered listeners.
|
|
967
|
-
*/
|
|
968
|
-
private setupStatsCallback(tunnelId: string, managed: ManagedTunnel): void {
|
|
969
|
-
try {
|
|
970
|
-
const callback = (usage: Record<string, any>) => {
|
|
971
|
-
this.updateStats(tunnelId, usage);
|
|
972
|
-
};
|
|
973
|
-
|
|
974
|
-
// Set the callback on the tunnel instance
|
|
975
|
-
managed.instance.setUsageUpdateCallback(callback);
|
|
976
|
-
logger.debug("Stats callback set up for tunnel", { tunnelId });
|
|
977
|
-
|
|
978
|
-
} catch (error) {
|
|
979
|
-
logger.warn("Failed to set up stats callback", { tunnelId, error });
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
private notifyErrorListeners(tunnelId: string, errorMsg: string, isFatal: boolean): void {
|
|
984
|
-
try {
|
|
985
|
-
const listeners = this.tunnelErrorListeners.get(tunnelId);
|
|
986
|
-
if (!listeners) return;
|
|
987
|
-
for (const [id, listener] of listeners) {
|
|
988
|
-
try {
|
|
989
|
-
listener(tunnelId, errorMsg, isFatal);
|
|
990
|
-
} catch (err) {
|
|
991
|
-
logger.debug("Error in error-listener callback", { listenerId: id, tunnelId, err });
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
} catch (err) {
|
|
995
|
-
logger.debug("Failed to notify error listeners", { tunnelId, err });
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
private setupErrorCallback(tunnelId: string, managed: ManagedTunnel): void {
|
|
1000
|
-
try {
|
|
1001
|
-
const callback = ({ errorNo, error, recoverable }: {
|
|
1002
|
-
errorNo: number;
|
|
1003
|
-
error: string;
|
|
1004
|
-
recoverable: boolean;
|
|
1005
|
-
}) => {
|
|
1006
|
-
try {
|
|
1007
|
-
const msg = typeof error === "string" ? error : String(error);
|
|
1008
|
-
const isFatal = true;
|
|
1009
|
-
logger.debug("Tunnel reported error", { tunnelId, errorNo, errorMsg: msg, recoverable });
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
this.notifyErrorListeners(tunnelId, msg, isFatal);
|
|
1013
|
-
|
|
1014
|
-
// TODO: IF the error is fatal, we can stop the tunnel and exit.
|
|
1015
|
-
} catch (e) {
|
|
1016
|
-
logger.warn("Error handling tunnel error callback", { tunnelId, e });
|
|
1017
|
-
}
|
|
1018
|
-
};
|
|
1019
|
-
|
|
1020
|
-
managed.instance.setTunnelErrorCallback(callback);
|
|
1021
|
-
logger.debug("Error callback set up for tunnel", { tunnelId });
|
|
1022
|
-
} catch (error) {
|
|
1023
|
-
logger.warn("Failed to set up error callback", { tunnelId, error });
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
private setupDisconnectCallback(tunnelId: string, managed: ManagedTunnel): void {
|
|
1028
|
-
try {
|
|
1029
|
-
const callback = ({ error, messages }: {
|
|
1030
|
-
error: string;
|
|
1031
|
-
messages: string[];
|
|
1032
|
-
}) => {
|
|
1033
|
-
try {
|
|
1034
|
-
logger.debug("Tunnel disconnected", { tunnelId, error, messages });
|
|
1035
|
-
// get managed tunnel
|
|
1036
|
-
const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
|
|
1037
|
-
if (managedTunnel) {
|
|
1038
|
-
managedTunnel.isStopped = true;
|
|
1039
|
-
managedTunnel.stoppedAt = new Date().toISOString();
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
// initiate autoReconnect if enabled
|
|
1043
|
-
if (managedTunnel && managedTunnel.autoReconnect) {
|
|
1044
|
-
logger.info("Auto-reconnecting tunnel", { tunnelId });
|
|
1045
|
-
setTimeout(async () => {
|
|
1046
|
-
try {
|
|
1047
|
-
await this.restartTunnel(tunnelId);
|
|
1048
|
-
logger.info("Tunnel auto-reconnected successfully", { tunnelId });
|
|
1049
|
-
} catch (e) {
|
|
1050
|
-
logger.error("Failed to auto-reconnect tunnel", { tunnelId, e });
|
|
1051
|
-
}
|
|
1052
|
-
}, 10000); // wait 10 seconds before reconnecting
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
const listeners = this.tunnelDisconnectListeners.get(tunnelId);
|
|
1056
|
-
if (!listeners) return;
|
|
1057
|
-
for (const [id, listener] of listeners) {
|
|
1058
|
-
try {
|
|
1059
|
-
listener(tunnelId, error, messages);
|
|
1060
|
-
} catch (err) {
|
|
1061
|
-
logger.debug("Error in disconnect-listener callback", { listenerId: id, tunnelId, err });
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
} catch (e) {
|
|
1065
|
-
logger.warn("Error handling tunnel disconnect callback", { tunnelId, e });
|
|
1066
|
-
}
|
|
1067
|
-
};
|
|
1068
|
-
|
|
1069
|
-
managed.instance.setTunnelDisconnectedCallback(callback);
|
|
1070
|
-
logger.debug("Disconnect callback set up for tunnel", { tunnelId });
|
|
1071
|
-
} catch (error) {
|
|
1072
|
-
logger.warn("Failed to set up disconnect callback", { tunnelId, error });
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
private setUpTunnelWorkerErrorCallback(tunnelId: string, managed: ManagedTunnel): void {
|
|
1077
|
-
try {
|
|
1078
|
-
const callback = (error: Error) => {
|
|
1079
|
-
try {
|
|
1080
|
-
logger.debug("Error in Tunnel Worker", { tunnelId, errorMessage: error.message });
|
|
1081
|
-
|
|
1082
|
-
const listeners = this.tunnelWorkerErrorListeners.get(tunnelId);
|
|
1083
|
-
if (!listeners) return;
|
|
1084
|
-
for (const [id, listener] of listeners) {
|
|
1085
|
-
try {
|
|
1086
|
-
listener(tunnelId, error);
|
|
1087
|
-
} catch (err) {
|
|
1088
|
-
logger.debug("Error in worker-error-listener callback", { listenerId: id, tunnelId, err });
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
} catch (e) {
|
|
1092
|
-
logger.warn("Error handling tunnel worker error callback", { tunnelId, e });
|
|
1093
|
-
}
|
|
1094
|
-
};
|
|
1095
|
-
|
|
1096
|
-
managed.instance.setWorkerErrorCallback(callback);
|
|
1097
|
-
logger.debug("Disconnect callback set up for tunnel", { tunnelId });
|
|
1098
|
-
|
|
1099
|
-
} catch (error) {
|
|
1100
|
-
logger.warn("Failed to setup tunnel worker error callback")
|
|
1101
|
-
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
/**
|
|
1107
|
-
* Updates the stored stats for a tunnel and notifies all registered listeners.
|
|
1108
|
-
*/
|
|
1109
|
-
private updateStats(tunnelId: string, rawUsage: Record<string, any>): void {
|
|
1110
|
-
try {
|
|
1111
|
-
// Normalize the stats
|
|
1112
|
-
const normalizedStats = this.normalizeStats(rawUsage);
|
|
1113
|
-
|
|
1114
|
-
// get existing stats
|
|
1115
|
-
const existingStats = this.tunnelStats.get(tunnelId) || [];
|
|
1116
|
-
// Append the new stats to existing stats
|
|
1117
|
-
const updatedStats = [...existingStats, normalizedStats];
|
|
1118
|
-
// Store the latest stats
|
|
1119
|
-
this.tunnelStats.set(tunnelId, updatedStats);
|
|
1120
|
-
|
|
1121
|
-
// Notify all registered listeners for this specific tunnel
|
|
1122
|
-
const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
|
|
1123
|
-
if (tunnelListeners) {
|
|
1124
|
-
for (const [listenerId, listener] of tunnelListeners) {
|
|
1125
|
-
try {
|
|
1126
|
-
listener(tunnelId, normalizedStats);
|
|
1127
|
-
} catch (error) {
|
|
1128
|
-
logger.warn("Error in stats listener callback", { listenerId, tunnelId, error });
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
logger.debug("Stats updated and listeners notified", {
|
|
1134
|
-
tunnelId,
|
|
1135
|
-
listenersCount: tunnelListeners?.size || 0
|
|
1136
|
-
});
|
|
1137
|
-
} catch (error) {
|
|
1138
|
-
logger.warn("Error updating stats", { tunnelId, error });
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
/**
|
|
1143
|
-
* Normalizes raw usage data from the SDK into a consistent TunnelStats format.
|
|
1144
|
-
*/
|
|
1145
|
-
private normalizeStats(rawStats: Record<string, any>): TunnelUsageType {
|
|
1146
|
-
const elapsed = this.parseNumber(rawStats.elapsedTime ?? 0);
|
|
1147
|
-
const liveConns = this.parseNumber(rawStats.numLiveConnections ?? 0);
|
|
1148
|
-
const totalConns = this.parseNumber(rawStats.numTotalConnections ?? 0);
|
|
1149
|
-
const reqBytes = this.parseNumber(rawStats.numTotalReqBytes ?? 0);
|
|
1150
|
-
const resBytes = this.parseNumber(rawStats.numTotalResBytes ?? 0);
|
|
1151
|
-
const txBytes = this.parseNumber(rawStats.numTotalTxBytes ?? 0);
|
|
1152
|
-
|
|
1153
|
-
return {
|
|
1154
|
-
elapsedTime: elapsed,
|
|
1155
|
-
numLiveConnections: liveConns,
|
|
1156
|
-
numTotalConnections: totalConns,
|
|
1157
|
-
numTotalReqBytes: reqBytes,
|
|
1158
|
-
numTotalResBytes: resBytes,
|
|
1159
|
-
numTotalTxBytes: txBytes,
|
|
1160
|
-
};
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
private parseNumber(value: any): number {
|
|
1164
|
-
const parsed = typeof value === 'number' ? value : parseInt(String(value), 10);
|
|
1165
|
-
return isNaN(parsed) ? 0 : parsed;
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
private startStaticFileServer(managed: ManagedTunnel): void {
|
|
1169
|
-
try {
|
|
1170
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
1171
|
-
const __dirname = path.dirname(__filename);
|
|
1172
|
-
|
|
1173
|
-
const fileServerWorkerPath = path.join(__dirname, "workers", "file_serve_worker.cjs");
|
|
1174
|
-
|
|
1175
|
-
const staticServerWorker = new Worker(fileServerWorkerPath, {
|
|
1176
|
-
workerData: {
|
|
1177
|
-
dir: managed.serve,
|
|
1178
|
-
port: managed.tunnelConfig?.forwarding,
|
|
1179
|
-
},
|
|
1180
|
-
});
|
|
1181
|
-
|
|
1182
|
-
staticServerWorker.on("message", (msg) => {
|
|
1183
|
-
switch (msg.type) {
|
|
1184
|
-
case "started":
|
|
1185
|
-
logger.info("Static file server started", { dir: managed.serve });
|
|
1186
|
-
break;
|
|
1187
|
-
|
|
1188
|
-
case "warning":
|
|
1189
|
-
if (msg.code === "INVALID_TUNNEL_SERVE_PATH") {
|
|
1190
|
-
managed.warnings = managed.warnings ?? [];
|
|
1191
|
-
managed.warnings.push({ code: msg.code, message: msg.message });
|
|
1192
|
-
}
|
|
1193
|
-
CLIPrinter.warn(msg.message);
|
|
1194
|
-
break;
|
|
1195
|
-
|
|
1196
|
-
case "error":
|
|
1197
|
-
managed.warnings = managed.warnings ?? [];
|
|
1198
|
-
managed.warnings.push({
|
|
1199
|
-
code: "UNKNOWN_WARNING" as TunnelWarningCode,
|
|
1200
|
-
message: msg.message,
|
|
1201
|
-
});
|
|
1202
|
-
break;
|
|
1203
|
-
}
|
|
1204
|
-
});
|
|
1205
|
-
|
|
1206
|
-
managed.serveWorker = staticServerWorker;
|
|
1207
|
-
} catch (error) {
|
|
1208
|
-
logger.error("Error starting static file server", error);
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
}
|