@serve.zone/dcrouter 9.1.4 → 9.1.6
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/dist_serve/bundle.js +2 -2
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.js +26 -1
- package/dist_ts/opsserver/classes.opsserver.js +5 -1
- package/dist_ts/opsserver/handlers/logs.handler.d.ts +10 -0
- package/dist_ts/opsserver/handlers/logs.handler.js +63 -27
- package/dist_ts/remoteingress/classes.tunnel-manager.d.ts +6 -0
- package/dist_ts/remoteingress/classes.tunnel-manager.js +47 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/package.json +6 -6
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +27 -1
- package/ts/opsserver/classes.opsserver.ts +4 -0
- package/ts/opsserver/handlers/logs.handler.ts +82 -41
- package/ts/remoteingress/classes.tunnel-manager.ts +49 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
|
@@ -3,8 +3,15 @@ import type { OpsServer } from '../classes.opsserver.js';
|
|
|
3
3
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
4
|
import { logBuffer, baseLogger } from '../../logger.js';
|
|
5
5
|
|
|
6
|
+
// Module-level singleton: the log push destination is added once and reuses
|
|
7
|
+
// the current OpsServer reference so it survives OpsServer restarts without
|
|
8
|
+
// accumulating duplicate destinations.
|
|
9
|
+
let logPushDestinationInstalled = false;
|
|
10
|
+
let currentOpsServerRef: OpsServer | null = null;
|
|
11
|
+
|
|
6
12
|
export class LogsHandler {
|
|
7
13
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
14
|
+
private activeStreamStops: Set<() => void> = new Set();
|
|
8
15
|
|
|
9
16
|
constructor(private opsServerRef: OpsServer) {
|
|
10
17
|
// Add this handler's router to the parent
|
|
@@ -12,7 +19,21 @@ export class LogsHandler {
|
|
|
12
19
|
this.registerHandlers();
|
|
13
20
|
this.setupLogPushDestination();
|
|
14
21
|
}
|
|
15
|
-
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Clean up all active log streams and deactivate the push destination.
|
|
25
|
+
* Called when OpsServer stops.
|
|
26
|
+
*/
|
|
27
|
+
public cleanup(): void {
|
|
28
|
+
// Stop all active follow-mode log streams
|
|
29
|
+
for (const stop of this.activeStreamStops) {
|
|
30
|
+
stop();
|
|
31
|
+
}
|
|
32
|
+
this.activeStreamStops.clear();
|
|
33
|
+
// Deactivate the push destination (it stays registered but becomes a no-op)
|
|
34
|
+
currentOpsServerRef = null;
|
|
35
|
+
}
|
|
36
|
+
|
|
16
37
|
private registerHandlers(): void {
|
|
17
38
|
// Get Recent Logs Handler
|
|
18
39
|
this.typedrouter.addTypedHandler(
|
|
@@ -27,16 +48,16 @@ export class LogsHandler {
|
|
|
27
48
|
dataArg.search,
|
|
28
49
|
dataArg.timeRange
|
|
29
50
|
);
|
|
30
|
-
|
|
51
|
+
|
|
31
52
|
return {
|
|
32
53
|
logs,
|
|
33
|
-
total: logs.length,
|
|
34
|
-
hasMore: false,
|
|
54
|
+
total: logs.length,
|
|
55
|
+
hasMore: false,
|
|
35
56
|
};
|
|
36
57
|
}
|
|
37
58
|
)
|
|
38
59
|
);
|
|
39
|
-
|
|
60
|
+
|
|
40
61
|
// Get Log Stream Handler
|
|
41
62
|
this.typedrouter.addTypedHandler(
|
|
42
63
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetLogStream>(
|
|
@@ -44,7 +65,7 @@ export class LogsHandler {
|
|
|
44
65
|
async (dataArg, toolsArg) => {
|
|
45
66
|
// Create a virtual stream for log streaming
|
|
46
67
|
const virtualStream = new plugins.typedrequest.VirtualStream<Uint8Array>();
|
|
47
|
-
|
|
68
|
+
|
|
48
69
|
// Set up log streaming
|
|
49
70
|
const streamLogs = this.setupLogStream(
|
|
50
71
|
virtualStream,
|
|
@@ -52,20 +73,21 @@ export class LogsHandler {
|
|
|
52
73
|
dataArg.filters?.category,
|
|
53
74
|
dataArg.follow
|
|
54
75
|
);
|
|
55
|
-
|
|
76
|
+
|
|
56
77
|
// Start streaming
|
|
57
78
|
streamLogs.start();
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
|
|
79
|
+
|
|
80
|
+
// Track the stop function so we can clean up on shutdown
|
|
81
|
+
this.activeStreamStops.add(streamLogs.stop);
|
|
82
|
+
|
|
61
83
|
return {
|
|
62
|
-
logStream: virtualStream as any,
|
|
84
|
+
logStream: virtualStream as any,
|
|
63
85
|
};
|
|
64
86
|
}
|
|
65
87
|
)
|
|
66
88
|
);
|
|
67
89
|
}
|
|
68
|
-
|
|
90
|
+
|
|
69
91
|
private static mapLogLevel(smartlogLevel: string): 'debug' | 'info' | 'warn' | 'error' {
|
|
70
92
|
switch (smartlogLevel) {
|
|
71
93
|
case 'silly':
|
|
@@ -165,18 +187,30 @@ export class LogsHandler {
|
|
|
165
187
|
|
|
166
188
|
return mapped;
|
|
167
189
|
}
|
|
168
|
-
|
|
190
|
+
|
|
169
191
|
/**
|
|
170
192
|
* Add a log destination to the base logger that pushes entries
|
|
171
193
|
* to all connected ops_dashboard TypedSocket clients.
|
|
194
|
+
*
|
|
195
|
+
* Uses a module-level singleton so the destination is added only once,
|
|
196
|
+
* even across OpsServer restart cycles. The destination reads
|
|
197
|
+
* `currentOpsServerRef` dynamically so it always uses the active server.
|
|
172
198
|
*/
|
|
173
199
|
private setupLogPushDestination(): void {
|
|
174
|
-
|
|
200
|
+
// Update the module-level reference so the existing destination uses the new server
|
|
201
|
+
currentOpsServerRef = this.opsServerRef;
|
|
202
|
+
|
|
203
|
+
if (logPushDestinationInstalled) {
|
|
204
|
+
return; // destination already registered — just updated the ref
|
|
205
|
+
}
|
|
206
|
+
logPushDestinationInstalled = true;
|
|
175
207
|
|
|
176
208
|
baseLogger.addLogDestination({
|
|
177
209
|
async handleLog(logPackage: any) {
|
|
178
|
-
|
|
179
|
-
|
|
210
|
+
const opsServer = currentOpsServerRef;
|
|
211
|
+
if (!opsServer) return;
|
|
212
|
+
|
|
213
|
+
const typedsocket = opsServer.server?.typedserver?.typedsocket;
|
|
180
214
|
if (!typedsocket) return;
|
|
181
215
|
|
|
182
216
|
let connections: any[];
|
|
@@ -220,8 +254,18 @@ export class LogsHandler {
|
|
|
220
254
|
stop: () => void;
|
|
221
255
|
} {
|
|
222
256
|
let intervalId: NodeJS.Timeout | null = null;
|
|
257
|
+
let stopped = false;
|
|
223
258
|
let logIndex = 0;
|
|
224
|
-
|
|
259
|
+
|
|
260
|
+
const stop = () => {
|
|
261
|
+
stopped = true;
|
|
262
|
+
if (intervalId) {
|
|
263
|
+
clearInterval(intervalId);
|
|
264
|
+
intervalId = null;
|
|
265
|
+
}
|
|
266
|
+
this.activeStreamStops.delete(stop);
|
|
267
|
+
};
|
|
268
|
+
|
|
225
269
|
const start = () => {
|
|
226
270
|
if (!follow) {
|
|
227
271
|
// Send existing logs and close
|
|
@@ -236,13 +280,19 @@ export class LogsHandler {
|
|
|
236
280
|
const encoder = new TextEncoder();
|
|
237
281
|
virtualStream.sendData(encoder.encode(logData));
|
|
238
282
|
});
|
|
239
|
-
// VirtualStream doesn't have end() method - it closes automatically
|
|
240
283
|
});
|
|
241
284
|
return;
|
|
242
285
|
}
|
|
243
|
-
|
|
286
|
+
|
|
244
287
|
// For follow mode, simulate real-time log streaming
|
|
245
288
|
intervalId = setInterval(async () => {
|
|
289
|
+
if (stopped) {
|
|
290
|
+
// Guard: clear interval if stop() was called between ticks
|
|
291
|
+
clearInterval(intervalId!);
|
|
292
|
+
intervalId = null;
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
246
296
|
const categories: Array<'smtp' | 'dns' | 'security' | 'system' | 'email'> = ['smtp', 'dns', 'security', 'system', 'email'];
|
|
247
297
|
const levels: Array<'debug' | 'info' | 'warn' | 'error'> = ['info', 'warn', 'error', 'debug'];
|
|
248
298
|
|
|
@@ -266,30 +316,21 @@ export class LogsHandler {
|
|
|
266
316
|
const logData = JSON.stringify(logEntry);
|
|
267
317
|
const encoder = new TextEncoder();
|
|
268
318
|
try {
|
|
269
|
-
|
|
319
|
+
// Use a timeout to detect hung streams (sendData can hang if the
|
|
320
|
+
// VirtualStream's keepAlive loop has ended)
|
|
321
|
+
await Promise.race([
|
|
322
|
+
virtualStream.sendData(encoder.encode(logData)),
|
|
323
|
+
new Promise<never>((_, reject) =>
|
|
324
|
+
setTimeout(() => reject(new Error('stream send timeout')), 10_000)
|
|
325
|
+
),
|
|
326
|
+
]);
|
|
270
327
|
} catch {
|
|
271
|
-
// Stream closed or
|
|
272
|
-
|
|
273
|
-
intervalId = null;
|
|
328
|
+
// Stream closed, errored, or timed out — clean up
|
|
329
|
+
stop();
|
|
274
330
|
}
|
|
275
|
-
}, 2000);
|
|
276
|
-
|
|
277
|
-
// TODO: Hook into actual logger events
|
|
278
|
-
// logger.on('log', (logEntry) => {
|
|
279
|
-
// if (matchesCriteria(logEntry, level, service)) {
|
|
280
|
-
// virtualStream.sendData(formatLogEntry(logEntry));
|
|
281
|
-
// }
|
|
282
|
-
// });
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
const stop = () => {
|
|
286
|
-
if (intervalId) {
|
|
287
|
-
clearInterval(intervalId);
|
|
288
|
-
intervalId = null;
|
|
289
|
-
}
|
|
290
|
-
// TODO: Unhook from logger events
|
|
331
|
+
}, 2000);
|
|
291
332
|
};
|
|
292
|
-
|
|
333
|
+
|
|
293
334
|
return { start, stop };
|
|
294
335
|
}
|
|
295
|
-
}
|
|
336
|
+
}
|
|
@@ -15,6 +15,7 @@ export class TunnelManager {
|
|
|
15
15
|
private manager: RemoteIngressManager;
|
|
16
16
|
private config: ITunnelManagerConfig;
|
|
17
17
|
private edgeStatuses: Map<string, IRemoteIngressStatus> = new Map();
|
|
18
|
+
private reconcileInterval: ReturnType<typeof setInterval> | null = null;
|
|
18
19
|
|
|
19
20
|
constructor(manager: RemoteIngressManager, config: ITunnelManagerConfig = {}) {
|
|
20
21
|
this.manager = manager;
|
|
@@ -65,16 +66,64 @@ export class TunnelManager {
|
|
|
65
66
|
|
|
66
67
|
// Send allowed edges to the hub
|
|
67
68
|
await this.syncAllowedEdges();
|
|
69
|
+
|
|
70
|
+
// Periodically reconcile with authoritative Rust hub status
|
|
71
|
+
this.reconcileInterval = setInterval(() => {
|
|
72
|
+
this.reconcile().catch(() => {});
|
|
73
|
+
}, 15_000);
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
/**
|
|
71
77
|
* Stop the tunnel hub.
|
|
72
78
|
*/
|
|
73
79
|
public async stop(): Promise<void> {
|
|
80
|
+
if (this.reconcileInterval) {
|
|
81
|
+
clearInterval(this.reconcileInterval);
|
|
82
|
+
this.reconcileInterval = null;
|
|
83
|
+
}
|
|
84
|
+
// Remove event listeners before stopping to prevent leaks
|
|
85
|
+
this.hub.removeAllListeners();
|
|
74
86
|
await this.hub.stop();
|
|
75
87
|
this.edgeStatuses.clear();
|
|
76
88
|
}
|
|
77
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Reconcile TS-side edge statuses with the authoritative Rust hub status.
|
|
92
|
+
* Overwrites event-derived activeTunnels with the real activeStreams count.
|
|
93
|
+
*/
|
|
94
|
+
private async reconcile(): Promise<void> {
|
|
95
|
+
const hubStatus = await this.hub.getStatus();
|
|
96
|
+
if (!hubStatus || !hubStatus.connectedEdges) return;
|
|
97
|
+
|
|
98
|
+
const rustEdgeIds = new Set<string>();
|
|
99
|
+
|
|
100
|
+
for (const rustEdge of hubStatus.connectedEdges) {
|
|
101
|
+
rustEdgeIds.add(rustEdge.edgeId);
|
|
102
|
+
const existing = this.edgeStatuses.get(rustEdge.edgeId);
|
|
103
|
+
if (existing) {
|
|
104
|
+
existing.activeTunnels = rustEdge.activeStreams;
|
|
105
|
+
existing.lastHeartbeat = Date.now();
|
|
106
|
+
} else {
|
|
107
|
+
// Missed edgeConnected event — add entry
|
|
108
|
+
this.edgeStatuses.set(rustEdge.edgeId, {
|
|
109
|
+
edgeId: rustEdge.edgeId,
|
|
110
|
+
connected: true,
|
|
111
|
+
publicIp: null,
|
|
112
|
+
activeTunnels: rustEdge.activeStreams,
|
|
113
|
+
lastHeartbeat: Date.now(),
|
|
114
|
+
connectedAt: rustEdge.connectedAt * 1000,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Remove entries for edges no longer connected in Rust (missed edgeDisconnected)
|
|
120
|
+
for (const edgeId of this.edgeStatuses.keys()) {
|
|
121
|
+
if (!rustEdgeIds.has(edgeId)) {
|
|
122
|
+
this.edgeStatuses.delete(edgeId);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
78
127
|
/**
|
|
79
128
|
* Sync allowed edges from the manager to the hub.
|
|
80
129
|
* Call this after creating/deleting/updating edges.
|