@matterbridge/thread 3.10.13-dev-20261005-49e0dbd → 3.10.13-dev-20261007-320f739

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.
@@ -0,0 +1,393 @@
1
+ import { Logger, LogLevel as MatterLogLevel } from '@matter/general';
2
+ import { hasAnyParameter } from '@matterbridge/utils/cli';
3
+ import { writeDiagnostic } from '@matterbridge/utils/diagnostic';
4
+ import { inspectError } from '@matterbridge/utils/error';
5
+ import { logModuleLoaded } from '@matterbridge/utils/loader';
6
+ import { isValidNumber, isValidString } from '@matterbridge/utils/validate';
7
+ import { fireAndForget, withTimeout } from '@matterbridge/utils/wait';
8
+ import { AnsiLogger, CYAN, debugStringify, nf } from 'node-ansi-logger';
9
+ import { WebSocket, WebSocketServer } from 'ws';
10
+ import { BroadcastServer } from './broadcastServer.js';
11
+ logModuleLoaded('BackendWsServer');
12
+ export class BackendWsServer {
13
+ debug;
14
+ verbose;
15
+ diagnostic;
16
+ log;
17
+ backend;
18
+ matterbridge;
19
+ server;
20
+ webSocketServer;
21
+ constructor(matterbridge, backend) {
22
+ this.debug = hasAnyParameter('debug', 'verbose', 'debug-backend', 'verbose-backend');
23
+ this.verbose = hasAnyParameter('verbose', 'verbose-backend');
24
+ this.diagnostic = hasAnyParameter('diagnostic', 'diagnostic-backend');
25
+ this.backend = backend;
26
+ this.matterbridge = matterbridge;
27
+ this.log = new AnsiLogger({
28
+ logName: 'BackendWsServer',
29
+ logNameColor: '\x1b[38;5;97m',
30
+ logTimestampFormat: 4,
31
+ logLevel: this.debug ? "debug" : "info",
32
+ });
33
+ this.server = new BroadcastServer('frontend', this.log);
34
+ this.server.on('broadcast_message', (msg) => fireAndForget(this.broadcastMsgHandler(msg), this.log, 'Broadcast message handler'));
35
+ }
36
+ destroy() {
37
+ this.server.close();
38
+ }
39
+ async broadcastMsgHandler(msg) {
40
+ if (this.server.isWorkerRequest(msg)) {
41
+ switch (msg.type) {
42
+ case 'get_log_level':
43
+ this.server.respond({ ...msg, result: { logLevel: this.log.logLevel } });
44
+ break;
45
+ case 'set_log_level':
46
+ this.log.logLevel = msg.params.logLevel;
47
+ this.server.respond({ ...msg, result: { logLevel: this.log.logLevel } });
48
+ break;
49
+ }
50
+ }
51
+ }
52
+ async start() {
53
+ this.log.debug(`Creating WebSocketServer...`);
54
+ this.webSocketServer = new WebSocketServer({ noServer: true });
55
+ this.backend.emit('websocket_server_listening', this.backend.secure ? 'wss' : 'ws');
56
+ this.webSocketServer.on('connection', (websocket, request) => {
57
+ const clientIp = request.socket.remoteAddress;
58
+ this.log.info(`WebSocketServer client ${CYAN}${clientIp}${nf} connected to Matterbridge`);
59
+ let callbackLogLevel = "notice";
60
+ if (this.log.logLevel === "info" || Logger.level === MatterLogLevel.INFO)
61
+ callbackLogLevel = "info";
62
+ if (this.log.logLevel === "debug" || Logger.level === MatterLogLevel.DEBUG)
63
+ callbackLogLevel = "debug";
64
+ AnsiLogger.setGlobalCallback(this.wssSendLogMessage.bind(this), callbackLogLevel);
65
+ this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
66
+ websocket.on('message', (data) => {
67
+ this.log.debug(`WebSocket client ${CYAN}${clientIp}${nf} sent a message`);
68
+ fireAndForget(this.wsMessageHandler(websocket, data), this.log, `Error handling message from WebSocket client ${clientIp}`);
69
+ });
70
+ websocket.on('ping', () => {
71
+ this.log.debug(`WebSocket client ${CYAN}${clientIp}${nf} ping received`);
72
+ websocket.pong();
73
+ });
74
+ websocket.on('pong', () => {
75
+ this.log.debug(`WebSocket client ${CYAN}${clientIp}${nf} pong received`);
76
+ });
77
+ websocket.on('close', (code, reason) => {
78
+ this.log.info(`WebSocket client ${CYAN}${clientIp}${nf} disconnected code ${code} reason ${reason.toString()}`);
79
+ if (this.webSocketServer?.clients.size === 0) {
80
+ AnsiLogger.setGlobalCallback(undefined);
81
+ this.log.debug('All WebSocket clients disconnected. WebSocketServer logger global callback removed');
82
+ setTimeout(() => {
83
+ if (this.webSocketServer?.clients.size === 0) {
84
+ this.log.debug('All WebSocket clients disconnected. Auth clients list cleared');
85
+ this.backend.authClients.clear();
86
+ }
87
+ }, 1000).unref();
88
+ }
89
+ else {
90
+ this.log.debug(`WebSocketServer still has ${this.webSocketServer?.clients.size} connected clients`);
91
+ }
92
+ });
93
+ websocket.on('error', (error) => {
94
+ inspectError(this.log, `WebSocket client error`, error);
95
+ });
96
+ });
97
+ this.webSocketServer.on('close', () => {
98
+ this.log.debug(`WebSocketServer closed`);
99
+ });
100
+ this.webSocketServer.on('error', (ws, error) => {
101
+ inspectError(this.log, `WebSocketServer error`, error);
102
+ });
103
+ return this.webSocketServer;
104
+ }
105
+ async stop() {
106
+ if (this.webSocketServer) {
107
+ this.log.debug('Closing WebSocket server...');
108
+ this.webSocketServer.clients.forEach((client) => {
109
+ if (client.readyState === WebSocket.OPEN) {
110
+ client.close();
111
+ }
112
+ });
113
+ await withTimeout(new Promise((resolve) => {
114
+ this.webSocketServer?.close((error) => {
115
+ if (error) {
116
+ inspectError(this.log, `Error closing WebSocket server`, error);
117
+ }
118
+ else {
119
+ this.log.debug('WebSocket server closed successfully');
120
+ this.backend.emit('websocket_server_stopped');
121
+ }
122
+ resolve();
123
+ });
124
+ }), 10000, false);
125
+ this.webSocketServer.removeAllListeners();
126
+ this.webSocketServer = undefined;
127
+ }
128
+ else {
129
+ this.log.debug('WebSocket server is not running');
130
+ }
131
+ return this.webSocketServer;
132
+ }
133
+ hasActiveClients() {
134
+ return this.webSocketServer !== undefined && this.webSocketServer.clients.size > 0;
135
+ }
136
+ async wsMessageHandler(client, rawData) {
137
+ let data;
138
+ const sendResponse = (data) => {
139
+ if (client.readyState === client.OPEN) {
140
+ if ('response' in data) {
141
+ const { response, ...rest } = data;
142
+ this.log.debug(`Sending api response message: ${debugStringify(rest)}`);
143
+ }
144
+ else {
145
+ this.log.debug(`Sending api error message: ${debugStringify(data)}`);
146
+ }
147
+ const bigintReplacer = (_, v) => (typeof v === 'bigint' ? `${v}n` : v);
148
+ client.send(JSON.stringify(data, bigintReplacer));
149
+ }
150
+ else {
151
+ this.log.error('Cannot send api response, client not connected');
152
+ }
153
+ };
154
+ try {
155
+ const raw = Array.isArray(rawData) ? Buffer.concat(rawData) : rawData instanceof ArrayBuffer ? Buffer.from(rawData) : rawData;
156
+ data = JSON.parse(raw.toString());
157
+ if (!isValidNumber(data.id) || !isValidString(data.dst) || !isValidString(data.src) || !isValidString(data.method) || data.dst !== 'Matterbridge') {
158
+ this.log.error(`Invalid message from websocket client: ${debugStringify(data)}`);
159
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Invalid message' });
160
+ return;
161
+ }
162
+ this.log.debug(`Received message from websocket client: ${debugStringify(data)}`);
163
+ if (data.method === 'ping') {
164
+ sendResponse({ id: data.id, method: 'pong', src: 'Matterbridge', dst: data.src, success: true, response: 'pong' });
165
+ }
166
+ else if (data.method === '/api/restart') {
167
+ this.wssSendSnackbarMessage('Restarting matterbridge...', 0);
168
+ await this.server.fetch({ type: 'matterbridge_restart', src: 'frontend', dst: 'matterbridge', params: undefined });
169
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
170
+ }
171
+ else if (data.method === '/api/shutdown') {
172
+ this.wssSendSnackbarMessage('Shutting down matterbridge...', 0);
173
+ await this.server.fetch({ type: 'matterbridge_shutdown', src: 'frontend', dst: 'matterbridge', params: undefined });
174
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
175
+ }
176
+ else if (data.method === '/api/settings') {
177
+ const start = performance.now();
178
+ const settings = await this.backend.getApiSettings();
179
+ if (this.diagnostic)
180
+ writeDiagnostic(`Frontend:${data.sender}`, `getApiSettings() took ${(performance.now() - start).toFixed(2)} ms`);
181
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: settings });
182
+ }
183
+ else if (data.method === '/api/plugins') {
184
+ const start = performance.now();
185
+ const plugins = await this.backend.getApiPlugins();
186
+ if (this.diagnostic)
187
+ writeDiagnostic(`Frontend:${data.sender}`, `getApiPlugins() took ${(performance.now() - start).toFixed(2)} ms`);
188
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: plugins });
189
+ }
190
+ else if (data.method === '/api/devices') {
191
+ const start = performance.now();
192
+ const devices = await this.backend.getApiDevices(isValidString(data.params.pluginName) ? data.params.pluginName : undefined);
193
+ if (this.diagnostic)
194
+ writeDiagnostic(`Frontend:${data.sender}`, `getApiDevices() took ${(performance.now() - start).toFixed(2)} ms`);
195
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: devices });
196
+ }
197
+ else if (data.method === '/api/clusters') {
198
+ if (!isValidString(data.params.plugin, 10)) {
199
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter plugin in /api/clusters' });
200
+ return;
201
+ }
202
+ if (!isValidNumber(data.params.endpoint, 1)) {
203
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter endpoint in /api/clusters' });
204
+ return;
205
+ }
206
+ const start = performance.now();
207
+ const clusters = await this.backend.getApiCluster(data.params.plugin, data.params.endpoint, data.params.serialNumber, data.params.uniqueId);
208
+ if (this.diagnostic)
209
+ writeDiagnostic(`Frontend:${data.sender}`, `getApiCluster() took ${(performance.now() - start).toFixed(2)} ms`);
210
+ if (clusters) {
211
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: clusters });
212
+ }
213
+ else {
214
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Endpoint not found in /api/clusters' });
215
+ }
216
+ }
217
+ else if (data.method === '/api/matter') {
218
+ if (!isValidString(data.params.id)) {
219
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter id in /api/matter' });
220
+ return;
221
+ }
222
+ const matter = await this.backend.getApiMatter(data.params.id);
223
+ if (!matter) {
224
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: `Unknown server node id ${data.params.id} in /api/matter` });
225
+ return;
226
+ }
227
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: matter });
228
+ this.wssSendRefreshRequired('matter', { matter });
229
+ }
230
+ }
231
+ catch (error) {
232
+ inspectError(this.log, `Error parsing message from websocket client`, error);
233
+ return;
234
+ }
235
+ }
236
+ wssBroadcastMessage(msg) {
237
+ if (!this.hasActiveClients())
238
+ return;
239
+ try {
240
+ const stringifiedMsg = JSON.stringify(msg);
241
+ this.webSocketServer?.clients.forEach((client) => {
242
+ if (client.readyState === client.OPEN) {
243
+ client.send(stringifiedMsg);
244
+ }
245
+ });
246
+ }
247
+ catch (error) {
248
+ inspectError(this.log, `Error broadcasting message to websocket clients`, error);
249
+ }
250
+ }
251
+ wssSendLogMessage(level, time, name, message) {
252
+ if (!this.hasActiveClients())
253
+ return;
254
+ if (!level || !time || !name || !message)
255
+ return;
256
+ message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
257
+ message = message.replace(/^\*+/, '');
258
+ message = message.replace(/[\t\n]/g, '');
259
+ message = message.replace(/[\x00-\x1F\x7F]/g, '');
260
+ message = message.replace(/\\"/g, '"');
261
+ const maxContinuousLength = 100;
262
+ const keepStartLength = 20;
263
+ const keepEndLength = 20;
264
+ if (level !== 'spawn') {
265
+ message = message
266
+ .split(' ')
267
+ .map((word) => {
268
+ if (word.length > maxContinuousLength) {
269
+ return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
270
+ }
271
+ return word;
272
+ })
273
+ .join(' ');
274
+ }
275
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'log', success: true, response: { level, time, name, message } });
276
+ }
277
+ wssSendRefreshRequired(changed, params) {
278
+ if (!this.hasActiveClients())
279
+ return;
280
+ if (this.verbose)
281
+ this.log.debug('Sending a refresh required message to all connected clients');
282
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', success: true, response: { changed, lock: params?.lock, ...params } });
283
+ }
284
+ wssSendRestartRequired(snackbar = true, fixed = false) {
285
+ if (!this.hasActiveClients())
286
+ return;
287
+ if (this.verbose)
288
+ this.log.debug('Sending a restart required message to all connected clients');
289
+ if (snackbar)
290
+ this.wssSendSnackbarMessage(`Restart required`, 0);
291
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', success: true, response: { fixed } });
292
+ }
293
+ wssSendRestartNotRequired(snackbar = true) {
294
+ if (!this.hasActiveClients())
295
+ return;
296
+ if (this.verbose)
297
+ this.log.debug('Sending a restart not required message to all connected clients');
298
+ if (snackbar)
299
+ this.wssSendCloseSnackbarMessage(`Restart required`);
300
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_not_required', success: true });
301
+ }
302
+ wssSendUpdateRequired(version, devVersion = false) {
303
+ if (!this.hasActiveClients())
304
+ return;
305
+ if (this.verbose)
306
+ this.log.debug('Sending a matterbridge version update required message to all connected clients');
307
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', success: true, response: { version, devVersion } });
308
+ }
309
+ wssSendPluginUpdateRequired(plugin, version, devVersion = false) {
310
+ if (!this.hasActiveClients())
311
+ return;
312
+ if (this.verbose)
313
+ this.log.debug('Sending a plugin version update required message to all connected clients');
314
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'plugin_update_required', success: true, response: { plugin, version, devVersion } });
315
+ }
316
+ wssSendPluginStatusUpdate(plugin, status) {
317
+ if (!this.hasActiveClients())
318
+ return;
319
+ if (this.verbose)
320
+ this.log.debug('Sending a plugin status update message to all connected clients');
321
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'plugin_status_update', success: true, response: { plugin, status } });
322
+ }
323
+ wssSendMatterbridgeStatusUpdate(status) {
324
+ if (!this.hasActiveClients())
325
+ return;
326
+ if (this.verbose)
327
+ this.log.debug('Sending a matterbridge status update message to all connected clients');
328
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'matterbridge_status_update', success: true, response: { status } });
329
+ }
330
+ wssSendCpuUpdate(cpuUsage, processCpuUsage) {
331
+ if (!this.hasActiveClients())
332
+ return;
333
+ if (this.verbose)
334
+ this.log.debug('Sending a cpu update message to all connected clients');
335
+ this.wssBroadcastMessage({
336
+ id: 0,
337
+ src: 'Matterbridge',
338
+ dst: 'Frontend',
339
+ method: 'cpu_update',
340
+ success: true,
341
+ response: { cpuUsage: Math.round(cpuUsage * 100) / 100, processCpuUsage: Math.round(processCpuUsage * 100) / 100 },
342
+ });
343
+ }
344
+ wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
345
+ if (!this.hasActiveClients())
346
+ return;
347
+ if (this.verbose)
348
+ this.log.debug('Sending a memory update message to all connected clients');
349
+ this.wssBroadcastMessage({
350
+ id: 0,
351
+ src: 'Matterbridge',
352
+ dst: 'Frontend',
353
+ method: 'memory_update',
354
+ success: true,
355
+ response: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers },
356
+ });
357
+ }
358
+ wssSendUptimeUpdate(systemUptime, processUptime) {
359
+ if (!this.hasActiveClients())
360
+ return;
361
+ if (this.verbose)
362
+ this.log.debug('Sending a uptime update message to all connected clients');
363
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', success: true, response: { systemUptime, processUptime } });
364
+ }
365
+ wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
366
+ if (!this.hasActiveClients())
367
+ return;
368
+ if (this.verbose)
369
+ this.log.debug('Sending a snackbar message to all connected clients');
370
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'snackbar', success: true, response: { message, timeout, severity } });
371
+ }
372
+ wssSendCloseSnackbarMessage(message) {
373
+ if (!this.hasActiveClients())
374
+ return;
375
+ if (this.verbose)
376
+ this.log.debug('Sending a close snackbar message to all connected clients');
377
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'close_snackbar', success: true, response: { message } });
378
+ }
379
+ wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, number, id, cluster, attribute, value) {
380
+ if (!this.hasActiveClients())
381
+ return;
382
+ if (this.verbose)
383
+ this.log.debug('Sending an attribute update message to all connected clients');
384
+ this.wssBroadcastMessage({
385
+ id: 0,
386
+ src: 'Matterbridge',
387
+ dst: 'Frontend',
388
+ method: 'state_update',
389
+ success: true,
390
+ response: { plugin, serialNumber, uniqueId, number, id, cluster, attribute, value },
391
+ });
392
+ }
393
+ }
@@ -0,0 +1,3 @@
1
+ import { WorkerWrapper } from './workerWrapper.js';
2
+ declare const _default: WorkerWrapper;
3
+ export default _default;
@@ -0,0 +1,24 @@
1
+ import { inspectError } from '@matterbridge/utils/error';
2
+ import { logModuleLoaded } from '@matterbridge/utils/loader';
3
+ import { WorkerWrapper } from './workerWrapper.js';
4
+ logModuleLoaded('Backend', '\u001B[35m');
5
+ export default new WorkerWrapper('Backend', async (worker) => {
6
+ worker.logger("info", 'Creating backend...');
7
+ try {
8
+ worker.logger("debug", 'Fetching shared Matterbridge state...');
9
+ const shared = (await worker.server.fetch({ type: 'matterbridge_shared', src: 'matterbridge', dst: 'matterbridge' }, 1000)).result.data;
10
+ worker.logger("debug", 'Shared Matterbridge state fetched; importing Backend...');
11
+ const { Backend } = await import('./backend.js');
12
+ worker.logger("debug", 'Backend imported; creating instance...');
13
+ const backend = new Backend(shared);
14
+ worker.logger("debug", 'Starting Backend...');
15
+ await backend.start();
16
+ worker.logger("debug", 'Backend started');
17
+ return true;
18
+ }
19
+ catch (error) {
20
+ const errorMessage = inspectError(worker.log, 'Failed to create backend', error);
21
+ worker.logger("error", errorMessage);
22
+ return false;
23
+ }
24
+ });
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath, pathToFileURL } from 'node:url';
4
4
  import { Worker } from 'node:worker_threads';
5
- import { hasParameter } from '@matterbridge/utils/cli';
5
+ import { hasAnyParameter, hasParameter } from '@matterbridge/utils/cli';
6
6
  import { getErrorMessage } from '@matterbridge/utils/error';
7
7
  import { logModuleLoaded } from '@matterbridge/utils/loader';
8
8
  import { fireAndForget } from '@matterbridge/utils/wait';
@@ -26,12 +26,13 @@ export class ThreadsManager {
26
26
  { name: 'SpawnCommand', path: 'workerSpawnCommand.js', type: 'worker' },
27
27
  { name: 'ArchiveCommand', path: 'workerArchiveCommand.js', type: 'worker' },
28
28
  { name: 'DockerVersion', path: 'workerDockerVersion.js', type: 'worker' },
29
+ { name: 'Backend', path: 'threadBackend.js', type: 'thread' },
29
30
  ];
30
31
  terminateWorkers = new Set();
31
32
  constructor(intervalMs = 60_000) {
32
- this.debug = hasParameter('debug') || hasParameter('verbose') || hasParameter('debug-threads') || hasParameter('verbose-threads');
33
- this.verbose = hasParameter('verbose') || hasParameter('verbose-threads');
34
- this.tracker = hasParameter('tracker') || hasParameter('tracker-threads');
33
+ this.debug = hasAnyParameter('debug', 'verbose', 'debug-threads', 'verbose-threads');
34
+ this.verbose = hasAnyParameter('verbose', 'verbose-threads');
35
+ this.tracker = hasAnyParameter('tracker', 'tracker-threads');
35
36
  this.log = new AnsiLogger({
36
37
  logName: 'ThreadsManager',
37
38
  logNameColor: MAGENTA,
@@ -103,7 +104,7 @@ export class ThreadsManager {
103
104
  }
104
105
  intervalHandler() {
105
106
  for (const thread of this.threads) {
106
- this.log.debug(`Thread ${thread.name} running: ${thread.worker ? 'yes' : 'no'}, lastSeen: ${thread.lastSeen ? new Date(thread.lastSeen).toISOString() : 'never'}, runs: ${thread.runCount ?? 0}, errors: ${thread.errorCount ?? 0}`);
107
+ this.log.debug(`Thread ${thread.name} running: ${thread.worker ? 'yes' : 'no'}, threadId: ${thread.worker?.threadId ?? 'none'}, lastSeen: ${thread.lastSeen ? new Date(thread.lastSeen).toISOString() : 'never'}, runs: ${thread.runCount ?? 0}, errors: ${thread.errorCount ?? 0}`);
107
108
  }
108
109
  this.terminateExitedWorkers();
109
110
  for (const thread of this.threads) {
@@ -132,7 +133,7 @@ export class ThreadsManager {
132
133
  threadInfo.lastStarted = undefined;
133
134
  threadInfo.lastStopped = undefined;
134
135
  threadInfo.lastDuration = undefined;
135
- threadInfo.worker = this.createESMWorker(threadInfo.name, path, { ...workerData, debug: this.debug, verbose: this.verbose, logLevel: this.log.logLevel }, argv, env, execArgv, pipedOutput);
136
+ threadInfo.worker = this.createESMWorker(threadInfo.name, path, { ...workerData, type: threadInfo.type, debug: this.debug, verbose: this.verbose, logLevel: this.log.logLevel }, argv, env, execArgv, pipedOutput);
136
137
  const worker = threadInfo.worker;
137
138
  worker.once('online', () => {
138
139
  const now = Date.now();
@@ -201,9 +202,14 @@ export class ThreadsManager {
201
202
  let success = false;
202
203
  const workerWrapper = (await import(this.resolvePath(threadInfo.path))).default;
203
204
  if (workerWrapper && typeof workerWrapper === 'object' && workerWrapper.name === name && workerWrapper.callback && typeof workerWrapper.callback === 'function') {
204
- workerWrapper.workerData = workerData;
205
- success = await workerWrapper.callback(workerWrapper);
206
- workerWrapper.destroy(success);
205
+ workerWrapper.workerData = workerData ? { ...workerData, type: threadInfo.type } : null;
206
+ try {
207
+ success = await workerWrapper.callback(workerWrapper);
208
+ }
209
+ finally {
210
+ if (!success || threadInfo.type !== 'thread')
211
+ workerWrapper.destroy(success);
212
+ }
207
213
  }
208
214
  this.log.debug(`Finished running thread ${threadInfo.name} in the main thread.`);
209
215
  return success;
@@ -226,7 +232,7 @@ export class ThreadsManager {
226
232
  createESMWorker(name, relativePath, workerData, argv, env, execArgv, pipedOutput = false) {
227
233
  const fileURL = pathToFileURL(path.resolve(relativePath));
228
234
  const options = {
229
- workerData: { ...workerData, threadName: name, debug: this.debug, verbose: this.verbose, logLevel: this.log.logLevel, tracker: this.tracker },
235
+ workerData: { ...workerData, threadName: name, debug: this.debug, verbose: this.verbose, tracker: this.tracker, logLevel: this.log.logLevel },
230
236
  name,
231
237
  argv: argv ?? process.argv.slice(2),
232
238
  env: env ?? process.env,
@@ -1,6 +1,6 @@
1
1
  import { isMainThread, parentPort, threadId, workerData } from 'node:worker_threads';
2
2
  import { getErrorMessage, inspectError } from '@matterbridge/utils';
3
- import { hasParameter } from '@matterbridge/utils/cli';
3
+ import { hasAnyParameter } from '@matterbridge/utils/cli';
4
4
  import { formatBytes } from '@matterbridge/utils/format';
5
5
  import { logModuleLoaded } from '@matterbridge/utils/loader';
6
6
  import { AnsiLogger, debugStringify, MAGENTA } from 'node-ansi-logger';
@@ -10,9 +10,9 @@ logModuleLoaded('WorkerWrapper');
10
10
  export class WorkerWrapper {
11
11
  name;
12
12
  callback;
13
- debug = hasParameter('debug') || hasParameter('verbose') || hasParameter('debug-threads') || hasParameter('verbose-threads');
14
- verbose = hasParameter('verbose') || hasParameter('verbose-threads');
15
- useTracker = hasParameter('tracker') || hasParameter('tracker-threads');
13
+ debug = hasAnyParameter('debug', 'verbose', 'debug-threads', 'verbose-threads');
14
+ verbose = hasAnyParameter('verbose', 'verbose-threads');
15
+ useTracker = hasAnyParameter('tracker', 'tracker-threads');
16
16
  log;
17
17
  server;
18
18
  workerData = workerData;
@@ -32,25 +32,24 @@ export class WorkerWrapper {
32
32
  this.verbose = this.workerData.verbose ?? this.verbose;
33
33
  this.useTracker = this.workerData.tracker ?? this.useTracker;
34
34
  }
35
+ this.log = new AnsiLogger({
36
+ logName: this.name,
37
+ logNameColor: MAGENTA,
38
+ logTimestampFormat: 4,
39
+ logLevel: this.debug ? "debug" : "info",
40
+ });
35
41
  if (this.useTracker) {
36
42
  void import('@matterbridge/utils/tracker')
37
43
  .then(({ Tracker }) => {
38
- this.tracker = new Tracker(`Thread${this.name}`, this.debug, this.verbose);
44
+ this.tracker = new Tracker(`Thread${this.name}`, this.debug || this.useTracker, this.verbose, this.useTracker);
39
45
  this.tracker.start();
40
46
  return;
41
47
  })
42
48
  .catch((err) => {
43
- if (this.debug)
44
- console.error(`WorkerWrapper ${this.name}: failed to load Tracker ${getErrorMessage(err)}`);
49
+ this.safeParentLog("error", `WorkerWrapper ${this.name}: failed to load Tracker ${getErrorMessage(err)}`);
45
50
  return;
46
51
  });
47
52
  }
48
- this.log = new AnsiLogger({
49
- logName: this.name,
50
- logNameColor: MAGENTA,
51
- logTimestampFormat: 4,
52
- logLevel: this.debug ? "debug" : "info",
53
- });
54
53
  this.server = new BroadcastServer('matterbridge', this.log);
55
54
  if (!isMainThread) {
56
55
  process.on('unhandledRejection', this.boundUnhandledRejectionHandler);
@@ -96,7 +95,8 @@ export class WorkerWrapper {
96
95
  inspectError(this.log, `Worker ${this.name} callback failed`, err);
97
96
  }
98
97
  finally {
99
- this.destroy(success);
98
+ if (!success || this.workerData?.type !== 'thread')
99
+ this.destroy(success);
100
100
  }
101
101
  })();
102
102
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matterbridge/thread",
3
- "version": "3.10.13-dev-20261005-49e0dbd",
3
+ "version": "3.10.13-dev-20261007-320f739",
4
4
  "description": "Matterbridge thread library",
5
5
  "author": "https://github.com/Luligu",
6
6
  "homepage": "https://matterbridge.io/",
@@ -75,9 +75,16 @@
75
75
  "skipTsconfig": true
76
76
  },
77
77
  "dependencies": {
78
- "@matterbridge/types": "3.10.13-dev-20261005-49e0dbd",
79
- "@matterbridge/utils": "3.10.13-dev-20261005-49e0dbd",
78
+ "@matter/main": "0.17.9",
79
+ "@matterbridge/types": "3.10.13-dev-20261007-320f739",
80
+ "@matterbridge/utils": "3.10.13-dev-20261007-320f739",
80
81
  "@zip.js/zip.js": "2.22.0",
81
- "node-ansi-logger": "3.3.1"
82
+ "escape-html": "1.0.3",
83
+ "express": "5.2.1",
84
+ "express-rate-limit": "8.7.0",
85
+ "multer": "2.4.0",
86
+ "node-ansi-logger": "3.3.1",
87
+ "node-persist-manager": "2.1.1",
88
+ "ws": "8.22.0"
82
89
  }
83
90
  }