pinggy 0.4.8 → 0.5.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/CLAUDE.md +112 -0
- package/README.md +214 -97
- package/dist/TunnelManager-OPUMAZFX.js +11 -0
- package/dist/TunnelTui-QZEWWH2H.js +1338 -0
- package/dist/{chunk-3RTRUYNW.js → chunk-7G6SJEEA.js} +35 -7
- package/dist/chunk-BFARGPGP.js +164 -0
- package/dist/chunk-DLNUDW6G.js +1690 -0
- package/dist/chunk-FVLXFHBL.js +2157 -0
- package/dist/chunk-GBYF2H4H.js +77 -0
- package/dist/chunk-HUP6YWH6.js +269 -0
- package/dist/chunk-MT44NAXX.js +36 -0
- package/dist/chunk-UB26QJ4T.js +10 -0
- package/dist/chunk-YJQC6LQN.js +3407 -0
- package/dist/configStore-TSGRNOE3.js +42 -0
- package/dist/daemonChild-E2CORSSB.js +24 -0
- package/dist/daemonConfig-G6S46GPJ.js +9 -0
- package/dist/index.cjs +5153 -1596
- package/dist/index.d.cts +473 -13
- package/dist/index.d.ts +473 -13
- package/dist/index.js +12 -5
- package/dist/ipcClient-LZQCCNMR.js +6 -0
- package/dist/main-F4U5R4SW.js +42 -0
- package/dist/workers/file_serve_worker.cjs +70 -21
- package/dist/workers/file_serve_worker.js +15 -9
- package/eslint.config.js +27 -0
- package/package.json +8 -4
- package/dist/chunk-YFTL44B3.js +0 -2857
- package/dist/main-4WTJG54V.js +0 -2925
|
@@ -0,0 +1,1690 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FileServerMessage
|
|
3
|
+
} from "./chunk-UB26QJ4T.js";
|
|
4
|
+
import {
|
|
5
|
+
getLogLevel,
|
|
6
|
+
logger
|
|
7
|
+
} from "./chunk-7G6SJEEA.js";
|
|
8
|
+
import {
|
|
9
|
+
getTunnelLogPath
|
|
10
|
+
} from "./chunk-GBYF2H4H.js";
|
|
11
|
+
|
|
12
|
+
// src/tunnel_manager/TunnelManager.ts
|
|
13
|
+
import { TunnelInstance, LogLevel as SdkLogLevel } from "@pinggy/pinggy";
|
|
14
|
+
|
|
15
|
+
// src/logger/tunnelLogger.ts
|
|
16
|
+
import winston from "winston";
|
|
17
|
+
import fs from "fs";
|
|
18
|
+
import path from "path";
|
|
19
|
+
var tunnelTransports = /* @__PURE__ */ new Map();
|
|
20
|
+
var tunnelLoggingEnabled = true;
|
|
21
|
+
function setTunnelLoggingEnabled(enabled) {
|
|
22
|
+
if (tunnelLoggingEnabled === enabled) return;
|
|
23
|
+
tunnelLoggingEnabled = enabled;
|
|
24
|
+
if (!enabled) {
|
|
25
|
+
detachAllTunnelLoggers();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function isTunnelLoggingEnabled() {
|
|
29
|
+
return tunnelLoggingEnabled;
|
|
30
|
+
}
|
|
31
|
+
function attachTunnelLogger(tunnelId, origin, name) {
|
|
32
|
+
if (!tunnelLoggingEnabled) {
|
|
33
|
+
return logger.child({ tunnelId, source: "daemon" });
|
|
34
|
+
}
|
|
35
|
+
const logPath = getTunnelLogPath(tunnelId, origin, name);
|
|
36
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
37
|
+
const tunnelFilter = winston.format((info) => {
|
|
38
|
+
return info.tunnelId === tunnelId ? info : false;
|
|
39
|
+
});
|
|
40
|
+
const transport = new winston.transports.File({
|
|
41
|
+
filename: logPath,
|
|
42
|
+
format: winston.format.combine(
|
|
43
|
+
tunnelFilter(),
|
|
44
|
+
winston.format.timestamp(),
|
|
45
|
+
winston.format.printf(({ level, message, timestamp, source, ...meta }) => {
|
|
46
|
+
const srcLabel = source ? `[${source}] ` : "";
|
|
47
|
+
const { tunnelId: _tid, ...rest } = meta;
|
|
48
|
+
return `${timestamp} [${level}] ${srcLabel}${message}${Object.keys(rest).length ? " " + JSON.stringify(rest) : ""}`;
|
|
49
|
+
})
|
|
50
|
+
)
|
|
51
|
+
});
|
|
52
|
+
logger.add(transport);
|
|
53
|
+
tunnelTransports.set(tunnelId, transport);
|
|
54
|
+
return logger.child({ tunnelId, source: "daemon" });
|
|
55
|
+
}
|
|
56
|
+
function detachTunnelLogger(tunnelId) {
|
|
57
|
+
const transport = tunnelTransports.get(tunnelId);
|
|
58
|
+
if (!transport) return;
|
|
59
|
+
logger.remove(transport);
|
|
60
|
+
tunnelTransports.delete(tunnelId);
|
|
61
|
+
transport.close?.();
|
|
62
|
+
}
|
|
63
|
+
function detachAllTunnelLoggers() {
|
|
64
|
+
for (const tunnelId of tunnelTransports.keys()) {
|
|
65
|
+
detachTunnelLogger(tunnelId);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/logger/rotateLog.ts
|
|
70
|
+
import fs2 from "fs";
|
|
71
|
+
var DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
|
72
|
+
var DEFAULT_MAX_ARCHIVES = 3;
|
|
73
|
+
var ROTATE_TRIGGER_RATIO = 0.8;
|
|
74
|
+
function maybeRotate(file, maxBytes = DEFAULT_MAX_BYTES, maxArchives = DEFAULT_MAX_ARCHIVES) {
|
|
75
|
+
let size;
|
|
76
|
+
try {
|
|
77
|
+
size = fs2.statSync(file).size;
|
|
78
|
+
} catch {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (size < maxBytes * ROTATE_TRIGGER_RATIO) return;
|
|
82
|
+
const archive = (n) => `${file}.${n}`;
|
|
83
|
+
try {
|
|
84
|
+
if (fs2.existsSync(archive(maxArchives))) {
|
|
85
|
+
fs2.unlinkSync(archive(maxArchives));
|
|
86
|
+
}
|
|
87
|
+
for (let n = maxArchives - 1; n >= 1; n--) {
|
|
88
|
+
if (fs2.existsSync(archive(n))) {
|
|
89
|
+
fs2.renameSync(archive(n), archive(n + 1));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
fs2.renameSync(file, archive(1));
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/tunnel_manager/TunnelManager.ts
|
|
98
|
+
import path2 from "path";
|
|
99
|
+
import { Worker } from "worker_threads";
|
|
100
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
101
|
+
|
|
102
|
+
// src/utils/printer.ts
|
|
103
|
+
import pico2 from "picocolors";
|
|
104
|
+
|
|
105
|
+
// src/tui/spinner/spinner.ts
|
|
106
|
+
import pico from "picocolors";
|
|
107
|
+
var spinners = {
|
|
108
|
+
dots: {
|
|
109
|
+
interval: 80,
|
|
110
|
+
frames: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"]
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
var currentTimer = null;
|
|
114
|
+
var currentText = "";
|
|
115
|
+
function startSpinner(name = "dots", text = "Loading") {
|
|
116
|
+
const spinner = spinners[name];
|
|
117
|
+
let i = 0;
|
|
118
|
+
currentText = text;
|
|
119
|
+
if (currentTimer) {
|
|
120
|
+
clearInterval(currentTimer);
|
|
121
|
+
}
|
|
122
|
+
currentTimer = setInterval(() => {
|
|
123
|
+
const frame = spinner.frames[i = ++i % spinner.frames.length];
|
|
124
|
+
process.stdout.write(`\r${pico.cyan(frame)} ${text}`);
|
|
125
|
+
}, spinner.interval);
|
|
126
|
+
return () => stopSpinner();
|
|
127
|
+
}
|
|
128
|
+
function stopSpinner() {
|
|
129
|
+
if (currentTimer) {
|
|
130
|
+
clearInterval(currentTimer);
|
|
131
|
+
currentTimer = null;
|
|
132
|
+
process.stdout.write("\r\x1B[K");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function stopSpinnerSuccess(message) {
|
|
136
|
+
if (currentTimer) {
|
|
137
|
+
clearInterval(currentTimer);
|
|
138
|
+
currentTimer = null;
|
|
139
|
+
const finalMessage = message || currentText;
|
|
140
|
+
process.stdout.write(`\r${pico.green("\u2714")} ${finalMessage}
|
|
141
|
+
`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function stopSpinnerFail(message) {
|
|
145
|
+
if (currentTimer) {
|
|
146
|
+
clearInterval(currentTimer);
|
|
147
|
+
currentTimer = null;
|
|
148
|
+
const finalMessage = message || currentText;
|
|
149
|
+
process.stdout.write(`\r${pico.red("\u2716")} ${finalMessage}
|
|
150
|
+
`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/utils/printer.ts
|
|
155
|
+
var _CLIPrinter = class _CLIPrinter {
|
|
156
|
+
static isCLIError(err) {
|
|
157
|
+
return err instanceof Error;
|
|
158
|
+
}
|
|
159
|
+
static print(message, ...args) {
|
|
160
|
+
console.log(message, ...args);
|
|
161
|
+
}
|
|
162
|
+
static error(err) {
|
|
163
|
+
const def = this.errorDefinitions.find((d) => d.match(err));
|
|
164
|
+
const msg = def.message(err);
|
|
165
|
+
console.error(pico2.red(pico2.bold("\u2716 Error:")), pico2.red(msg));
|
|
166
|
+
}
|
|
167
|
+
static fatal(err) {
|
|
168
|
+
const def = this.errorDefinitions.find((d) => d.match(err));
|
|
169
|
+
const msg = def.message(err);
|
|
170
|
+
console.error(pico2.red(pico2.bold("\u2716 Fatal Error:")), pico2.red(msg));
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
static red(message) {
|
|
174
|
+
return pico2.red(message);
|
|
175
|
+
}
|
|
176
|
+
static warn(message) {
|
|
177
|
+
console.warn(pico2.yellow(pico2.bold("\u26A0 Warning:")), pico2.yellow(message));
|
|
178
|
+
}
|
|
179
|
+
static warnTxt(message) {
|
|
180
|
+
console.warn(pico2.yellow(pico2.bold("\u26A0 Warning:")), pico2.yellow(message));
|
|
181
|
+
}
|
|
182
|
+
static success(message) {
|
|
183
|
+
console.log(pico2.green(pico2.bold(" \u2714 Success:")), pico2.green(message));
|
|
184
|
+
}
|
|
185
|
+
static info(message) {
|
|
186
|
+
console.log(pico2.blue(message));
|
|
187
|
+
}
|
|
188
|
+
static startSpinner(message) {
|
|
189
|
+
startSpinner("dots", message);
|
|
190
|
+
}
|
|
191
|
+
static stopSpinnerSuccess(message) {
|
|
192
|
+
stopSpinnerSuccess(message);
|
|
193
|
+
}
|
|
194
|
+
static stopSpinnerFail(message) {
|
|
195
|
+
stopSpinnerFail(message);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
_CLIPrinter.errorDefinitions = [
|
|
199
|
+
{
|
|
200
|
+
match: (err) => _CLIPrinter.isCLIError(err) && err.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION",
|
|
201
|
+
message: (err) => {
|
|
202
|
+
const match = /Unknown option '(.+?)'/.exec(err.message);
|
|
203
|
+
const option = match ? match[1] : "(unknown)";
|
|
204
|
+
return `Unknown option '${option}'. Please check your command or use pinggy -h for guidance.`;
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
match: (err) => _CLIPrinter.isCLIError(err) && err.code === "ERR_PARSE_ARGS_MISSING_OPTION_VALUE",
|
|
209
|
+
message: (err) => `Missing required argument for option '${err.option}'.`
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
match: (err) => _CLIPrinter.isCLIError(err) && err.code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE",
|
|
213
|
+
message: (err) => `Invalid argument'${err.message}'.`
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
match: (err) => _CLIPrinter.isCLIError(err) && err.code === "ENOENT",
|
|
217
|
+
message: (err) => `File or directory not found: ${err.message}`
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
match: () => true,
|
|
221
|
+
// fallback
|
|
222
|
+
message: (err) => _CLIPrinter.isCLIError(err) ? err.message : String(err)
|
|
223
|
+
}
|
|
224
|
+
];
|
|
225
|
+
var CLIPrinter = _CLIPrinter;
|
|
226
|
+
var printer_default = CLIPrinter;
|
|
227
|
+
|
|
228
|
+
// src/utils/util.ts
|
|
229
|
+
import { readFileSync } from "fs";
|
|
230
|
+
import { randomUUID } from "crypto";
|
|
231
|
+
import { fileURLToPath } from "url";
|
|
232
|
+
import { dirname, join } from "path";
|
|
233
|
+
function getRandomId() {
|
|
234
|
+
return randomUUID();
|
|
235
|
+
}
|
|
236
|
+
function isValidPort(p) {
|
|
237
|
+
return Number.isInteger(p) && p > 0 && p < 65536;
|
|
238
|
+
}
|
|
239
|
+
function errorMessage(err) {
|
|
240
|
+
return err instanceof Error ? err.message : String(err);
|
|
241
|
+
}
|
|
242
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
243
|
+
var __dirname2 = dirname(__filename2);
|
|
244
|
+
function getVersion() {
|
|
245
|
+
try {
|
|
246
|
+
const packageJsonPath = join(__dirname2, "../package.json");
|
|
247
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
248
|
+
return pkg.version ?? "";
|
|
249
|
+
} catch (error) {
|
|
250
|
+
printer_default.error("Error reading version info");
|
|
251
|
+
return "";
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function getLocalAddress(config) {
|
|
255
|
+
if (!config) return "-";
|
|
256
|
+
if (config.forwarding) {
|
|
257
|
+
if (typeof config.forwarding === "string") return config.forwarding;
|
|
258
|
+
if (Array.isArray(config.forwarding) && config.forwarding.length > 0) {
|
|
259
|
+
const f = config.forwarding[0];
|
|
260
|
+
if (f.address) return f.address;
|
|
261
|
+
if (f.localDomain && f.localPort) return `${f.localDomain}:${f.localPort}`;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (config.localAddress) return config.localAddress;
|
|
265
|
+
return "-";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/tunnel_manager/TunnelManager.ts
|
|
269
|
+
var __filename3 = fileURLToPath2(import.meta.url);
|
|
270
|
+
var __dirname3 = path2.dirname(__filename3);
|
|
271
|
+
function mapToSdkLogLevel(level) {
|
|
272
|
+
if (level === "debug") return SdkLogLevel.DEBUG;
|
|
273
|
+
if (level === "error") return SdkLogLevel.ERROR;
|
|
274
|
+
return SdkLogLevel.INFO;
|
|
275
|
+
}
|
|
276
|
+
var STATS_HISTORY_LIMIT = 100;
|
|
277
|
+
var TunnelAlreadyRunningError = class extends Error {
|
|
278
|
+
constructor(configId, existingTunnelId) {
|
|
279
|
+
super(`Tunnel with configId "${configId}" is already running (tunnelid: ${existingTunnelId})`);
|
|
280
|
+
this.configId = configId;
|
|
281
|
+
this.existingTunnelId = existingTunnelId;
|
|
282
|
+
this.name = "TunnelAlreadyRunningError";
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
var TunnelManager = class _TunnelManager {
|
|
286
|
+
constructor() {
|
|
287
|
+
this.tunnelsByTunnelId = /* @__PURE__ */ new Map();
|
|
288
|
+
this.tunnelsByConfigId = /* @__PURE__ */ new Map();
|
|
289
|
+
this.tunnelStats = /* @__PURE__ */ new Map();
|
|
290
|
+
this.tunnelStatsListeners = /* @__PURE__ */ new Map();
|
|
291
|
+
this.tunnelErrorListeners = /* @__PURE__ */ new Map();
|
|
292
|
+
this.tunnelPollingErrorListeners = /* @__PURE__ */ new Map();
|
|
293
|
+
this.tunnelDisconnectListeners = /* @__PURE__ */ new Map();
|
|
294
|
+
this.tunnelStoppedListeners = /* @__PURE__ */ new Map();
|
|
295
|
+
this.tunnelWorkerErrorListeners = /* @__PURE__ */ new Map();
|
|
296
|
+
this.tunnelStartListeners = /* @__PURE__ */ new Map();
|
|
297
|
+
this.tunnelWillReconnectListeners = /* @__PURE__ */ new Map();
|
|
298
|
+
this.tunnelReconnectingListeners = /* @__PURE__ */ new Map();
|
|
299
|
+
this.tunnelReconnectionCompletedListeners = /* @__PURE__ */ new Map();
|
|
300
|
+
this.tunnelReconnectionFailedListeners = /* @__PURE__ */ new Map();
|
|
301
|
+
}
|
|
302
|
+
static getInstance() {
|
|
303
|
+
if (!_TunnelManager.instance) {
|
|
304
|
+
_TunnelManager.instance = new _TunnelManager();
|
|
305
|
+
}
|
|
306
|
+
return _TunnelManager.instance;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Creates a new managed tunnel instance with the given configuration.
|
|
310
|
+
* Optionally builds the config with forwarding rules based on buildConfig flag.
|
|
311
|
+
*
|
|
312
|
+
* @param config - The tunnel configuration options
|
|
313
|
+
*
|
|
314
|
+
* @throws {Error} When configId is invalid or empty
|
|
315
|
+
* @throws {Error} When a tunnel with the given configId already exists
|
|
316
|
+
*
|
|
317
|
+
* @returns {ManagedTunnel} A new managed tunnel instance containing the tunnel details,
|
|
318
|
+
* status information, and statistics
|
|
319
|
+
*/
|
|
320
|
+
async createTunnel(config, origin = "cli") {
|
|
321
|
+
const { configId, tunnelid: requestedTunnelId, tunnelName, name } = config;
|
|
322
|
+
const tunnelid = requestedTunnelId || getRandomId();
|
|
323
|
+
const autoReconnect = config.autoReconnect || false;
|
|
324
|
+
const serve = this.resolveServePath(config);
|
|
325
|
+
if (!configId || typeof configId !== "string" || configId.trim() === "") {
|
|
326
|
+
throw new Error("configId is required and must be a non-empty string");
|
|
327
|
+
}
|
|
328
|
+
const existing = this.tunnelsByConfigId.get(configId);
|
|
329
|
+
if (existing && !existing.isStopped) {
|
|
330
|
+
throw new TunnelAlreadyRunningError(configId, existing.tunnelid);
|
|
331
|
+
}
|
|
332
|
+
return this._createTunnelWithProcessedConfig({
|
|
333
|
+
configId,
|
|
334
|
+
tunnelid,
|
|
335
|
+
tunnelName: tunnelName || name,
|
|
336
|
+
origin,
|
|
337
|
+
originalConfig: config,
|
|
338
|
+
serve,
|
|
339
|
+
autoReconnect
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Internal method to create a tunnel with an already-processed configuration.
|
|
344
|
+
* This is used by createTunnel, restartTunnel, and updateConfig to avoid config processing.
|
|
345
|
+
*
|
|
346
|
+
* @param params - Configuration parameters with already-processed forwarding rules
|
|
347
|
+
* @returns The created ManagedTunnel instance
|
|
348
|
+
* @private
|
|
349
|
+
*/
|
|
350
|
+
async _createTunnelWithProcessedConfig(params) {
|
|
351
|
+
const tunnelLogName = params.tunnelName || params.originalConfig?.name;
|
|
352
|
+
const tunnelLogPath = getTunnelLogPath(params.tunnelid, params.origin, tunnelLogName);
|
|
353
|
+
maybeRotate(tunnelLogPath);
|
|
354
|
+
attachTunnelLogger(params.tunnelid, params.origin, tunnelLogName);
|
|
355
|
+
let instance;
|
|
356
|
+
try {
|
|
357
|
+
logger.debug("Creating tunnel instance with processed config", params.originalConfig);
|
|
358
|
+
instance = await TunnelInstance.create(params.originalConfig, {
|
|
359
|
+
enabled: true,
|
|
360
|
+
logLevel: mapToSdkLogLevel(getLogLevel()),
|
|
361
|
+
logFilePath: tunnelLogPath,
|
|
362
|
+
libpinggyLogPath: tunnelLogPath
|
|
363
|
+
});
|
|
364
|
+
} catch (e) {
|
|
365
|
+
logger.error("Error creating tunnel instance:", e);
|
|
366
|
+
detachTunnelLogger(params.tunnelid);
|
|
367
|
+
throw e;
|
|
368
|
+
}
|
|
369
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
370
|
+
const managed = {
|
|
371
|
+
tunnelid: params.tunnelid,
|
|
372
|
+
configId: params.configId,
|
|
373
|
+
tunnelName: params.tunnelName,
|
|
374
|
+
origin: params.origin,
|
|
375
|
+
instance,
|
|
376
|
+
tunnelConfig: params.originalConfig,
|
|
377
|
+
serve: params.serve,
|
|
378
|
+
warnings: [],
|
|
379
|
+
isStopped: false,
|
|
380
|
+
createdAt: now,
|
|
381
|
+
startedAt: null,
|
|
382
|
+
stoppedAt: null,
|
|
383
|
+
autoReconnect: params.autoReconnect,
|
|
384
|
+
lastError: {}
|
|
385
|
+
};
|
|
386
|
+
instance.setTunnelEstablishedCallback(({}) => {
|
|
387
|
+
managed.startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
388
|
+
});
|
|
389
|
+
this.setupStatsCallback(params.tunnelid, managed);
|
|
390
|
+
this.setupErrorCallback(params.tunnelid, managed);
|
|
391
|
+
this.setupTunnelPollingErrorCallback(params.tunnelid, managed);
|
|
392
|
+
this.setupDisconnectCallback(params.tunnelid, managed);
|
|
393
|
+
this.setupWillReconnectCallback(params.tunnelid, managed);
|
|
394
|
+
this.setupReconnectingCallback(params.tunnelid, managed);
|
|
395
|
+
this.setupReconnectionCompletedCallback(params.tunnelid, managed);
|
|
396
|
+
this.setupReconnectionFailedCallback(params.tunnelid, managed);
|
|
397
|
+
this.setUpTunnelWorkerErrorCallback(params.tunnelid, managed);
|
|
398
|
+
this.tunnelsByTunnelId.set(params.tunnelid, managed);
|
|
399
|
+
this.tunnelsByConfigId.set(params.configId, managed);
|
|
400
|
+
logger.info("Tunnel created", { configId: params.configId, tunnelId: params.tunnelid });
|
|
401
|
+
return managed;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Start a tunnel that was created but not yet started
|
|
405
|
+
*/
|
|
406
|
+
async startTunnel(tunnelId) {
|
|
407
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
408
|
+
if (!managed) {
|
|
409
|
+
throw new Error(`Tunnel with id "${tunnelId}" not found`);
|
|
410
|
+
}
|
|
411
|
+
logger.info("Starting tunnel", { tunnelId });
|
|
412
|
+
let urls;
|
|
413
|
+
try {
|
|
414
|
+
urls = await managed.instance.start();
|
|
415
|
+
} catch (error) {
|
|
416
|
+
logger.warn("Failed to start tunnel", { tunnelId, error });
|
|
417
|
+
managed.isStopped = true;
|
|
418
|
+
managed.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
419
|
+
managed.lastError = {
|
|
420
|
+
message: "Failed to start tunnel",
|
|
421
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
422
|
+
isFatal: true
|
|
423
|
+
};
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
426
|
+
logger.info("Tunnel started", { tunnelId, urls });
|
|
427
|
+
logger.info("Checking serve config for tunnel", { tunnelId, serve: managed.serve });
|
|
428
|
+
if (managed.serve) {
|
|
429
|
+
this.startStaticFileServer(managed);
|
|
430
|
+
} else {
|
|
431
|
+
logger.debug("No serve path configured, skipping static file server", { tunnelId });
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
const startListeners = this.tunnelStartListeners.get(tunnelId);
|
|
435
|
+
if (startListeners) {
|
|
436
|
+
for (const [id, listener] of startListeners) {
|
|
437
|
+
try {
|
|
438
|
+
listener(tunnelId, urls);
|
|
439
|
+
} catch (err) {
|
|
440
|
+
logger.debug("Error in start-listener callback", { listenerId: id, tunnelId, err });
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
} catch (e) {
|
|
445
|
+
logger.warn("Failed to notify start listeners", { tunnelId, e });
|
|
446
|
+
}
|
|
447
|
+
return urls;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Stops a running tunnel and updates its status.
|
|
451
|
+
*
|
|
452
|
+
* @param tunnelId - The unique identifier of the tunnel to stop
|
|
453
|
+
* @throws {Error} If the tunnel with the given tunnelId is not found
|
|
454
|
+
* @remarks
|
|
455
|
+
* - Clears the tunnel's remote URLs
|
|
456
|
+
* - Updates the tunnel's state to Exited if stopped successfully
|
|
457
|
+
* - Logs the stop operation with tunnelId and configId
|
|
458
|
+
*/
|
|
459
|
+
stopTunnel(tunnelId) {
|
|
460
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
461
|
+
if (!managed) {
|
|
462
|
+
throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
463
|
+
}
|
|
464
|
+
logger.info("Stopping tunnel", { tunnelId, configId: managed.configId });
|
|
465
|
+
managed.isStopping = true;
|
|
466
|
+
try {
|
|
467
|
+
void managed.instance.stop();
|
|
468
|
+
if (managed.serveWorker) {
|
|
469
|
+
logger.info("terminating serveWorker");
|
|
470
|
+
void managed.serveWorker.terminate();
|
|
471
|
+
}
|
|
472
|
+
this.tunnelStats.delete(tunnelId);
|
|
473
|
+
this.tunnelStatsListeners.delete(tunnelId);
|
|
474
|
+
this.tunnelStats.delete(tunnelId);
|
|
475
|
+
this.tunnelStatsListeners.delete(tunnelId);
|
|
476
|
+
this.tunnelErrorListeners.delete(tunnelId);
|
|
477
|
+
this.tunnelPollingErrorListeners.delete(tunnelId);
|
|
478
|
+
this.tunnelDisconnectListeners.delete(tunnelId);
|
|
479
|
+
this.tunnelWorkerErrorListeners.delete(tunnelId);
|
|
480
|
+
this.tunnelStartListeners.delete(tunnelId);
|
|
481
|
+
this.tunnelWillReconnectListeners.delete(tunnelId);
|
|
482
|
+
this.tunnelReconnectingListeners.delete(tunnelId);
|
|
483
|
+
this.tunnelReconnectionCompletedListeners.delete(tunnelId);
|
|
484
|
+
this.tunnelReconnectionFailedListeners.delete(tunnelId);
|
|
485
|
+
managed.serveWorker = null;
|
|
486
|
+
managed.warnings = managed.warnings ?? [];
|
|
487
|
+
managed.isStopped = true;
|
|
488
|
+
managed.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
489
|
+
detachTunnelLogger(tunnelId);
|
|
490
|
+
this.notifyStoppedListeners(tunnelId);
|
|
491
|
+
logger.info("Tunnel stopped", { tunnelId, configId: managed.configId });
|
|
492
|
+
return { configId: managed.configId, tunnelid: managed.tunnelid };
|
|
493
|
+
} catch (error) {
|
|
494
|
+
managed.isStopping = false;
|
|
495
|
+
logger.error("Failed to stop tunnel", { tunnelId, error });
|
|
496
|
+
throw error;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
notifyStoppedListeners(tunnelId) {
|
|
500
|
+
const listeners = this.tunnelStoppedListeners.get(tunnelId);
|
|
501
|
+
if (!listeners) return;
|
|
502
|
+
for (const [id, listener] of listeners) {
|
|
503
|
+
try {
|
|
504
|
+
listener(tunnelId);
|
|
505
|
+
} catch (err) {
|
|
506
|
+
logger.debug("Error in stopped-listener callback", { listenerId: id, tunnelId, err });
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
this.tunnelStoppedListeners.delete(tunnelId);
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Get all public URLs for a tunnel
|
|
513
|
+
*/
|
|
514
|
+
async getTunnelUrls(tunnelId) {
|
|
515
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
516
|
+
if (!managed) {
|
|
517
|
+
logger.error(`Tunnel "${tunnelId}" not found when fetching URLs`);
|
|
518
|
+
return [];
|
|
519
|
+
}
|
|
520
|
+
if (managed.isStopped) {
|
|
521
|
+
logger.debug(`Skipping URL fetch for stopped tunnel`, { tunnelId });
|
|
522
|
+
return [];
|
|
523
|
+
}
|
|
524
|
+
const urls = await managed.instance.urls();
|
|
525
|
+
return urls;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Get all TunnelStatus currently managed by this TunnelManager
|
|
529
|
+
* @returns An array of all TunnelStatus objects
|
|
530
|
+
*/
|
|
531
|
+
async getAllTunnels() {
|
|
532
|
+
try {
|
|
533
|
+
const tunnelList = await Promise.all(Array.from(this.tunnelsByTunnelId.values()).map(async (tunnel) => {
|
|
534
|
+
return {
|
|
535
|
+
tunnelid: tunnel.tunnelid,
|
|
536
|
+
configId: tunnel.configId,
|
|
537
|
+
tunnelName: tunnel.tunnelName,
|
|
538
|
+
tunnelConfig: tunnel.tunnelConfig,
|
|
539
|
+
remoteurls: tunnel.isStopped || tunnel.lastError?.isFatal ? [] : await this.getTunnelUrls(tunnel.tunnelid),
|
|
540
|
+
serve: tunnel.serve
|
|
541
|
+
};
|
|
542
|
+
}));
|
|
543
|
+
return tunnelList;
|
|
544
|
+
} catch (err) {
|
|
545
|
+
logger.error("Error fetching tunnels", { error: err });
|
|
546
|
+
return [];
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Re-apply the given log level to every live tunnel's worker JS logger.
|
|
551
|
+
* Native libpinggy level is fixed at worker init and cannot be re-pathed
|
|
552
|
+
* or re-levelled at runtime without restarting the tunnel.
|
|
553
|
+
*/
|
|
554
|
+
applyLogLevelToActiveTunnels(level) {
|
|
555
|
+
const sdkLevel = mapToSdkLogLevel(level);
|
|
556
|
+
for (const tunnel of this.tunnelsByTunnelId.values()) {
|
|
557
|
+
if (tunnel.isStopped) continue;
|
|
558
|
+
if (tunnel.lastError?.isFatal) continue;
|
|
559
|
+
const logPath = getTunnelLogPath(tunnel.tunnelid, tunnel.origin, tunnel.tunnelName || tunnel.tunnelConfig?.name);
|
|
560
|
+
tunnel.instance.setDebugLogging(true, sdkLevel, logPath).catch((err) => {
|
|
561
|
+
logger.error("Failed to apply log level to tunnel", { tunnelId: tunnel.tunnelid, error: err?.message ?? err });
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Tunnel IDs whose ManagedTunnel is currently alive (not stopped, no fatal error).
|
|
567
|
+
* Cheap, synchronous, suitable for hot paths like /logs/paths.
|
|
568
|
+
*/
|
|
569
|
+
getActiveTunnelIds() {
|
|
570
|
+
const ids = /* @__PURE__ */ new Set();
|
|
571
|
+
for (const tunnel of this.tunnelsByTunnelId.values()) {
|
|
572
|
+
if (tunnel.isStopped) continue;
|
|
573
|
+
if (tunnel.lastError?.isFatal) continue;
|
|
574
|
+
ids.add(tunnel.tunnelid);
|
|
575
|
+
}
|
|
576
|
+
return ids;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Get status of a tunnel
|
|
580
|
+
*/
|
|
581
|
+
async getTunnelStatus(tunnelId) {
|
|
582
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
583
|
+
if (!managed) {
|
|
584
|
+
logger.error(`Tunnel "${tunnelId}" not found when fetching status`);
|
|
585
|
+
throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
586
|
+
}
|
|
587
|
+
if (managed.isStopped) {
|
|
588
|
+
return "exited";
|
|
589
|
+
}
|
|
590
|
+
const status = await managed.instance.getStatus();
|
|
591
|
+
return status;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Stop all tunnels
|
|
595
|
+
*/
|
|
596
|
+
stopAllTunnels() {
|
|
597
|
+
for (const managed of this.tunnelsByTunnelId.values()) {
|
|
598
|
+
if (managed.isStopped) continue;
|
|
599
|
+
try {
|
|
600
|
+
void managed.instance.stop();
|
|
601
|
+
} catch (e) {
|
|
602
|
+
logger.warn("Error stopping tunnel instance", e);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
this.tunnelsByTunnelId.clear();
|
|
606
|
+
this.tunnelsByConfigId.clear();
|
|
607
|
+
this.tunnelStats.clear();
|
|
608
|
+
this.tunnelStatsListeners.clear();
|
|
609
|
+
this.tunnelErrorListeners.clear();
|
|
610
|
+
this.tunnelPollingErrorListeners.clear();
|
|
611
|
+
this.tunnelDisconnectListeners.clear();
|
|
612
|
+
this.tunnelWorkerErrorListeners.clear();
|
|
613
|
+
this.tunnelStartListeners.clear();
|
|
614
|
+
this.tunnelWillReconnectListeners.clear();
|
|
615
|
+
this.tunnelReconnectingListeners.clear();
|
|
616
|
+
this.tunnelReconnectionCompletedListeners.clear();
|
|
617
|
+
this.tunnelReconnectionFailedListeners.clear();
|
|
618
|
+
logger.info("All tunnels stopped and cleared");
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Remove a stopped tunnel's records so it will no longer be returned by list methods.
|
|
622
|
+
*
|
|
623
|
+
*
|
|
624
|
+
* @param tunnelId - the tunnel id to remove
|
|
625
|
+
* @returns true if the record was removed, false otherwise
|
|
626
|
+
*/
|
|
627
|
+
removeStoppedTunnelByTunnelId(tunnelId) {
|
|
628
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
629
|
+
if (!managed) {
|
|
630
|
+
logger.debug("Attempted to remove non-existent tunnel", { tunnelId });
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
if (!managed.isStopped) {
|
|
634
|
+
logger.warn("Attempted to remove tunnel that is not stopped", { tunnelId });
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
this._cleanupTunnelRecords(managed);
|
|
638
|
+
logger.info("Removed stopped tunnel records", { tunnelId, configId: managed.configId });
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Remove a stopped tunnel by its config id.
|
|
643
|
+
* @param configId - the config id to remove
|
|
644
|
+
* @returns true if the record was removed, false otherwise
|
|
645
|
+
*/
|
|
646
|
+
removeStoppedTunnelByConfigId(configId) {
|
|
647
|
+
const managed = this.tunnelsByConfigId.get(configId);
|
|
648
|
+
if (!managed) {
|
|
649
|
+
logger.debug("Attempted to remove non-existent tunnel by configId", { configId });
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
return this.removeStoppedTunnelByTunnelId(managed.tunnelid);
|
|
653
|
+
}
|
|
654
|
+
_cleanupTunnelRecords(managed) {
|
|
655
|
+
if (!managed.isStopped) {
|
|
656
|
+
throw new Error(`Active tunnel "${managed.tunnelid}" cannot be removed`);
|
|
657
|
+
}
|
|
658
|
+
try {
|
|
659
|
+
if (managed.serveWorker) {
|
|
660
|
+
managed.serveWorker = null;
|
|
661
|
+
}
|
|
662
|
+
this.tunnelStats.delete(managed.tunnelid);
|
|
663
|
+
this.tunnelStatsListeners.delete(managed.tunnelid);
|
|
664
|
+
this.tunnelErrorListeners.delete(managed.tunnelid);
|
|
665
|
+
this.tunnelPollingErrorListeners.delete(managed.tunnelid);
|
|
666
|
+
this.tunnelDisconnectListeners.delete(managed.tunnelid);
|
|
667
|
+
this.tunnelWorkerErrorListeners.delete(managed.tunnelid);
|
|
668
|
+
this.tunnelStartListeners.delete(managed.tunnelid);
|
|
669
|
+
this.tunnelWillReconnectListeners.delete(managed.tunnelid);
|
|
670
|
+
this.tunnelReconnectingListeners.delete(managed.tunnelid);
|
|
671
|
+
this.tunnelReconnectionCompletedListeners.delete(managed.tunnelid);
|
|
672
|
+
this.tunnelReconnectionFailedListeners.delete(managed.tunnelid);
|
|
673
|
+
this.tunnelsByTunnelId.delete(managed.tunnelid);
|
|
674
|
+
this.tunnelsByConfigId.delete(managed.configId);
|
|
675
|
+
} catch (e) {
|
|
676
|
+
logger.warn("Failed cleaning up tunnel records", { tunnelId: managed.tunnelid, error: e });
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Get tunnel instance by either configId or tunnelId
|
|
681
|
+
* @param configId - The configuration ID of the tunnel
|
|
682
|
+
* @param tunnelId - The tunnel ID
|
|
683
|
+
* @returns The tunnel instance
|
|
684
|
+
* @throws Error if neither configId nor tunnelId is provided, or if tunnel is not found
|
|
685
|
+
*/
|
|
686
|
+
getTunnelInstance(configId, tunnelId) {
|
|
687
|
+
if (configId) {
|
|
688
|
+
const managed = this.tunnelsByConfigId.get(configId);
|
|
689
|
+
if (!managed) {
|
|
690
|
+
throw new Error(`Tunnel "${configId}" not found`);
|
|
691
|
+
}
|
|
692
|
+
return managed.instance;
|
|
693
|
+
}
|
|
694
|
+
if (tunnelId) {
|
|
695
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
696
|
+
if (!managed) {
|
|
697
|
+
throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
698
|
+
}
|
|
699
|
+
return managed.instance;
|
|
700
|
+
}
|
|
701
|
+
throw new Error(`Either configId or tunnelId must be provided`);
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Get tunnel config by either configId or tunnelId
|
|
705
|
+
* @param configId - The configuration ID of the tunnel
|
|
706
|
+
* @param tunnelId - The tunnel ID
|
|
707
|
+
* @returns The tunnel config
|
|
708
|
+
* @throws Error if neither configId nor tunnelId is provided, or if tunnel is not found
|
|
709
|
+
*/
|
|
710
|
+
getTunnelConfig(configId, tunnelId) {
|
|
711
|
+
if (configId) {
|
|
712
|
+
const managed = this.tunnelsByConfigId.get(configId);
|
|
713
|
+
if (!managed) {
|
|
714
|
+
return Promise.reject(new Error(`Tunnel with configId "${configId}" not found`));
|
|
715
|
+
}
|
|
716
|
+
return Promise.resolve(managed.instance.getConfig());
|
|
717
|
+
}
|
|
718
|
+
if (tunnelId) {
|
|
719
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
720
|
+
if (!managed) {
|
|
721
|
+
return Promise.reject(new Error(`Tunnel with tunnelId "${tunnelId}" not found`));
|
|
722
|
+
}
|
|
723
|
+
return Promise.resolve(managed.instance.getConfig());
|
|
724
|
+
}
|
|
725
|
+
return Promise.reject(new Error(`Either configId or tunnelId must be provided`));
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Restarts a tunnel with its current configuration.
|
|
729
|
+
* This function will stop the tunnel if it's running and start it again.
|
|
730
|
+
* All configurations including additional forwarding rules are preserved.
|
|
731
|
+
*/
|
|
732
|
+
async restartTunnel(tunnelid) {
|
|
733
|
+
const existingTunnel = this.tunnelsByTunnelId.get(tunnelid);
|
|
734
|
+
if (!existingTunnel) {
|
|
735
|
+
throw new Error(`Tunnel "${tunnelid}" not found`);
|
|
736
|
+
}
|
|
737
|
+
logger.info("Initiating tunnel restart", {
|
|
738
|
+
tunnelId: tunnelid,
|
|
739
|
+
configId: existingTunnel.configId
|
|
740
|
+
});
|
|
741
|
+
try {
|
|
742
|
+
const tunnelName = existingTunnel.tunnelName;
|
|
743
|
+
const currentConfigId = existingTunnel.configId;
|
|
744
|
+
const currentConfig = existingTunnel.tunnelConfig;
|
|
745
|
+
const currentServe = existingTunnel.serve;
|
|
746
|
+
const autoReconnect = existingTunnel.autoReconnect || false;
|
|
747
|
+
const currentOrigin = existingTunnel.origin;
|
|
748
|
+
this.tunnelsByTunnelId.delete(tunnelid);
|
|
749
|
+
this.tunnelsByConfigId.delete(existingTunnel.configId);
|
|
750
|
+
this.tunnelStats.delete(tunnelid);
|
|
751
|
+
this.tunnelStatsListeners.delete(tunnelid);
|
|
752
|
+
this.tunnelErrorListeners.delete(tunnelid);
|
|
753
|
+
this.tunnelPollingErrorListeners.delete(tunnelid);
|
|
754
|
+
this.tunnelDisconnectListeners.delete(tunnelid);
|
|
755
|
+
this.tunnelWorkerErrorListeners.delete(tunnelid);
|
|
756
|
+
this.tunnelStartListeners.delete(tunnelid);
|
|
757
|
+
this.tunnelWillReconnectListeners.delete(tunnelid);
|
|
758
|
+
this.tunnelReconnectingListeners.delete(tunnelid);
|
|
759
|
+
this.tunnelReconnectionCompletedListeners.delete(tunnelid);
|
|
760
|
+
this.tunnelReconnectionFailedListeners.delete(tunnelid);
|
|
761
|
+
const newTunnel = await this._createTunnelWithProcessedConfig({
|
|
762
|
+
configId: currentConfigId,
|
|
763
|
+
tunnelid,
|
|
764
|
+
tunnelName,
|
|
765
|
+
origin: currentOrigin,
|
|
766
|
+
originalConfig: currentConfig,
|
|
767
|
+
serve: currentServe,
|
|
768
|
+
autoReconnect
|
|
769
|
+
});
|
|
770
|
+
if (existingTunnel.createdAt) {
|
|
771
|
+
newTunnel.createdAt = existingTunnel.createdAt;
|
|
772
|
+
}
|
|
773
|
+
await this.startTunnel(newTunnel.tunnelid);
|
|
774
|
+
} catch (error) {
|
|
775
|
+
logger.error("Failed to restart tunnel", {
|
|
776
|
+
tunnelid,
|
|
777
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
778
|
+
});
|
|
779
|
+
throw new Error(`Failed to restart tunnel: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Updates the configuration of an existing tunnel.
|
|
784
|
+
*
|
|
785
|
+
* This method handles the process of updating a tunnel's configuration while preserving
|
|
786
|
+
* its state. If the tunnel is running, it will be stopped, updated, and restarted.
|
|
787
|
+
* In case of failure, it attempts to restore the original configuration.
|
|
788
|
+
*
|
|
789
|
+
* @param newConfig - The new configuration to apply, including configid and optional additional forwarding
|
|
790
|
+
*
|
|
791
|
+
* @returns Promise resolving to the updated ManagedTunnel
|
|
792
|
+
* @throws Error if the tunnel is not found or if the update process fails
|
|
793
|
+
*/
|
|
794
|
+
async updateConfig(newConfig) {
|
|
795
|
+
const { configId, tunnelName: newTunnelName } = newConfig;
|
|
796
|
+
if (!configId || configId.trim().length === 0) {
|
|
797
|
+
throw new Error(`Invalid configId: "${configId}"`);
|
|
798
|
+
}
|
|
799
|
+
const existingTunnel = this.tunnelsByConfigId.get(configId);
|
|
800
|
+
if (!existingTunnel) {
|
|
801
|
+
throw new Error(`Tunnel with config id "${configId}" not found`);
|
|
802
|
+
}
|
|
803
|
+
const isStopped = existingTunnel.isStopped;
|
|
804
|
+
const currentTunnelConfig = existingTunnel.tunnelConfig;
|
|
805
|
+
const currentTunnelId = existingTunnel.tunnelid;
|
|
806
|
+
const currentTunnelConfigId = existingTunnel.configId;
|
|
807
|
+
const currentTunnelName = existingTunnel.tunnelName;
|
|
808
|
+
const currentServe = existingTunnel.serve;
|
|
809
|
+
const currentAutoReconnect = existingTunnel.autoReconnect || false;
|
|
810
|
+
const currentOrigin = existingTunnel.origin;
|
|
811
|
+
const requestedServe = this.resolveServePath(newConfig);
|
|
812
|
+
try {
|
|
813
|
+
if (!isStopped) {
|
|
814
|
+
void existingTunnel.instance.stop();
|
|
815
|
+
}
|
|
816
|
+
this.tunnelsByTunnelId.delete(currentTunnelId);
|
|
817
|
+
this.tunnelsByConfigId.delete(currentTunnelConfigId);
|
|
818
|
+
const mergedBaseConfig = {
|
|
819
|
+
...newConfig,
|
|
820
|
+
configId,
|
|
821
|
+
tunnelName: newTunnelName !== void 0 ? newTunnelName : currentTunnelName,
|
|
822
|
+
serve: requestedServe !== void 0 ? requestedServe : currentServe
|
|
823
|
+
};
|
|
824
|
+
const effectiveServe = requestedServe !== void 0 ? requestedServe : currentServe;
|
|
825
|
+
const effectiveTunnelName = newTunnelName !== void 0 ? newTunnelName : currentTunnelName;
|
|
826
|
+
let configWithForwarding;
|
|
827
|
+
const newTunnel = await this._createTunnelWithProcessedConfig({
|
|
828
|
+
configId,
|
|
829
|
+
tunnelid: currentTunnelId,
|
|
830
|
+
tunnelName: effectiveTunnelName,
|
|
831
|
+
origin: currentOrigin,
|
|
832
|
+
originalConfig: mergedBaseConfig,
|
|
833
|
+
serve: effectiveServe,
|
|
834
|
+
autoReconnect: currentAutoReconnect
|
|
835
|
+
});
|
|
836
|
+
if (!isStopped) {
|
|
837
|
+
await this.startTunnel(newTunnel.tunnelid);
|
|
838
|
+
}
|
|
839
|
+
logger.info("Tunnel configuration updated", {
|
|
840
|
+
tunnelId: newTunnel.tunnelid,
|
|
841
|
+
configId: newTunnel.configId,
|
|
842
|
+
isStopped
|
|
843
|
+
});
|
|
844
|
+
return newTunnel;
|
|
845
|
+
} catch (error) {
|
|
846
|
+
logger.error("Error updating tunnel configuration", {
|
|
847
|
+
configId,
|
|
848
|
+
error: errorMessage(error)
|
|
849
|
+
});
|
|
850
|
+
try {
|
|
851
|
+
const originalTunnel = await this._createTunnelWithProcessedConfig({
|
|
852
|
+
configId: currentTunnelConfigId,
|
|
853
|
+
tunnelid: currentTunnelId,
|
|
854
|
+
tunnelName: currentTunnelName,
|
|
855
|
+
origin: currentOrigin,
|
|
856
|
+
originalConfig: currentTunnelConfig,
|
|
857
|
+
serve: currentServe,
|
|
858
|
+
autoReconnect: currentAutoReconnect
|
|
859
|
+
});
|
|
860
|
+
if (!isStopped) {
|
|
861
|
+
await this.startTunnel(originalTunnel.tunnelid);
|
|
862
|
+
}
|
|
863
|
+
logger.warn("Restored original tunnel configuration after update failure", {
|
|
864
|
+
currentTunnelId,
|
|
865
|
+
error: errorMessage(error)
|
|
866
|
+
});
|
|
867
|
+
} catch (restoreError) {
|
|
868
|
+
logger.error("Failed to restore original tunnel configuration", {
|
|
869
|
+
currentTunnelId,
|
|
870
|
+
error: errorMessage(restoreError)
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
throw error;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Retrieve the ManagedTunnel object by either configId or tunnelId.
|
|
878
|
+
* Throws an error if neither id is provided or the tunnel is not found.
|
|
879
|
+
*/
|
|
880
|
+
getManagedTunnel(configId, tunnelId) {
|
|
881
|
+
if (configId) {
|
|
882
|
+
const managed = this.tunnelsByConfigId.get(configId);
|
|
883
|
+
if (!managed) {
|
|
884
|
+
throw new Error(`Tunnel "${configId}" not found`);
|
|
885
|
+
}
|
|
886
|
+
return managed;
|
|
887
|
+
}
|
|
888
|
+
if (tunnelId) {
|
|
889
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
890
|
+
if (!managed) {
|
|
891
|
+
throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
892
|
+
}
|
|
893
|
+
return managed;
|
|
894
|
+
}
|
|
895
|
+
throw new Error(`Either configId or tunnelId must be provided`);
|
|
896
|
+
}
|
|
897
|
+
async getTunnelGreetMessage(tunnelId) {
|
|
898
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
899
|
+
if (!managed) {
|
|
900
|
+
logger.error(`Tunnel "${tunnelId}" not found when fetching greet message`);
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
|
+
try {
|
|
904
|
+
if (managed.isStopped) {
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
const messages = await managed.instance.getGreetMessage();
|
|
908
|
+
if (Array.isArray(messages)) {
|
|
909
|
+
return messages.join(" ");
|
|
910
|
+
}
|
|
911
|
+
return messages ?? null;
|
|
912
|
+
} catch (e) {
|
|
913
|
+
logger.error(
|
|
914
|
+
`Error fetching greet message for tunnel "${tunnelId}": ${e instanceof Error ? e.message : String(e)}`
|
|
915
|
+
);
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
getTunnelStats(tunnelId) {
|
|
920
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
921
|
+
if (!managed) {
|
|
922
|
+
return null;
|
|
923
|
+
}
|
|
924
|
+
const stats = this.tunnelStats.get(tunnelId);
|
|
925
|
+
return stats || null;
|
|
926
|
+
}
|
|
927
|
+
getLatestTunnelStats(tunnelId) {
|
|
928
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
929
|
+
if (!managed) {
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
const stats = this.tunnelStats.get(tunnelId);
|
|
933
|
+
if (stats && stats.length > 0) {
|
|
934
|
+
return stats[stats.length - 1];
|
|
935
|
+
}
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Registers a listener function to receive tunnel statistics updates.
|
|
940
|
+
* The listener will be called whenever any tunnel's stats are updated.
|
|
941
|
+
*
|
|
942
|
+
* @param tunnelId - The tunnel ID to listen to stats for
|
|
943
|
+
* @param listener - Function that receives tunnelId and stats when updates occur
|
|
944
|
+
* @returns A unique listener ID that can be used to deregister the listener and tunnelId
|
|
945
|
+
*
|
|
946
|
+
* @throws {Error} When the specified tunnelId does not exist
|
|
947
|
+
*/
|
|
948
|
+
registerStatsListener(tunnelId, listener) {
|
|
949
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
950
|
+
if (!managed) {
|
|
951
|
+
return Promise.reject(new Error(`Tunnel "${tunnelId}" not found`));
|
|
952
|
+
}
|
|
953
|
+
if (!this.tunnelStatsListeners.has(tunnelId)) {
|
|
954
|
+
this.tunnelStatsListeners.set(tunnelId, /* @__PURE__ */ new Map());
|
|
955
|
+
}
|
|
956
|
+
const listenerId = getRandomId();
|
|
957
|
+
const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
|
|
958
|
+
tunnelListeners.set(listenerId, listener);
|
|
959
|
+
logger.info("Stats listener registered for tunnel", { tunnelId, listenerId });
|
|
960
|
+
return Promise.resolve([listenerId, tunnelId]);
|
|
961
|
+
}
|
|
962
|
+
registerErrorListener(tunnelId, listener) {
|
|
963
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
964
|
+
if (!managed) {
|
|
965
|
+
return Promise.reject(new Error(`Tunnel "${tunnelId}" not found`));
|
|
966
|
+
}
|
|
967
|
+
if (!this.tunnelErrorListeners.has(tunnelId)) {
|
|
968
|
+
this.tunnelErrorListeners.set(tunnelId, /* @__PURE__ */ new Map());
|
|
969
|
+
}
|
|
970
|
+
const listenerId = getRandomId();
|
|
971
|
+
const tunnelErrorListeners = this.tunnelErrorListeners.get(tunnelId);
|
|
972
|
+
tunnelErrorListeners.set(listenerId, listener);
|
|
973
|
+
logger.info("Error listener registered for tunnel", { tunnelId, listenerId });
|
|
974
|
+
return Promise.resolve(listenerId);
|
|
975
|
+
}
|
|
976
|
+
registerPollingErrorListener(tunnelId, listener) {
|
|
977
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
978
|
+
if (!managed) {
|
|
979
|
+
return Promise.reject(new Error(`Tunnel "${tunnelId}" not found`));
|
|
980
|
+
}
|
|
981
|
+
if (!this.tunnelPollingErrorListeners.has(tunnelId)) {
|
|
982
|
+
this.tunnelPollingErrorListeners.set(tunnelId, /* @__PURE__ */ new Map());
|
|
983
|
+
}
|
|
984
|
+
const listenerId = getRandomId();
|
|
985
|
+
this.tunnelPollingErrorListeners.get(tunnelId).set(listenerId, listener);
|
|
986
|
+
logger.info("Polling error listener registered for tunnel", { tunnelId, listenerId });
|
|
987
|
+
return Promise.resolve(listenerId);
|
|
988
|
+
}
|
|
989
|
+
registerDisconnectListener(tunnelId, listener) {
|
|
990
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
991
|
+
if (!managed) {
|
|
992
|
+
return Promise.reject(new Error(`Tunnel "${tunnelId}" not found`));
|
|
993
|
+
}
|
|
994
|
+
if (!this.tunnelDisconnectListeners.has(tunnelId)) {
|
|
995
|
+
this.tunnelDisconnectListeners.set(tunnelId, /* @__PURE__ */ new Map());
|
|
996
|
+
}
|
|
997
|
+
const listenerId = getRandomId();
|
|
998
|
+
const tunnelDisconnectListeners = this.tunnelDisconnectListeners.get(tunnelId);
|
|
999
|
+
tunnelDisconnectListeners.set(listenerId, listener);
|
|
1000
|
+
logger.info("Disconnect listener registered for tunnel", { tunnelId, listenerId });
|
|
1001
|
+
return Promise.resolve(listenerId);
|
|
1002
|
+
}
|
|
1003
|
+
registerWorkerErrorListner(tunnelId, listener) {
|
|
1004
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
1005
|
+
if (!managed) {
|
|
1006
|
+
return Promise.reject(new Error(`Tunnel "${tunnelId}" not found`));
|
|
1007
|
+
}
|
|
1008
|
+
if (!this.tunnelWorkerErrorListeners.has(tunnelId)) {
|
|
1009
|
+
this.tunnelWorkerErrorListeners.set(tunnelId, /* @__PURE__ */ new Map());
|
|
1010
|
+
}
|
|
1011
|
+
const listenerId = getRandomId();
|
|
1012
|
+
const tunnelWorkerErrorListner = this.tunnelWorkerErrorListeners.get(tunnelId);
|
|
1013
|
+
tunnelWorkerErrorListner?.set(listenerId, listener);
|
|
1014
|
+
logger.info("TunnelWorker error listener registered for tunnel", { tunnelId, listenerId });
|
|
1015
|
+
return Promise.resolve(listenerId);
|
|
1016
|
+
}
|
|
1017
|
+
deregisterWorkerErrorListener(tunnelId, listenerId) {
|
|
1018
|
+
const listeners = this.tunnelWorkerErrorListeners.get(tunnelId);
|
|
1019
|
+
if (!listeners) {
|
|
1020
|
+
logger.warn("No worker error listeners found for tunnel", { tunnelId });
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
const removed = listeners.delete(listenerId);
|
|
1024
|
+
if (removed) {
|
|
1025
|
+
logger.info("Worker error listener deregistered", { tunnelId, listenerId });
|
|
1026
|
+
if (listeners.size === 0) {
|
|
1027
|
+
this.tunnelWorkerErrorListeners.delete(tunnelId);
|
|
1028
|
+
}
|
|
1029
|
+
} else {
|
|
1030
|
+
logger.warn("Attempted to deregister non-existent worker error listener", { tunnelId, listenerId });
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
registerStartListener(tunnelId, listener) {
|
|
1034
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
1035
|
+
if (!managed) {
|
|
1036
|
+
return Promise.reject(new Error(`Tunnel "${tunnelId}" not found`));
|
|
1037
|
+
}
|
|
1038
|
+
if (!this.tunnelStartListeners.has(tunnelId)) {
|
|
1039
|
+
this.tunnelStartListeners.set(tunnelId, /* @__PURE__ */ new Map());
|
|
1040
|
+
}
|
|
1041
|
+
const listenerId = getRandomId();
|
|
1042
|
+
const listeners = this.tunnelStartListeners.get(tunnelId);
|
|
1043
|
+
listeners.set(listenerId, listener);
|
|
1044
|
+
logger.info("Start listener registered for tunnel", { tunnelId, listenerId });
|
|
1045
|
+
return Promise.resolve(listenerId);
|
|
1046
|
+
}
|
|
1047
|
+
registerWillReconnectListener(tunnelId, listener) {
|
|
1048
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
1049
|
+
if (!managed) {
|
|
1050
|
+
return Promise.reject(new Error(`Tunnel "${tunnelId}" not found`));
|
|
1051
|
+
}
|
|
1052
|
+
if (!this.tunnelWillReconnectListeners.has(tunnelId)) {
|
|
1053
|
+
this.tunnelWillReconnectListeners.set(tunnelId, /* @__PURE__ */ new Map());
|
|
1054
|
+
}
|
|
1055
|
+
const listenerId = getRandomId();
|
|
1056
|
+
this.tunnelWillReconnectListeners.get(tunnelId).set(listenerId, listener);
|
|
1057
|
+
logger.info("WillReconnect listener registered for tunnel", { tunnelId, listenerId });
|
|
1058
|
+
return Promise.resolve(listenerId);
|
|
1059
|
+
}
|
|
1060
|
+
registerReconnectingListener(tunnelId, listener) {
|
|
1061
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
1062
|
+
if (!managed) {
|
|
1063
|
+
return Promise.reject(new Error(`Tunnel "${tunnelId}" not found`));
|
|
1064
|
+
}
|
|
1065
|
+
if (!this.tunnelReconnectingListeners.has(tunnelId)) {
|
|
1066
|
+
this.tunnelReconnectingListeners.set(tunnelId, /* @__PURE__ */ new Map());
|
|
1067
|
+
}
|
|
1068
|
+
const listenerId = getRandomId();
|
|
1069
|
+
this.tunnelReconnectingListeners.get(tunnelId).set(listenerId, listener);
|
|
1070
|
+
logger.info("Reconnecting listener registered for tunnel", { tunnelId, listenerId });
|
|
1071
|
+
return Promise.resolve(listenerId);
|
|
1072
|
+
}
|
|
1073
|
+
registerReconnectionCompletedListener(tunnelId, listener) {
|
|
1074
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
1075
|
+
if (!managed) {
|
|
1076
|
+
return Promise.reject(new Error(`Tunnel "${tunnelId}" not found`));
|
|
1077
|
+
}
|
|
1078
|
+
if (!this.tunnelReconnectionCompletedListeners.has(tunnelId)) {
|
|
1079
|
+
this.tunnelReconnectionCompletedListeners.set(tunnelId, /* @__PURE__ */ new Map());
|
|
1080
|
+
}
|
|
1081
|
+
const listenerId = getRandomId();
|
|
1082
|
+
this.tunnelReconnectionCompletedListeners.get(tunnelId).set(listenerId, listener);
|
|
1083
|
+
logger.info("ReconnectionCompleted listener registered for tunnel", { tunnelId, listenerId });
|
|
1084
|
+
return Promise.resolve(listenerId);
|
|
1085
|
+
}
|
|
1086
|
+
registerReconnectionFailedListener(tunnelId, listener) {
|
|
1087
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
1088
|
+
if (!managed) {
|
|
1089
|
+
return Promise.reject(new Error(`Tunnel "${tunnelId}" not found`));
|
|
1090
|
+
}
|
|
1091
|
+
if (!this.tunnelReconnectionFailedListeners.has(tunnelId)) {
|
|
1092
|
+
this.tunnelReconnectionFailedListeners.set(tunnelId, /* @__PURE__ */ new Map());
|
|
1093
|
+
}
|
|
1094
|
+
const listenerId = getRandomId();
|
|
1095
|
+
this.tunnelReconnectionFailedListeners.get(tunnelId).set(listenerId, listener);
|
|
1096
|
+
logger.info("ReconnectionFailed listener registered for tunnel", { tunnelId, listenerId });
|
|
1097
|
+
return Promise.resolve(listenerId);
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Removes a previously registered stats listener.
|
|
1101
|
+
*
|
|
1102
|
+
* @param tunnelId - The tunnel ID the listener was registered for
|
|
1103
|
+
* @param listenerId - The unique ID returned when the listener was registered
|
|
1104
|
+
*/
|
|
1105
|
+
deregisterStatsListener(tunnelId, listenerId) {
|
|
1106
|
+
const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
|
|
1107
|
+
if (!tunnelListeners) {
|
|
1108
|
+
logger.warn("No listeners found for tunnel", { tunnelId });
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
const removed = tunnelListeners.delete(listenerId);
|
|
1112
|
+
if (removed) {
|
|
1113
|
+
logger.info("Stats listener deregistered", { tunnelId, listenerId });
|
|
1114
|
+
if (tunnelListeners.size === 0) {
|
|
1115
|
+
this.tunnelStatsListeners.delete(tunnelId);
|
|
1116
|
+
}
|
|
1117
|
+
} else {
|
|
1118
|
+
logger.warn("Attempted to deregister non-existent stats listener", { tunnelId, listenerId });
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
deregisterErrorListener(tunnelId, listenerId) {
|
|
1122
|
+
const listeners = this.tunnelErrorListeners.get(tunnelId);
|
|
1123
|
+
if (!listeners) {
|
|
1124
|
+
logger.warn("No error listeners found for tunnel", { tunnelId });
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
const removed = listeners.delete(listenerId);
|
|
1128
|
+
if (removed) {
|
|
1129
|
+
logger.info("Error listener deregistered", { tunnelId, listenerId });
|
|
1130
|
+
if (listeners.size === 0) {
|
|
1131
|
+
this.tunnelErrorListeners.delete(tunnelId);
|
|
1132
|
+
}
|
|
1133
|
+
} else {
|
|
1134
|
+
logger.warn("Attempted to deregister non-existent error listener", { tunnelId, listenerId });
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
deregisterPollingErrorListener(tunnelId, listenerId) {
|
|
1138
|
+
const listeners = this.tunnelPollingErrorListeners.get(tunnelId);
|
|
1139
|
+
if (!listeners) {
|
|
1140
|
+
logger.warn("No polling error listeners found for tunnel", { tunnelId });
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
const removed = listeners.delete(listenerId);
|
|
1144
|
+
if (removed) {
|
|
1145
|
+
logger.info("Polling error listener deregistered", { tunnelId, listenerId });
|
|
1146
|
+
if (listeners.size === 0) {
|
|
1147
|
+
this.tunnelPollingErrorListeners.delete(tunnelId);
|
|
1148
|
+
}
|
|
1149
|
+
} else {
|
|
1150
|
+
logger.warn("Attempted to deregister non-existent polling error listener", { tunnelId, listenerId });
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
deregisterDisconnectListener(tunnelId, listenerId) {
|
|
1154
|
+
const listeners = this.tunnelDisconnectListeners.get(tunnelId);
|
|
1155
|
+
if (!listeners) {
|
|
1156
|
+
logger.warn("No disconnect listeners found for tunnel", { tunnelId });
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
const removed = listeners.delete(listenerId);
|
|
1160
|
+
if (removed) {
|
|
1161
|
+
logger.info("Disconnect listener deregistered", { tunnelId, listenerId });
|
|
1162
|
+
if (listeners.size === 0) {
|
|
1163
|
+
this.tunnelDisconnectListeners.delete(tunnelId);
|
|
1164
|
+
}
|
|
1165
|
+
} else {
|
|
1166
|
+
logger.warn("Attempted to deregister non-existent disconnect listener", { tunnelId, listenerId });
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
registerStoppedListener(tunnelId, listener) {
|
|
1170
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
1171
|
+
if (!managed) {
|
|
1172
|
+
return Promise.reject(new Error(`Tunnel "${tunnelId}" not found`));
|
|
1173
|
+
}
|
|
1174
|
+
if (!this.tunnelStoppedListeners.has(tunnelId)) {
|
|
1175
|
+
this.tunnelStoppedListeners.set(tunnelId, /* @__PURE__ */ new Map());
|
|
1176
|
+
}
|
|
1177
|
+
const listenerId = getRandomId();
|
|
1178
|
+
this.tunnelStoppedListeners.get(tunnelId).set(listenerId, listener);
|
|
1179
|
+
return Promise.resolve(listenerId);
|
|
1180
|
+
}
|
|
1181
|
+
deregisterStoppedListener(tunnelId, listenerId) {
|
|
1182
|
+
const listeners = this.tunnelStoppedListeners.get(tunnelId);
|
|
1183
|
+
if (!listeners) return;
|
|
1184
|
+
listeners.delete(listenerId);
|
|
1185
|
+
if (listeners.size === 0) {
|
|
1186
|
+
this.tunnelStoppedListeners.delete(tunnelId);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
deregisterWillReconnectListener(tunnelId, listenerId) {
|
|
1190
|
+
const listeners = this.tunnelWillReconnectListeners.get(tunnelId);
|
|
1191
|
+
if (!listeners) {
|
|
1192
|
+
logger.warn("No will-reconnect listeners found for tunnel", { tunnelId });
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
;
|
|
1196
|
+
const removed = listeners.delete(listenerId);
|
|
1197
|
+
if (removed) {
|
|
1198
|
+
logger.info("WillReconnect listener deregistered", { tunnelId, listenerId });
|
|
1199
|
+
if (listeners.size === 0) {
|
|
1200
|
+
this.tunnelWillReconnectListeners.delete(tunnelId);
|
|
1201
|
+
}
|
|
1202
|
+
} else {
|
|
1203
|
+
logger.warn("Attempted to deregister non-existent will-reconnect listener", { tunnelId, listenerId });
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
deregisterReconnectingListener(tunnelId, listenerId) {
|
|
1207
|
+
const listeners = this.tunnelReconnectingListeners.get(tunnelId);
|
|
1208
|
+
if (!listeners) {
|
|
1209
|
+
logger.warn("No reconnecting listeners found for tunnel", { tunnelId });
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
;
|
|
1213
|
+
const removed = listeners.delete(listenerId);
|
|
1214
|
+
if (removed) {
|
|
1215
|
+
logger.info("Reconnecting listener deregistered", { tunnelId, listenerId });
|
|
1216
|
+
if (listeners.size === 0) {
|
|
1217
|
+
this.tunnelReconnectingListeners.delete(tunnelId);
|
|
1218
|
+
}
|
|
1219
|
+
} else {
|
|
1220
|
+
logger.warn("Attempted to deregister non-existent reconnecting listener", { tunnelId, listenerId });
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
deregisterReconnectionCompletedListener(tunnelId, listenerId) {
|
|
1224
|
+
const listeners = this.tunnelReconnectionCompletedListeners.get(tunnelId);
|
|
1225
|
+
if (!listeners) {
|
|
1226
|
+
logger.warn("No reconnection completed listeners found for tunnel", { tunnelId });
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
const removed = listeners.delete(listenerId);
|
|
1230
|
+
if (removed) {
|
|
1231
|
+
logger.info("Reconnection completed listener deregistered", { tunnelId, listenerId });
|
|
1232
|
+
if (listeners.size === 0) {
|
|
1233
|
+
this.tunnelReconnectionCompletedListeners.delete(tunnelId);
|
|
1234
|
+
}
|
|
1235
|
+
} else {
|
|
1236
|
+
logger.warn("Attempted to deregister non-existent reconnection completed listener", { tunnelId, listenerId });
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
deregisterReconnectionFailedListener(tunnelId, listenerId) {
|
|
1240
|
+
const listeners = this.tunnelReconnectionFailedListeners.get(tunnelId);
|
|
1241
|
+
if (!listeners) {
|
|
1242
|
+
logger.warn("No reconnection failed listeners found for tunnel", { tunnelId });
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
const removed = listeners.delete(listenerId);
|
|
1246
|
+
if (removed) {
|
|
1247
|
+
logger.info("Reconnection failed listener deregistered", { tunnelId, listenerId });
|
|
1248
|
+
if (listeners.size === 0) {
|
|
1249
|
+
this.tunnelReconnectionFailedListeners.delete(tunnelId);
|
|
1250
|
+
}
|
|
1251
|
+
} else {
|
|
1252
|
+
logger.warn("Attempted to deregister non-existent reconnection failed listener", { tunnelId, listenerId });
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
async getLocalserverTlsInfo(tunnelId) {
|
|
1256
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
1257
|
+
if (!managed) {
|
|
1258
|
+
logger.error(`Tunnel "${tunnelId}" not found when fetching local server TLS info`);
|
|
1259
|
+
return false;
|
|
1260
|
+
}
|
|
1261
|
+
try {
|
|
1262
|
+
if (managed.isStopped) {
|
|
1263
|
+
return false;
|
|
1264
|
+
}
|
|
1265
|
+
const tlsInfo = await managed.instance.getLocalServerTls();
|
|
1266
|
+
if (tlsInfo) {
|
|
1267
|
+
return tlsInfo;
|
|
1268
|
+
}
|
|
1269
|
+
return false;
|
|
1270
|
+
} catch (e) {
|
|
1271
|
+
logger.error(`Error fetching TLS info for tunnel "${tunnelId}": ${e instanceof Error ? e.message : e}`);
|
|
1272
|
+
return false;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Sets up the stats callback for a tunnel during creation.
|
|
1277
|
+
* This callback will update stored stats and notify all registered listeners.
|
|
1278
|
+
*/
|
|
1279
|
+
setupStatsCallback(tunnelId, managed) {
|
|
1280
|
+
try {
|
|
1281
|
+
const callback = (usage) => {
|
|
1282
|
+
this.updateStats(tunnelId, usage);
|
|
1283
|
+
};
|
|
1284
|
+
managed.instance.setUsageUpdateCallback(callback);
|
|
1285
|
+
logger.debug("Stats callback set up for tunnel", { tunnelId });
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
logger.warn("Failed to set up stats callback", { tunnelId, error });
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
setupTunnelPollingErrorCallback(tunnelId, managed) {
|
|
1291
|
+
try {
|
|
1292
|
+
const callback = ({ error }) => {
|
|
1293
|
+
try {
|
|
1294
|
+
const errorMessage2 = error instanceof Error ? error.message : String(error);
|
|
1295
|
+
logger.info("Tunnel reported polling error", { tunnelId, errorMessage: errorMessage2 });
|
|
1296
|
+
const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
|
|
1297
|
+
if (managedTunnel) {
|
|
1298
|
+
managedTunnel.lastError = {
|
|
1299
|
+
message: errorMessage2,
|
|
1300
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1301
|
+
isFatal: true
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
this.notifyPollingErrorListeners(tunnelId, errorMessage2);
|
|
1305
|
+
} catch (e) {
|
|
1306
|
+
logger.warn("Error handling tunnel polling error callback", { tunnelId, e });
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
managed.instance.setPollingErrorCallback(callback);
|
|
1310
|
+
logger.debug("Tunnel polling error callback set up for tunnel", { tunnelId });
|
|
1311
|
+
} catch (error) {
|
|
1312
|
+
logger.warn("Failed to set up tunnel polling error callback", { tunnelId, error });
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
notifyPollingErrorListeners(tunnelId, errorMsg) {
|
|
1316
|
+
try {
|
|
1317
|
+
const listeners = this.tunnelPollingErrorListeners.get(tunnelId);
|
|
1318
|
+
if (!listeners) {
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
for (const [id, listener] of listeners) {
|
|
1322
|
+
try {
|
|
1323
|
+
listener(tunnelId, errorMsg);
|
|
1324
|
+
} catch (err) {
|
|
1325
|
+
logger.debug("Error in polling-error-listener callback", { listenerId: id, tunnelId, err });
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
} catch (err) {
|
|
1329
|
+
logger.debug("Failed to notify polling error listeners", { tunnelId, err });
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
notifyErrorListeners(tunnelId, errorMsg, isFatal) {
|
|
1333
|
+
try {
|
|
1334
|
+
const listeners = this.tunnelErrorListeners.get(tunnelId);
|
|
1335
|
+
if (!listeners) {
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
for (const [id, listener] of listeners) {
|
|
1339
|
+
try {
|
|
1340
|
+
listener(tunnelId, errorMsg, isFatal);
|
|
1341
|
+
} catch (err) {
|
|
1342
|
+
logger.debug("Error in error-listener callback", { listenerId: id, tunnelId, err });
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
} catch (err) {
|
|
1346
|
+
logger.debug("Failed to notify error listeners", { tunnelId, err });
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
setupErrorCallback(tunnelId, managed) {
|
|
1350
|
+
try {
|
|
1351
|
+
const callback = ({ errorNo, error, recoverable }) => {
|
|
1352
|
+
try {
|
|
1353
|
+
const msg = typeof error === "string" ? error : String(error);
|
|
1354
|
+
const isFatal = true;
|
|
1355
|
+
logger.debug("Tunnel reported error", { tunnelId, errorNo, errorMsg: msg, recoverable });
|
|
1356
|
+
const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
|
|
1357
|
+
if (managedTunnel) {
|
|
1358
|
+
managedTunnel.lastError = {
|
|
1359
|
+
message: msg,
|
|
1360
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1361
|
+
isFatal: false
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
this.notifyErrorListeners(tunnelId, msg, isFatal);
|
|
1365
|
+
} catch (e) {
|
|
1366
|
+
logger.warn("Error handling tunnel error callback", { tunnelId, e });
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
managed.instance.setTunnelErrorCallback(callback);
|
|
1370
|
+
logger.debug("Error callback set up for tunnel", { tunnelId });
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
logger.warn("Failed to set up error callback", { tunnelId, error });
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
setupDisconnectCallback(tunnelId, managed) {
|
|
1376
|
+
try {
|
|
1377
|
+
const callback = ({ error, messages }) => {
|
|
1378
|
+
try {
|
|
1379
|
+
logger.debug("Tunnel disconnected", { tunnelId, error, messages });
|
|
1380
|
+
const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
|
|
1381
|
+
if (managedTunnel) {
|
|
1382
|
+
managedTunnel.isStopped = true;
|
|
1383
|
+
managedTunnel.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1384
|
+
}
|
|
1385
|
+
if (managedTunnel?.isStopping) {
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
const listeners = this.tunnelDisconnectListeners.get(tunnelId);
|
|
1389
|
+
if (!listeners) {
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
for (const [id, listener] of listeners) {
|
|
1393
|
+
try {
|
|
1394
|
+
listener(tunnelId, error, messages);
|
|
1395
|
+
} catch (err) {
|
|
1396
|
+
logger.debug("Error in disconnect-listener callback", { listenerId: id, tunnelId, err });
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
} catch (e) {
|
|
1400
|
+
logger.warn("Error handling tunnel disconnect callback", { tunnelId, e });
|
|
1401
|
+
}
|
|
1402
|
+
};
|
|
1403
|
+
managed.instance.setTunnelDisconnectedCallback(callback);
|
|
1404
|
+
logger.debug("Disconnect callback set up for tunnel", { tunnelId });
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
logger.warn("Failed to set up disconnect callback", { tunnelId, error });
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Called when the tunnel disconnects and the SDK is about to start reconnecting.
|
|
1411
|
+
* Notifies registered will-reconnect listeners.
|
|
1412
|
+
*/
|
|
1413
|
+
setupWillReconnectCallback(tunnelId, managed) {
|
|
1414
|
+
try {
|
|
1415
|
+
const callback = ({ error, messages }) => {
|
|
1416
|
+
try {
|
|
1417
|
+
logger.info("Tunnel will reconnect", { tunnelId, error, messages });
|
|
1418
|
+
const listeners = this.tunnelWillReconnectListeners.get(tunnelId);
|
|
1419
|
+
if (!listeners) {
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
for (const [id, listener] of listeners) {
|
|
1423
|
+
try {
|
|
1424
|
+
listener(tunnelId, error, messages);
|
|
1425
|
+
} catch (err) {
|
|
1426
|
+
logger.debug("Error in will-reconnect-listener callback", { listenerId: id, tunnelId, err });
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
} catch (e) {
|
|
1430
|
+
logger.warn("Error handling will-reconnect callback", { tunnelId, e });
|
|
1431
|
+
}
|
|
1432
|
+
};
|
|
1433
|
+
managed.instance.setWillReconnectCallback(callback);
|
|
1434
|
+
logger.debug("WillReconnect callback set up for tunnel", { tunnelId });
|
|
1435
|
+
} catch (error) {
|
|
1436
|
+
logger.warn("Failed to set up will-reconnect callback", { tunnelId, error });
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Called for each reconnection attempt with the current retry count.
|
|
1441
|
+
* Notifies registered reconnecting listeners.
|
|
1442
|
+
*/
|
|
1443
|
+
setupReconnectingCallback(tunnelId, managed) {
|
|
1444
|
+
try {
|
|
1445
|
+
const callback = ({ retryCnt }) => {
|
|
1446
|
+
try {
|
|
1447
|
+
logger.info("Tunnel reconnecting", { tunnelId, retryCnt });
|
|
1448
|
+
const listeners = this.tunnelReconnectingListeners.get(tunnelId);
|
|
1449
|
+
if (!listeners) {
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
for (const [id, listener] of listeners) {
|
|
1453
|
+
try {
|
|
1454
|
+
listener(tunnelId, retryCnt);
|
|
1455
|
+
} catch (err) {
|
|
1456
|
+
logger.debug("Error in reconnecting-listener callback", { listenerId: id, tunnelId, err });
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
} catch (e) {
|
|
1460
|
+
logger.warn("Error handling reconnecting callback", { tunnelId, e });
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
managed.instance.setReconnectingCallback(callback);
|
|
1464
|
+
logger.debug("Reconnecting callback set up for tunnel", { tunnelId });
|
|
1465
|
+
} catch (error) {
|
|
1466
|
+
logger.warn("Failed to set up reconnecting callback", { tunnelId, error });
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* Called when reconnection succeeds. Updates tunnel state back to active,
|
|
1471
|
+
* and notifies registered reconnection-completed and start listeners with new URLs.
|
|
1472
|
+
*/
|
|
1473
|
+
setupReconnectionCompletedCallback(tunnelId, managed) {
|
|
1474
|
+
try {
|
|
1475
|
+
const callback = ({ urls }) => {
|
|
1476
|
+
try {
|
|
1477
|
+
logger.info("Tunnel reconnection completed", { tunnelId, urls });
|
|
1478
|
+
const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
|
|
1479
|
+
if (managedTunnel) {
|
|
1480
|
+
managedTunnel.isStopped = false;
|
|
1481
|
+
managedTunnel.startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1482
|
+
managedTunnel.stoppedAt = null;
|
|
1483
|
+
}
|
|
1484
|
+
const listeners = this.tunnelReconnectionCompletedListeners.get(tunnelId);
|
|
1485
|
+
if (listeners) {
|
|
1486
|
+
for (const [id, listener] of listeners) {
|
|
1487
|
+
try {
|
|
1488
|
+
listener(tunnelId, urls);
|
|
1489
|
+
} catch (err) {
|
|
1490
|
+
logger.debug("Error in reconnection-completed-listener callback", { listenerId: id, tunnelId, err });
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
const startListeners = this.tunnelStartListeners.get(tunnelId);
|
|
1495
|
+
if (startListeners) {
|
|
1496
|
+
for (const [id, listener] of startListeners) {
|
|
1497
|
+
try {
|
|
1498
|
+
listener(tunnelId, urls);
|
|
1499
|
+
} catch (err) {
|
|
1500
|
+
logger.debug("Error in start-listener callback on reconnection", { listenerId: id, tunnelId, err });
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
} catch (e) {
|
|
1505
|
+
logger.warn("Error handling reconnection-completed callback", { tunnelId, e });
|
|
1506
|
+
}
|
|
1507
|
+
};
|
|
1508
|
+
managed.instance.setReconnectionCompletedCallback(callback);
|
|
1509
|
+
logger.debug("ReconnectionCompleted callback set up for tunnel", { tunnelId });
|
|
1510
|
+
} catch (error) {
|
|
1511
|
+
logger.warn("Failed to set up reconnection-completed callback", { tunnelId, error });
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Called when all reconnection attempts are exhausted.
|
|
1516
|
+
* Marks the tunnel as stopped and notifies registered reconnection-failed listeners.
|
|
1517
|
+
*/
|
|
1518
|
+
setupReconnectionFailedCallback(tunnelId, managed) {
|
|
1519
|
+
try {
|
|
1520
|
+
const callback = ({ retryCnt }) => {
|
|
1521
|
+
try {
|
|
1522
|
+
logger.error("Tunnel reconnection failed", { tunnelId, retryCnt });
|
|
1523
|
+
const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
|
|
1524
|
+
if (managedTunnel) {
|
|
1525
|
+
managedTunnel.isStopped = true;
|
|
1526
|
+
managedTunnel.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1527
|
+
}
|
|
1528
|
+
const listeners = this.tunnelReconnectionFailedListeners.get(tunnelId);
|
|
1529
|
+
if (!listeners) {
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
for (const [id, listener] of listeners) {
|
|
1533
|
+
try {
|
|
1534
|
+
listener(tunnelId, retryCnt);
|
|
1535
|
+
} catch (err) {
|
|
1536
|
+
logger.debug("Error in reconnection-failed-listener callback", { listenerId: id, tunnelId, err });
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
} catch (e) {
|
|
1540
|
+
logger.warn("Error handling reconnection-failed callback", { tunnelId, e });
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
managed.instance.setReconnectionFailedCallback(callback);
|
|
1544
|
+
logger.debug("ReconnectionFailed callback set up for tunnel", { tunnelId });
|
|
1545
|
+
} catch (error) {
|
|
1546
|
+
logger.warn("Failed to set up reconnection-failed callback", { tunnelId, error });
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
setUpTunnelWorkerErrorCallback(tunnelId, managed) {
|
|
1550
|
+
try {
|
|
1551
|
+
const callback = (error) => {
|
|
1552
|
+
try {
|
|
1553
|
+
logger.debug("Error in Tunnel Worker", { tunnelId, errorMessage: error.message });
|
|
1554
|
+
const listeners = this.tunnelWorkerErrorListeners.get(tunnelId);
|
|
1555
|
+
if (!listeners) {
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
for (const [id, listener] of listeners) {
|
|
1559
|
+
try {
|
|
1560
|
+
listener(tunnelId, error);
|
|
1561
|
+
} catch (err) {
|
|
1562
|
+
logger.debug("Error in worker-error-listener callback", { listenerId: id, tunnelId, err });
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
} catch (e) {
|
|
1566
|
+
logger.warn("Error handling tunnel worker error callback", { tunnelId, e });
|
|
1567
|
+
}
|
|
1568
|
+
};
|
|
1569
|
+
managed.instance.setWorkerErrorCallback(callback);
|
|
1570
|
+
logger.debug("Disconnect callback set up for tunnel", { tunnelId });
|
|
1571
|
+
} catch (error) {
|
|
1572
|
+
logger.warn("Failed to setup tunnel worker error callback");
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Updates the stored stats for a tunnel and notifies all registered listeners.
|
|
1577
|
+
*/
|
|
1578
|
+
updateStats(tunnelId, rawUsage) {
|
|
1579
|
+
try {
|
|
1580
|
+
const normalizedStats = this.normalizeStats(rawUsage);
|
|
1581
|
+
const existingStats = this.tunnelStats.get(tunnelId) ?? [];
|
|
1582
|
+
const updatedStats = existingStats.length >= STATS_HISTORY_LIMIT ? [...existingStats.slice(existingStats.length - STATS_HISTORY_LIMIT + 1), normalizedStats] : [...existingStats, normalizedStats];
|
|
1583
|
+
this.tunnelStats.set(tunnelId, updatedStats);
|
|
1584
|
+
const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
|
|
1585
|
+
if (tunnelListeners) {
|
|
1586
|
+
for (const [listenerId, listener] of tunnelListeners) {
|
|
1587
|
+
try {
|
|
1588
|
+
listener(tunnelId, normalizedStats);
|
|
1589
|
+
} catch (error) {
|
|
1590
|
+
logger.warn("Error in stats listener callback", { listenerId, tunnelId, error });
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
logger.debug("Stats updated and listeners notified", {
|
|
1595
|
+
tunnelId,
|
|
1596
|
+
listenersCount: tunnelListeners?.size || 0
|
|
1597
|
+
});
|
|
1598
|
+
} catch (error) {
|
|
1599
|
+
logger.warn("Error updating stats", { tunnelId, error });
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Normalizes raw usage data from the SDK into a consistent TunnelStats format.
|
|
1604
|
+
*/
|
|
1605
|
+
normalizeStats(rawStats) {
|
|
1606
|
+
const elapsed = this.parseNumber(rawStats.elapsedTime ?? 0);
|
|
1607
|
+
const liveConns = this.parseNumber(rawStats.numLiveConnections ?? 0);
|
|
1608
|
+
const totalConns = this.parseNumber(rawStats.numTotalConnections ?? 0);
|
|
1609
|
+
const reqBytes = this.parseNumber(rawStats.numTotalReqBytes ?? 0);
|
|
1610
|
+
const resBytes = this.parseNumber(rawStats.numTotalResBytes ?? 0);
|
|
1611
|
+
const txBytes = this.parseNumber(rawStats.numTotalTxBytes ?? 0);
|
|
1612
|
+
return {
|
|
1613
|
+
elapsedTime: elapsed,
|
|
1614
|
+
numLiveConnections: liveConns,
|
|
1615
|
+
numTotalConnections: totalConns,
|
|
1616
|
+
numTotalReqBytes: reqBytes,
|
|
1617
|
+
numTotalResBytes: resBytes,
|
|
1618
|
+
numTotalTxBytes: txBytes
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
parseNumber(value) {
|
|
1622
|
+
const parsed = typeof value === "number" ? value : parseInt(String(value), 10);
|
|
1623
|
+
return isNaN(parsed) ? 0 : parsed;
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Read serve path only from config.optional.serve.
|
|
1627
|
+
*/
|
|
1628
|
+
resolveServePath(config) {
|
|
1629
|
+
const optional = config.optional;
|
|
1630
|
+
const servePath = optional?.serve;
|
|
1631
|
+
logger.debug("resolveServePath", { servePath, hasOptional: !!optional, optionalKeys: optional ? Object.keys(optional) : [] });
|
|
1632
|
+
return servePath;
|
|
1633
|
+
}
|
|
1634
|
+
startStaticFileServer(managed) {
|
|
1635
|
+
try {
|
|
1636
|
+
const __filename4 = fileURLToPath2(import.meta.url);
|
|
1637
|
+
const __dirname4 = path2.dirname(__filename4);
|
|
1638
|
+
const fileServerWorkerPath = path2.join(__dirname4, "workers", "file_serve_worker.cjs");
|
|
1639
|
+
logger.info("Starting static file server worker", {
|
|
1640
|
+
dir: managed.serve,
|
|
1641
|
+
forwarding: JSON.stringify(managed.tunnelConfig?.forwarding),
|
|
1642
|
+
workerPath: fileServerWorkerPath
|
|
1643
|
+
});
|
|
1644
|
+
const staticServerWorker = new Worker(fileServerWorkerPath, {
|
|
1645
|
+
workerData: {
|
|
1646
|
+
dir: managed.serve,
|
|
1647
|
+
forwarding: managed.tunnelConfig?.forwarding
|
|
1648
|
+
}
|
|
1649
|
+
});
|
|
1650
|
+
staticServerWorker.on("message", (msg) => {
|
|
1651
|
+
switch (msg.type) {
|
|
1652
|
+
case FileServerMessage.Started:
|
|
1653
|
+
logger.info("Static file server started", { dir: managed.serve, port: msg.portNum });
|
|
1654
|
+
break;
|
|
1655
|
+
case FileServerMessage.Warning:
|
|
1656
|
+
if (msg.code === "INVALID_TUNNEL_SERVE_PATH") {
|
|
1657
|
+
managed.warnings = managed.warnings ?? [];
|
|
1658
|
+
managed.warnings.push({ code: msg.code, message: msg.message });
|
|
1659
|
+
}
|
|
1660
|
+
printer_default.warn(msg.message);
|
|
1661
|
+
break;
|
|
1662
|
+
case FileServerMessage.Error:
|
|
1663
|
+
managed.warnings = managed.warnings ?? [];
|
|
1664
|
+
managed.warnings.push({
|
|
1665
|
+
code: "UNKNOWN_WARNING",
|
|
1666
|
+
message: msg.error
|
|
1667
|
+
});
|
|
1668
|
+
break;
|
|
1669
|
+
}
|
|
1670
|
+
});
|
|
1671
|
+
managed.serveWorker = staticServerWorker;
|
|
1672
|
+
} catch (error) {
|
|
1673
|
+
logger.error("Error starting static file server", error);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
};
|
|
1677
|
+
|
|
1678
|
+
export {
|
|
1679
|
+
printer_default,
|
|
1680
|
+
setTunnelLoggingEnabled,
|
|
1681
|
+
isTunnelLoggingEnabled,
|
|
1682
|
+
detachAllTunnelLoggers,
|
|
1683
|
+
getRandomId,
|
|
1684
|
+
isValidPort,
|
|
1685
|
+
errorMessage,
|
|
1686
|
+
getVersion,
|
|
1687
|
+
getLocalAddress,
|
|
1688
|
+
TunnelAlreadyRunningError,
|
|
1689
|
+
TunnelManager
|
|
1690
|
+
};
|