@matterbridge/thread 3.6.0 → 3.6.1-dev-20260309-6341dee

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.
@@ -2,7 +2,8 @@ if (process.argv.includes('--loader') || process.argv.includes('-loader'))
2
2
  console.log('\u001B[32mBroadcastServer loaded.\u001B[40;0m');
3
3
  import { EventEmitter } from 'node:events';
4
4
  import { BroadcastChannel } from 'node:worker_threads';
5
- import { hasParameter, logError } from '@matterbridge/utils';
5
+ import { hasParameter } from '@matterbridge/utils/cli';
6
+ import { logError } from '@matterbridge/utils/error';
6
7
  import { CYAN, db, debugStringify, er } from 'node-ansi-logger';
7
8
  export class BroadcastServer extends EventEmitter {
8
9
  name;
@@ -1,5 +1,5 @@
1
1
  import { plg } from '@matterbridge/types';
2
- import { isValidString } from '@matterbridge/utils';
2
+ import { isValidString } from '@matterbridge/utils/validate';
3
3
  import { AnsiLogger, db, debugStringify, nt, wr } from 'node-ansi-logger';
4
4
  import { BroadcastServer } from './broadcastServer.js';
5
5
  export async function checkUpdates(matterbridge) {
@@ -24,7 +24,7 @@ export async function checkUpdates(matterbridge) {
24
24
  server.close();
25
25
  }
26
26
  export async function checkUpdatesAndLog(matterbridge, log, server) {
27
- const { getGitHubUpdate } = await import('@matterbridge/utils');
27
+ const { getGitHubUpdate } = await import('@matterbridge/utils/github-version');
28
28
  const branch = matterbridge.matterbridgeVersion.includes('-dev-') ? 'dev' : 'main';
29
29
  try {
30
30
  const updateJson = await getGitHubUpdate(branch, 'update.json', 5_000);
@@ -50,7 +50,7 @@ export async function checkUpdatesAndLog(matterbridge, log, server) {
50
50
  }
51
51
  }
52
52
  export async function getMatterbridgeLatestVersion(matterbridge, log, server) {
53
- const { getNpmPackageVersion } = await import('@matterbridge/utils');
53
+ const { getNpmPackageVersion } = await import('@matterbridge/utils/npm-version');
54
54
  try {
55
55
  const version = await getNpmPackageVersion('matterbridge');
56
56
  server.request({ type: 'matterbridge_latest_version', src: server.name, dst: 'matterbridge', params: { version } });
@@ -75,7 +75,7 @@ export async function getMatterbridgeLatestVersion(matterbridge, log, server) {
75
75
  }
76
76
  }
77
77
  export async function getMatterbridgeDevVersion(matterbridge, log, server) {
78
- const { getNpmPackageVersion } = await import('@matterbridge/utils');
78
+ const { getNpmPackageVersion } = await import('@matterbridge/utils/npm-version');
79
79
  try {
80
80
  const version = await getNpmPackageVersion('matterbridge', 'dev');
81
81
  server.request({ type: 'matterbridge_dev_version', src: server.name, dst: 'matterbridge', params: { version } });
@@ -100,7 +100,7 @@ export async function getMatterbridgeDevVersion(matterbridge, log, server) {
100
100
  }
101
101
  }
102
102
  export async function getPluginLatestVersion(log, server, plugin) {
103
- const { getNpmPackageVersion } = await import('@matterbridge/utils');
103
+ const { getNpmPackageVersion } = await import('@matterbridge/utils/npm-version');
104
104
  try {
105
105
  const version = await getNpmPackageVersion(plugin.name);
106
106
  plugin.latestVersion = version;
@@ -119,7 +119,7 @@ export async function getPluginLatestVersion(log, server, plugin) {
119
119
  }
120
120
  }
121
121
  export async function getPluginDevVersion(log, server, plugin) {
122
- const { getNpmPackageVersion } = await import('@matterbridge/utils');
122
+ const { getNpmPackageVersion } = await import('@matterbridge/utils/npm-version');
123
123
  try {
124
124
  const version = await getNpmPackageVersion(plugin.name, 'dev');
125
125
  plugin.devVersion = version;
@@ -1,4 +1,4 @@
1
- import { isValidString } from '@matterbridge/utils';
1
+ import { isValidString } from '@matterbridge/utils/validate';
2
2
  async function httpsGetJson(url, headers, timeoutMs) {
3
3
  const https = await import('node:https');
4
4
  return new Promise((resolve, reject) => {
@@ -6,7 +6,7 @@ async function httpsGetJson(url, headers, timeoutMs) {
6
6
  const timeoutId = setTimeout(() => {
7
7
  controller.abort();
8
8
  reject(new Error(`Request timed out after ${timeoutMs / 1000} seconds`));
9
- }, timeoutMs);
9
+ }, timeoutMs).unref();
10
10
  const req = https.get(url, { headers, signal: controller.signal }, (res) => {
11
11
  let data = '';
12
12
  const statusCode = res.statusCode ?? 0;
package/dist/export.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export * from './broadcastServer.js';
2
- export * from './worker.js';
2
+ export * from './threadsManager.js';
3
3
  export declare function systemCheck(): Promise<void>;
package/dist/export.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export * from './broadcastServer.js';
2
- export * from './worker.js';
2
+ export * from './threadsManager.js';
3
3
  export async function systemCheck() {
4
4
  await import('./workerSystemCheck.js');
5
5
  }
@@ -0,0 +1 @@
1
+ export declare function spawnCommand(command: string, args: string[], packageCommand?: 'install' | 'uninstall', packageName?: string): Promise<boolean>;
@@ -0,0 +1,96 @@
1
+ import { hasParameter } from '@matterbridge/utils/cli';
2
+ import { AnsiLogger } from 'node-ansi-logger';
3
+ import { BroadcastServer } from './broadcastServer.js';
4
+ export async function spawnCommand(command, args, packageCommand, packageName) {
5
+ const { spawn } = await import('node:child_process');
6
+ const debug = hasParameter('debug') || hasParameter('verbose') || hasParameter('debug-spawn') || hasParameter('verbose-spawn');
7
+ const verbose = hasParameter('verbose') || hasParameter('verbose-spawn');
8
+ const log = new AnsiLogger({ logName: 'Spawn', logTimestampFormat: 4, logLevel: debug ? "debug" : "info" });
9
+ const server = new BroadcastServer('spawn', log);
10
+ const sendLog = (name, message) => {
11
+ try {
12
+ server.request({ type: 'frontend_logmessage', src: 'spawn', dst: 'frontend', params: { level: 'spawn', time: log.now(), name, message } });
13
+ }
14
+ catch (err) {
15
+ log.debug(`Failed to send log message to frontend: ${err instanceof Error ? err.message : String(err)}`);
16
+ }
17
+ };
18
+ if (verbose)
19
+ log.debug(`Spawning command: ${command} with ${args.join(' ')} ${packageCommand} ${packageName}`);
20
+ const cmdLine = command + ' ' + args.join(' ');
21
+ if (process.platform === 'win32' && command === 'npm') {
22
+ const argstring = 'npm ' + args.join(' ');
23
+ args.splice(0, args.length, '/c', argstring);
24
+ command = 'cmd.exe';
25
+ }
26
+ if (hasParameter('sudo') ||
27
+ (process.platform !== 'win32' && command === 'npm' && !hasParameter('docker') && !hasParameter('nosudo') && !process.env.PATH?.includes('/.nvm/versions/node/'))) {
28
+ args.unshift(command);
29
+ command = 'sudo';
30
+ }
31
+ log.debug(`Spawn command ${command} with ${args.join(' ')}`);
32
+ const success = await new Promise((resolve) => {
33
+ if (packageCommand === 'install')
34
+ sendLog('Matterbridge:spawn-init', `Installing ${packageName}`);
35
+ else if (packageCommand === 'uninstall')
36
+ sendLog('Matterbridge:spawn-init', `Uninstalling ${packageName}`);
37
+ const childProcess = spawn(command, args, {
38
+ stdio: ['inherit', 'pipe', 'pipe'],
39
+ });
40
+ childProcess.on('error', (err) => {
41
+ log.error(`Failed to start child process "${cmdLine}": ${err.message}`);
42
+ sendLog('Matterbridge:spawn-exit-error', 'Spawn process error');
43
+ resolve(false);
44
+ });
45
+ childProcess.on('close', (code, signal) => {
46
+ if (code === 0) {
47
+ log.debug(`Child process "${cmdLine}" closed with code ${code} and signal ${signal}`);
48
+ sendLog('Matterbridge:spawn-exit-success', 'Child process closed');
49
+ resolve(true);
50
+ }
51
+ else {
52
+ log.error(`Child process "${cmdLine}" closed with code ${code} and signal ${signal}`);
53
+ sendLog('Matterbridge:spawn-exit-error', 'Child process closed');
54
+ resolve(false);
55
+ }
56
+ });
57
+ childProcess.on('exit', (code, signal) => {
58
+ if (code === 0) {
59
+ log.debug(`Child process "${cmdLine}" exited with code ${code} and signal ${signal}`);
60
+ sendLog('Matterbridge:spawn-exit-success', 'Child process exited');
61
+ resolve(true);
62
+ }
63
+ else {
64
+ log.error(`Child process "${cmdLine}" exited with code ${code} and signal ${signal}`);
65
+ sendLog('Matterbridge:spawn-exit-error', 'Child process exited');
66
+ resolve(false);
67
+ }
68
+ });
69
+ childProcess.on('disconnect', () => {
70
+ log.debug(`Child process "${cmdLine}" has been disconnected from the parent`);
71
+ resolve(true);
72
+ });
73
+ if (childProcess.stdout) {
74
+ childProcess.stdout.on('data', (data) => {
75
+ const message = data.toString().trim();
76
+ const lines = message.split('\n');
77
+ for (const line of lines) {
78
+ log.debug(`Spawn output (stdout): ${line}`);
79
+ sendLog('Matterbridge:spawn', line);
80
+ }
81
+ });
82
+ }
83
+ if (childProcess.stderr) {
84
+ childProcess.stderr.on('data', (data) => {
85
+ const message = data.toString().trim();
86
+ const lines = message.split('\n');
87
+ for (const line of lines) {
88
+ log.debug(`Spawn verbose (stderr): ${line}`);
89
+ sendLog('Matterbridge:spawn', line);
90
+ }
91
+ });
92
+ }
93
+ });
94
+ server.close();
95
+ return success;
96
+ }
@@ -0,0 +1,20 @@
1
+ import { Worker } from 'node:worker_threads';
2
+ import { LogLevel } from 'node-ansi-logger';
3
+ export declare class ThreadsManager {
4
+ private debug;
5
+ private verbose;
6
+ private log;
7
+ static logLevel: LogLevel;
8
+ private server;
9
+ private readonly boundMsgHandler;
10
+ private interval;
11
+ private intervalMs;
12
+ private threads;
13
+ constructor(intervalMs?: number);
14
+ destroy(): void;
15
+ private msgHandler;
16
+ private intervalHandler;
17
+ runThread(name: string, workerData?: Record<string, boolean | number | string | object>, argv?: string[], env?: NodeJS.ProcessEnv, execArgv?: string[], pipedOutput?: boolean): Worker;
18
+ resolvePath(fileName: string): string;
19
+ createESMWorker(name: string, relativePath: string, workerData?: Record<string, boolean | number | string | object>, argv?: string[], env?: NodeJS.ProcessEnv, execArgv?: string[], pipedOutput?: boolean): Worker;
20
+ }
@@ -0,0 +1,170 @@
1
+ if (process.argv.includes('--loader') || process.argv.includes('-loader'))
2
+ console.log('\u001B[32mThreadsManager loaded.\u001B[40;0m');
3
+ import fs from 'node:fs';
4
+ import path, { resolve } from 'node:path';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
6
+ import { Worker } from 'node:worker_threads';
7
+ import { hasParameter } from '@matterbridge/utils/cli';
8
+ import { AnsiLogger, CYAN, db, debugStringify, MAGENTA } from 'node-ansi-logger';
9
+ import { BroadcastServer } from './broadcastServer.js';
10
+ export class ThreadsManager {
11
+ debug;
12
+ verbose;
13
+ log;
14
+ static logLevel;
15
+ server;
16
+ boundMsgHandler = this.msgHandler.bind(this);
17
+ interval;
18
+ intervalMs;
19
+ threads = [
20
+ { name: 'CheckUpdates', path: 'workerCheckUpdates.js', type: 'worker' },
21
+ { name: 'SystemCheck', path: 'workerSystemCheck.js', type: 'worker' },
22
+ { name: 'GlobalPrefix', path: 'workerGlobalPrefix.js', type: 'worker' },
23
+ { name: 'SpawnCommand', path: 'workerSpawnCommand.js', type: 'worker' },
24
+ ];
25
+ constructor(intervalMs = 10_000) {
26
+ this.debug = hasParameter('debug') || hasParameter('verbose') || hasParameter('debug-threads') || hasParameter('verbose-threads');
27
+ this.verbose = hasParameter('verbose') || hasParameter('verbose-threads');
28
+ this.log = new AnsiLogger({
29
+ logName: 'ThreadsManager',
30
+ logNameColor: MAGENTA,
31
+ logTimestampFormat: 4,
32
+ logLevel: this.debug ? "debug" : "info",
33
+ });
34
+ ThreadsManager.logLevel = this.log.logLevel;
35
+ this.server = new BroadcastServer('manager', this.log);
36
+ this.server.on('broadcast_message', this.boundMsgHandler);
37
+ this.intervalMs = intervalMs;
38
+ this.interval = setInterval(this.intervalHandler.bind(this), this.intervalMs);
39
+ if (this.verbose)
40
+ this.log.notice(`ThreadsManager initialized. Listening for broadcast messages...`);
41
+ }
42
+ destroy() {
43
+ clearInterval(this.interval);
44
+ this.server.off('broadcast_message', this.boundMsgHandler);
45
+ this.server.close();
46
+ if (this.verbose)
47
+ this.log.notice(`ThreadsManager destroyed. Broadcast server closed.`);
48
+ }
49
+ async msgHandler(msg) {
50
+ if (this.server.isWorkerRequest(msg) && (msg.dst === 'all' || msg.dst === 'manager')) {
51
+ if (this.verbose)
52
+ this.log.debug(`Received broadcast request ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}: ${debugStringify(msg)}${db}`);
53
+ switch (msg.type) {
54
+ case 'get_log_level':
55
+ this.server.respond({ ...msg, result: { logLevel: this.log.logLevel } });
56
+ break;
57
+ case 'set_log_level':
58
+ if (!this.debug) {
59
+ this.log.logLevel = msg.params.logLevel;
60
+ ThreadsManager.logLevel = msg.params.logLevel;
61
+ }
62
+ this.server.respond({ ...msg, result: { logLevel: this.log.logLevel } });
63
+ break;
64
+ case 'manager_run':
65
+ try {
66
+ this.runThread(msg.params.name, msg.params.workerData, msg.params.argv, msg.params.env, msg.params.execArgv, msg.params.pipedOutput);
67
+ this.server.respond({ ...msg, result: { success: true } });
68
+ }
69
+ catch (err) {
70
+ this.log.error(`Failed to run thread ${CYAN}${msg.params.name}${db}: ${err.message}`);
71
+ this.server.respond({ ...msg, result: { success: false } });
72
+ }
73
+ break;
74
+ default:
75
+ if (this.verbose)
76
+ this.log.debug(`Unknown broadcast request ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}`);
77
+ }
78
+ }
79
+ }
80
+ intervalHandler() {
81
+ this.log.debug(`Threads status:\n${this.threads.map((t) => `${t.name} running: ${t.worker ? 'yes' : 'no'}, lastSeen: ${t.lastSeen ? new Date(t.lastSeen).toISOString() : 'never'}, runs: ${t.runCount ?? 0}, errors: ${t.errorCount ?? 0}`).join('\n')}`);
82
+ for (const thread of this.threads) {
83
+ if (thread.worker && Date.now() - (thread.lastSeen || 0) > this.intervalMs) {
84
+ const msg = { type: 'ping', threadId: thread.worker.threadId, threadName: thread.name };
85
+ thread.worker.postMessage(msg);
86
+ }
87
+ if (thread.worker && Date.now() - (thread.lastSeen || 0) > this.intervalMs * 2) {
88
+ this.log.warn(`Thread ${CYAN}${thread.name}${db} has not been seen for more than ${this.intervalMs * 2} ms. It may be unresponsive.`);
89
+ }
90
+ }
91
+ }
92
+ runThread(name, workerData, argv, env, execArgv, pipedOutput = false) {
93
+ const threadInfo = this.threads.find((t) => t.name === name);
94
+ if (!threadInfo) {
95
+ throw new Error(`Thread ${name} not found`);
96
+ }
97
+ if (threadInfo.worker) {
98
+ throw new Error(`Thread ${name} is already running with thread ID ${threadInfo.worker.threadId}`);
99
+ }
100
+ this.log.info(`Starting thread ${threadInfo.name} from path: ${threadInfo.path} type ${threadInfo.type}...`);
101
+ threadInfo.lastStarted = undefined;
102
+ threadInfo.lastStopped = undefined;
103
+ threadInfo.lastDuration = undefined;
104
+ threadInfo.worker = this.createESMWorker(threadInfo.name, this.resolvePath(threadInfo.path), { ...workerData, debug: this.debug, verbose: this.verbose, logLevel: this.log.logLevel }, argv, env, execArgv, pipedOutput);
105
+ const worker = threadInfo.worker;
106
+ worker.once('online', () => {
107
+ threadInfo.runCount = (threadInfo.runCount ?? 0) + 1;
108
+ threadInfo.lastStarted = Date.now();
109
+ threadInfo.lastSeen = Date.now();
110
+ this.log.info(`Thread ${threadInfo.name} is online started at ${new Date(threadInfo.lastStarted).toISOString()} with thread ID ${worker.threadId}`);
111
+ });
112
+ worker.once('exit', () => {
113
+ const stoppedAt = Date.now();
114
+ threadInfo.lastStopped = stoppedAt;
115
+ threadInfo.lastDuration = Math.max(0, stoppedAt - (threadInfo.lastStarted ?? stoppedAt));
116
+ threadInfo.worker = undefined;
117
+ threadInfo.lastSeen = Date.now();
118
+ this.log.info(`Thread ${threadInfo.name} has exited at ${new Date(threadInfo.lastStopped).toISOString()} after running for ${threadInfo.lastDuration} ms`);
119
+ });
120
+ worker.on('message', (message) => {
121
+ threadInfo.lastSeen = Date.now();
122
+ this.log.info(`Thread ${threadInfo.name} sent a message at ${new Date().toISOString()} after running for ${threadInfo.lastDuration} ms: ${debugStringify(message)}`);
123
+ if (message.type === 'log') {
124
+ AnsiLogger.create({ logName: threadInfo.name, logNameColor: MAGENTA, logTimestampFormat: 4, logLevel: this.log.logLevel }).log(message.logLevel, message.message);
125
+ }
126
+ });
127
+ worker.on('messageerror', () => {
128
+ threadInfo.errorCount = (threadInfo.errorCount ?? 0) + 1;
129
+ this.log.error(`Thread ${threadInfo.name} encountered a message error at ${new Date().toISOString()} after running for ${threadInfo.lastDuration} ms`);
130
+ });
131
+ worker.once('error', () => {
132
+ threadInfo.errorCount = (threadInfo.errorCount ?? 0) + 1;
133
+ const stoppedAt = Date.now();
134
+ threadInfo.lastStopped = stoppedAt;
135
+ threadInfo.lastDuration = Math.max(0, stoppedAt - (threadInfo.lastStarted ?? stoppedAt));
136
+ threadInfo.worker = undefined;
137
+ this.log.error(`Thread ${threadInfo.name} encountered an error at ${new Date(threadInfo.lastStopped).toISOString()} after running for ${threadInfo.lastDuration} ms`);
138
+ });
139
+ this.log.info(`Started thread ${threadInfo.name} from path: ${threadInfo.path} type ${threadInfo.type}...`);
140
+ return threadInfo.worker;
141
+ }
142
+ resolvePath(fileName) {
143
+ const currentModuleDirectory = path.dirname(fileURLToPath(import.meta.url));
144
+ const candidates = [
145
+ path.join(currentModuleDirectory, fileName),
146
+ path.join(currentModuleDirectory, '..', 'dist', fileName),
147
+ ];
148
+ for (const candidate of candidates) {
149
+ if (fs.existsSync(candidate))
150
+ return candidate;
151
+ }
152
+ return candidates[0];
153
+ }
154
+ createESMWorker(name, relativePath, workerData, argv, env, execArgv, pipedOutput = false) {
155
+ const fileURL = pathToFileURL(resolve(relativePath));
156
+ const options = {
157
+ workerData: { ...workerData, threadName: name, debug: this.debug, verbose: this.verbose, logLevel: this.log.logLevel },
158
+ type: 'module',
159
+ name,
160
+ argv: argv ?? process.argv.slice(2),
161
+ env: env ?? process.env,
162
+ execArgv,
163
+ stdout: pipedOutput,
164
+ stderr: pipedOutput,
165
+ };
166
+ if (this.verbose)
167
+ this.log.debug(`Creating ESM Worker ${name} with file URL: ${fileURL.href} and options: ${debugStringify(options)}`);
168
+ return new Worker(fileURL, options);
169
+ }
170
+ }
@@ -1,41 +1,18 @@
1
- import { isMainThread, parentPort, threadId, workerData } from 'node:worker_threads';
2
- import { hasParameter, inspectError } from '@matterbridge/utils';
3
- import { AnsiLogger, MAGENTA } from 'node-ansi-logger';
4
- import { BroadcastServer } from './broadcastServer.js';
1
+ import { inspectError } from '@matterbridge/utils/error';
5
2
  import { checkUpdates } from './checkUpdates.js';
6
- import { logWorkerInfo, parentLog, parentPost, threadLogger } from './worker.js';
7
- const debug = hasParameter('debug') || hasParameter('verbose') || hasParameter('debug-worker') || hasParameter('verbose-worker');
8
- const verbose = hasParameter('verbose') || hasParameter('verbose-worker');
9
- const name = 'MatterbridgeCheckUpdates';
10
- if (!isMainThread && parentPort) {
11
- parentPost({ type: 'init', threadId, threadName: workerData.threadName, success: true });
12
- if (debug)
13
- parentLog(name, "info", `Worker ${workerData.threadName}:${threadId} initialized.`);
14
- }
15
- const log = new AnsiLogger({
16
- logName: name,
17
- logNameColor: MAGENTA,
18
- logTimestampFormat: 4,
19
- logLevel: debug ? "debug" : "info",
3
+ import { WorkerWrapper } from './workerWrapper.js';
4
+ new WorkerWrapper('GlobalPrefix', async (worker) => {
5
+ worker.logger("info", `Starting check updates...`);
6
+ let success = false;
7
+ try {
8
+ const shared = (await worker.server.fetch({ type: 'matterbridge_shared', src: `matterbridge`, dst: 'matterbridge' }, 5000)).result.data;
9
+ await checkUpdates(shared);
10
+ worker.logger("info", `Check updates succeeded`);
11
+ success = true;
12
+ }
13
+ catch (error) {
14
+ const errorMessage = inspectError(worker.log, `Failed to check updates`, error);
15
+ worker.logger("error", errorMessage);
16
+ }
17
+ return success;
20
18
  });
21
- const server = new BroadcastServer('matterbridge', log);
22
- if (verbose)
23
- logWorkerInfo(log, verbose);
24
- threadLogger(name, "info", `Starting check updates...`);
25
- let success = false;
26
- try {
27
- const shared = (await server.fetch({ type: 'matterbridge_shared', src: `matterbridge`, dst: 'matterbridge' }, 1000)).result.data;
28
- await checkUpdates(shared);
29
- threadLogger(name, "info", `Check updates succeeded`);
30
- success = true;
31
- }
32
- catch (error) {
33
- const errorMessage = inspectError(log, `Failed to check updates`, error);
34
- threadLogger(name, "error", errorMessage);
35
- }
36
- server.close();
37
- if (!isMainThread && parentPort) {
38
- parentPost({ type: 'exit', threadId, threadName: workerData.threadName, success });
39
- if (debug)
40
- parentLog(name, "info", `Worker ${workerData.threadName}:${threadId} exiting with success: ${success}.`);
41
- }
@@ -1,41 +1,19 @@
1
- import { isMainThread, parentPort, threadId, workerData } from 'node:worker_threads';
2
- import { getGlobalNodeModules, hasParameter, inspectError } from '@matterbridge/utils';
3
- import { AnsiLogger, MAGENTA } from 'node-ansi-logger';
4
- import { BroadcastServer } from './broadcastServer.js';
5
- import { logWorkerInfo, parentLog, parentPost, threadLogger } from './worker.js';
6
- const debug = hasParameter('debug') || hasParameter('verbose') || hasParameter('debug-worker') || hasParameter('verbose-worker');
7
- const verbose = hasParameter('verbose') || hasParameter('verbose-worker');
8
- const name = 'MatterbridgeGlobalPrefix';
9
- if (!isMainThread && parentPort) {
10
- parentPost({ type: 'init', threadId, threadName: workerData.threadName, success: true });
11
- if (debug)
12
- parentLog(name, "info", `Worker ${workerData.threadName}:${threadId} initialized.`);
13
- }
14
- const log = new AnsiLogger({
15
- logName: name,
16
- logNameColor: MAGENTA,
17
- logTimestampFormat: 4,
18
- logLevel: debug ? "debug" : "info",
1
+ import { inspectError } from '@matterbridge/utils/error';
2
+ import { getGlobalNodeModules } from '@matterbridge/utils/npm-prefix';
3
+ import { WorkerWrapper } from './workerWrapper.js';
4
+ new WorkerWrapper('GlobalPrefix', async (worker) => {
5
+ let prefix;
6
+ worker.logger("info", `Starting global prefix check...`);
7
+ let success = false;
8
+ try {
9
+ prefix = await getGlobalNodeModules();
10
+ worker.server.request({ type: 'matterbridge_global_prefix', src: `matterbridge`, dst: 'matterbridge', params: { prefix } });
11
+ worker.logger("info", `Global node_modules directory: ${prefix}`);
12
+ success = true;
13
+ }
14
+ catch (error) {
15
+ const errorMessage = inspectError(worker.log, `Failed to get global node modules`, error);
16
+ worker.logger("error", errorMessage);
17
+ }
18
+ return success;
19
19
  });
20
- const server = new BroadcastServer('matterbridge', log);
21
- if (verbose)
22
- logWorkerInfo(log, verbose);
23
- let prefix;
24
- threadLogger(name, "info", `Starting global prefix check...`);
25
- let success = false;
26
- try {
27
- prefix = await getGlobalNodeModules();
28
- server.request({ type: 'matterbridge_global_prefix', src: `matterbridge`, dst: 'matterbridge', params: { prefix } });
29
- threadLogger(name, "info", `Global node_modules directory: ${prefix}`);
30
- success = true;
31
- }
32
- catch (error) {
33
- const errorMessage = inspectError(log, `Failed to get global node modules`, error);
34
- threadLogger(name, "error", errorMessage);
35
- }
36
- server.close();
37
- if (!isMainThread && parentPort) {
38
- parentPost({ type: 'exit', threadId, threadName: workerData.threadName, success });
39
- if (debug)
40
- parentLog(name, "info", `Worker ${workerData.threadName}:${threadId} exiting with success: ${success}.`);
41
- }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import { workerData } from 'node:worker_threads';
2
+ import { spawnCommand } from './spawnCommand.js';
3
+ import { WorkerWrapper } from './workerWrapper.js';
4
+ new WorkerWrapper('GlobalPrefix', async (worker) => {
5
+ worker.logger("info", `Starting spawn command ${workerData.command} with args ${workerData.args.join(' ')} and package command ${workerData.packageCommand} for package ${workerData.packageName}...`);
6
+ const success = await spawnCommand(workerData.command, workerData.args, workerData.packageCommand, workerData.packageName);
7
+ if (success)
8
+ worker.logger("info", `Spawn command ${workerData.command} with args ${workerData.args.join(' ')} executed successfully`);
9
+ else
10
+ worker.logger("error", `Spawn command ${workerData.command} with args ${workerData.args.join(' ')} failed`);
11
+ return success;
12
+ });
@@ -1,84 +1,62 @@
1
1
  import os from 'node:os';
2
- import { isMainThread, parentPort, threadId, workerData } from 'node:worker_threads';
3
- import { excludedInterfaceNamePattern, hasParameter, inspectError } from '@matterbridge/utils';
4
- import { AnsiLogger, MAGENTA } from 'node-ansi-logger';
5
- import { BroadcastServer } from './broadcastServer.js';
6
- import { logWorkerInfo, parentLog, parentPost, threadLogger } from './worker.js';
7
- const debug = hasParameter('debug') || hasParameter('verbose') || hasParameter('debug-worker') || hasParameter('verbose-worker');
8
- const verbose = hasParameter('verbose') || hasParameter('verbose-worker');
9
- const name = 'MatterbridgeSystemCheck';
10
- if (!isMainThread && parentPort) {
11
- parentPost({ type: 'init', threadId, threadName: workerData.threadName, success: true });
12
- if (debug)
13
- parentLog(name, "info", `Worker ${workerData.threadName}:${threadId} initialized.`);
14
- }
15
- const log = new AnsiLogger({
16
- logName: name,
17
- logNameColor: MAGENTA,
18
- logTimestampFormat: 4,
19
- logLevel: debug ? "debug" : "info",
20
- });
21
- const server = new BroadcastServer('matterbridge', log);
22
- if (verbose)
23
- logWorkerInfo(log, verbose);
24
- threadLogger(name, "info", `Starting system check...`);
25
- let success = false;
26
- try {
27
- const shared = (await server.fetch({ type: 'matterbridge_shared', src: `matterbridge`, dst: 'matterbridge' }, 1000)).result.data;
28
- if (process.env.NVM_BIN && process.env.NVM_DIR)
29
- threadLogger(name, "error", `NVM is a development tool and is not supported in production. Please install node from https://github.com/nodesource/distributions.`);
30
- const nodeVersion = process.versions.node;
31
- const versionMajor = parseInt(nodeVersion.split('.')[0]);
32
- const versionMinor = parseInt(nodeVersion.split('.')[1]);
33
- const versionPatch = parseInt(nodeVersion.split('.')[2]);
34
- threadLogger(name, "debug", `Node.js Version: ${versionMajor}.${versionMinor}.${versionPatch}`);
35
- if (versionMajor === 20 && versionMinor < 19)
36
- threadLogger(name, "error", `Node.js version < 20.19.0 is not supported. Please upgrade to Node.js LTS version (24.x).`);
37
- if (versionMajor === 22 && versionMinor < 13)
38
- threadLogger(name, "error", `Node.js version < 22.13.0 is not supported. Please upgrade to Node.js LTS version (24.x).`);
39
- if (versionMajor === 21 || versionMajor === 23 || versionMajor === 25)
40
- threadLogger(name, "error", `Node.js odd major versions are not supported. Please upgrade to Node.js LTS version (24.x).`);
41
- if (versionMajor !== 24)
42
- threadLogger(name, "notice", `You are running Node.js ${versionMajor}.${versionMinor}.${versionPatch}. Please consider upgrading to Node.js LTS version (24.x).`);
43
- const networkInterfaces = os.networkInterfaces();
44
- let foundInternal = false;
45
- let foundExternal = false;
46
- let foundIpv4 = false;
47
- let foundIpv6 = false;
48
- for (const [interfaceName, interfaceDetails] of Object.entries(networkInterfaces)) {
49
- if (!shared.mdnsInterface && excludedInterfaceNamePattern.test(interfaceName))
50
- threadLogger(name, "warn", `Found network interface '${interfaceName}'. Please use --mdnsinterface parameter to specify the correct local interface for mDNS.`);
51
- if (excludedInterfaceNamePattern.test(interfaceName))
52
- continue;
53
- for (const detail of interfaceDetails || []) {
54
- if (detail.internal)
55
- foundInternal = true;
56
- if (!detail.internal)
57
- foundExternal = true;
58
- if (detail.family === 'IPv4' && !detail.internal && foundIpv4 === false)
59
- foundIpv4 = true;
60
- if (detail.family === 'IPv6' && !detail.internal && foundIpv6 === false)
61
- foundIpv6 = true;
2
+ import { inspectError } from '@matterbridge/utils/error';
3
+ import { excludedInterfaceNamePattern } from '@matterbridge/utils/network';
4
+ import { WorkerWrapper } from './workerWrapper.js';
5
+ new WorkerWrapper('SystemCheck', async (worker) => {
6
+ worker.logger("info", `Starting system check...`);
7
+ let success = false;
8
+ try {
9
+ const shared = (await worker.server.fetch({ type: 'matterbridge_shared', src: `matterbridge`, dst: 'matterbridge' }, 1000)).result.data;
10
+ if (process.env.NVM_BIN && process.env.NVM_DIR)
11
+ worker.logger("error", `NVM is a development tool and is not supported in production. Please install node from https://github.com/nodesource/distributions.`);
12
+ const nodeVersion = process.versions.node;
13
+ const versionMajor = parseInt(nodeVersion.split('.')[0]);
14
+ const versionMinor = parseInt(nodeVersion.split('.')[1]);
15
+ const versionPatch = parseInt(nodeVersion.split('.')[2]);
16
+ worker.logger("debug", `Node.js Version: ${versionMajor}.${versionMinor}.${versionPatch}`);
17
+ if (versionMajor === 20 && versionMinor < 19)
18
+ worker.logger("error", `Node.js version < 20.19.0 is not supported. Please upgrade to Node.js LTS version (24.x).`);
19
+ if (versionMajor === 22 && versionMinor < 13)
20
+ worker.logger("error", `Node.js version < 22.13.0 is not supported. Please upgrade to Node.js LTS version (24.x).`);
21
+ if (versionMajor === 21 || versionMajor === 23 || versionMajor === 25)
22
+ worker.logger("error", `Node.js odd major versions are not supported. Please upgrade to Node.js LTS version (24.x).`);
23
+ if (versionMajor !== 24)
24
+ worker.logger("notice", `You are running Node.js ${versionMajor}.${versionMinor}.${versionPatch}. Please consider upgrading to Node.js LTS version (24.x).`);
25
+ const networkInterfaces = os.networkInterfaces();
26
+ let foundInternal = false;
27
+ let foundExternal = false;
28
+ let foundIpv4 = false;
29
+ let foundIpv6 = false;
30
+ for (const [interfaceName, interfaceDetails] of Object.entries(networkInterfaces)) {
31
+ if (!shared.mdnsInterface && excludedInterfaceNamePattern.test(interfaceName))
32
+ worker.logger("warn", `Found network interface '${interfaceName}'. Please use --mdnsinterface parameter to specify the correct local interface for mDNS.`);
33
+ if (excludedInterfaceNamePattern.test(interfaceName))
34
+ continue;
35
+ for (const detail of interfaceDetails || []) {
36
+ if (detail.internal)
37
+ foundInternal = true;
38
+ if (!detail.internal)
39
+ foundExternal = true;
40
+ if (detail.family === 'IPv4' && !detail.internal && foundIpv4 === false)
41
+ foundIpv4 = true;
42
+ if (detail.family === 'IPv6' && !detail.internal && foundIpv6 === false)
43
+ foundIpv6 = true;
44
+ }
62
45
  }
46
+ if (!foundInternal)
47
+ worker.logger("error", `No internal network interface found. Check your network configuration.`);
48
+ if (!foundExternal)
49
+ worker.logger("error", `No external network interface found. Check your network configuration.`);
50
+ if (!foundIpv4)
51
+ worker.logger("error", `No IPv4 network interface found. Check your network configuration.`);
52
+ if (!foundIpv6)
53
+ worker.logger("error", `No IPv6 network interface found. Check your network configuration.`);
54
+ worker.logger("info", `System check succeeded`);
55
+ success = true;
56
+ }
57
+ catch (error) {
58
+ const errorMessage = inspectError(worker.log, `Failed to perform system check`, error);
59
+ worker.logger("error", errorMessage);
63
60
  }
64
- if (!foundInternal)
65
- threadLogger(name, "error", `No internal network interface found. Check your network configuration.`);
66
- if (!foundExternal)
67
- threadLogger(name, "error", `No external network interface found. Check your network configuration.`);
68
- if (!foundIpv4)
69
- threadLogger(name, "error", `No IPv4 network interface found. Check your network configuration.`);
70
- if (!foundIpv6)
71
- threadLogger(name, "error", `No IPv6 network interface found. Check your network configuration.`);
72
- threadLogger(name, "info", `System check succeeded`);
73
- success = true;
74
- }
75
- catch (error) {
76
- const errorMessage = inspectError(log, `Failed to perform system check`, error);
77
- threadLogger(name, "error", errorMessage);
78
- }
79
- server.close();
80
- if (!isMainThread && parentPort) {
81
- parentPost({ type: 'exit', threadId, threadName: workerData.threadName, success });
82
- if (debug)
83
- parentLog(name, "info", `Worker ${workerData.threadName}:${threadId} exiting with success: ${success}.`);
84
- }
61
+ return success;
62
+ });
@@ -0,0 +1,16 @@
1
+ import type { ParentPortMessage } from '@matterbridge/types';
2
+ import { AnsiLogger, LogLevel } from 'node-ansi-logger';
3
+ import { BroadcastServer } from './broadcastServer.js';
4
+ export declare class WorkerWrapper {
5
+ name: string;
6
+ debug: boolean;
7
+ verbose: boolean;
8
+ log: AnsiLogger;
9
+ server: BroadcastServer;
10
+ constructor(name: string, callback: (worker: WorkerWrapper) => Promise<boolean>);
11
+ destroy(success: boolean): void;
12
+ parentPost(message: ParentPortMessage): void;
13
+ parentLog(logName: string | undefined, logLevel: LogLevel, message: string): void;
14
+ logger(level: LogLevel, message: string): void;
15
+ logWorkerInfo(log: AnsiLogger, logEnv?: boolean): void;
16
+ }
@@ -0,0 +1,91 @@
1
+ import { inspect } from 'node:util';
2
+ import { isMainThread, parentPort, threadId, workerData } from 'node:worker_threads';
3
+ import { hasParameter } from '@matterbridge/utils/cli';
4
+ import { AnsiLogger, debugStringify, MAGENTA } from 'node-ansi-logger';
5
+ import { BroadcastServer } from './broadcastServer.js';
6
+ import { ThreadsManager } from './threadsManager.js';
7
+ export class WorkerWrapper {
8
+ name;
9
+ debug = hasParameter('debug') || hasParameter('verbose') || hasParameter('debug-threads') || hasParameter('verbose-threads');
10
+ verbose = hasParameter('verbose') || hasParameter('verbose-threads');
11
+ log;
12
+ server;
13
+ constructor(name, callback) {
14
+ this.name = name;
15
+ this.log = new AnsiLogger({
16
+ logName: name,
17
+ logNameColor: MAGENTA,
18
+ logTimestampFormat: 4,
19
+ logLevel: this.debug ? "debug" : "info",
20
+ });
21
+ this.server = new BroadcastServer('matterbridge', this.log);
22
+ if (!isMainThread && parentPort) {
23
+ parentPort.on('message', (message) => {
24
+ if (this.debug)
25
+ this.log.debug(`Worker ${workerData.threadName}:${threadId} received message from parent: ${debugStringify(message)}`);
26
+ switch (message.type) {
27
+ case 'ping':
28
+ this.parentLog(workerData.threadName, "debug", `Worker ${workerData.threadName}:${threadId} received ping message type from parent: ${debugStringify(message)}`);
29
+ this.parentPost({ type: 'pong', threadId, threadName: workerData.threadName });
30
+ this.parentLog(workerData.threadName, "debug", `Worker ${workerData.threadName}:${threadId} sent pong message type to parent: ${debugStringify(message)}`);
31
+ break;
32
+ case 'pong':
33
+ this.parentLog(workerData.threadName, "debug", `Worker ${workerData.threadName}:${threadId} received pong message type from parent: ${debugStringify(message)}`);
34
+ break;
35
+ default:
36
+ this.parentLog(workerData.threadName, "warn", `Worker ${workerData.threadName}:${threadId} received unknown message type from parent: ${debugStringify(message)}`);
37
+ }
38
+ });
39
+ }
40
+ if (!isMainThread && parentPort && workerData) {
41
+ name = workerData.threadName;
42
+ this.parentPost({ type: 'init', threadId, threadName: workerData.threadName, success: true });
43
+ if (this.debug)
44
+ this.parentLog(name, "info", `Worker ${workerData.threadName}:${threadId} initialized.`);
45
+ }
46
+ if (this.verbose)
47
+ this.logWorkerInfo(this.log, false);
48
+ setImmediate(async () => {
49
+ const success = await callback(this);
50
+ this.destroy(success);
51
+ });
52
+ }
53
+ destroy(success) {
54
+ this.server.close();
55
+ if (!isMainThread && parentPort && workerData) {
56
+ if (this.debug)
57
+ this.parentLog(this.name, "info", `Worker ${workerData.threadName}:${threadId} exiting with success: ${success}.`);
58
+ this.parentPost({ type: 'exit', threadId, threadName: workerData.threadName, success });
59
+ parentPort.close();
60
+ }
61
+ }
62
+ parentPost(message) {
63
+ if (!parentPort)
64
+ throw new Error(`WorkerServer ${workerData.threadName}: parentPort is not available.`);
65
+ parentPort.postMessage(message);
66
+ if (this.debug)
67
+ this.log.debug(`Worker ${workerData.threadName}:${threadId} sent message to parent: ${debugStringify(message)}`);
68
+ }
69
+ parentLog(logName, logLevel, message) {
70
+ if (!parentPort)
71
+ throw new Error(`WorkerServer ${workerData.threadName}: parentPort is not available.`);
72
+ const logMessage = { type: 'log', threadId, threadName: workerData.threadName, logName, logLevel, message };
73
+ parentPort.postMessage(logMessage);
74
+ if (this.debug)
75
+ this.log.debug(`Worker ${workerData.threadName}:${threadId} sent log to parent: ${logName} ${logLevel} ${message}`);
76
+ }
77
+ logger(level, message) {
78
+ if (!isMainThread && parentPort)
79
+ this.parentLog(this.name, level, message);
80
+ else
81
+ AnsiLogger.create({ logName: this.name, logNameColor: MAGENTA, logTimestampFormat: 4, logLevel: ThreadsManager.logLevel }).log(level, message);
82
+ }
83
+ logWorkerInfo(log, logEnv = false) {
84
+ log.debug(`${isMainThread ? 'Main thread' : 'Worker thread'}: ${workerData?.threadName}:${threadId} Pid: ${process.pid}`);
85
+ log.debug(`ParentPort: ${parentPort ? 'active' : 'not active'}`);
86
+ log.debug(`WorkerData: ${workerData ? debugStringify(workerData) : 'none'}`);
87
+ const argv = process.argv.slice(2);
88
+ log.debug(`Argv: ${argv.length ? argv.join(' ') : 'none'}`);
89
+ log.debug(`Env: ${logEnv ? inspect(process.env, true, 10, true) : 'not logged'}`);
90
+ }
91
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matterbridge/thread",
3
- "version": "3.6.0",
3
+ "version": "3.6.1-dev-20260309-6341dee",
4
4
  "description": "Matterbridge thread library",
5
5
  "author": "https://github.com/Luligu",
6
6
  "homepage": "https://matterbridge.io/",
@@ -49,6 +49,14 @@
49
49
  ".": {
50
50
  "types": "./dist/export.d.ts",
51
51
  "import": "./dist/export.js"
52
+ },
53
+ "./server": {
54
+ "types": "./dist/broadcastServer.d.ts",
55
+ "import": "./dist/broadcastServer.js"
56
+ },
57
+ "./manager": {
58
+ "types": "./dist/threadsManager.d.ts",
59
+ "import": "./dist/threadsManager.js"
52
60
  }
53
61
  },
54
62
  "engines": {
@@ -61,8 +69,8 @@
61
69
  "CHANGELOG.md"
62
70
  ],
63
71
  "dependencies": {
64
- "@matterbridge/types": "3.6.0",
65
- "@matterbridge/utils": "3.6.0",
72
+ "@matterbridge/types": "3.6.1-dev-20260309-6341dee",
73
+ "@matterbridge/utils": "3.6.1-dev-20260309-6341dee",
66
74
  "node-ansi-logger": "3.2.0"
67
75
  }
68
76
  }
package/dist/worker.d.ts DELETED
@@ -1,8 +0,0 @@
1
- import { Worker } from 'node:worker_threads';
2
- import type { ParentPortMessage } from '@matterbridge/types';
3
- import { AnsiLogger, LogLevel } from 'node-ansi-logger';
4
- export declare function parentPost(message: ParentPortMessage): void;
5
- export declare function parentLog(logName: string | undefined, logLevel: LogLevel, message: string): void;
6
- export declare function threadLogger(threadName: string, level: LogLevel, message: string): void;
7
- export declare function createESMWorker(name: string, relativePath: string, workerData?: Record<string, boolean | number | string | object>, argv?: string[], env?: NodeJS.ProcessEnv, execArgv?: string[], pipedOutput?: boolean): Worker;
8
- export declare function logWorkerInfo(log: AnsiLogger, logEnv?: boolean): void;
package/dist/worker.js DELETED
@@ -1,53 +0,0 @@
1
- import { resolve } from 'node:path';
2
- import { pathToFileURL } from 'node:url';
3
- import { inspect } from 'node:util';
4
- import { isMainThread, parentPort, threadId, Worker, workerData } from 'node:worker_threads';
5
- import { hasParameter } from '@matterbridge/utils';
6
- import { AnsiLogger, debugStringify, MAGENTA } from 'node-ansi-logger';
7
- const debug = hasParameter('debug') || hasParameter('verbose') || hasParameter('debug-worker') || hasParameter('verbose-worker');
8
- const verbose = hasParameter('verbose') || hasParameter('verbose-worker');
9
- const log = new AnsiLogger({ logName: 'Worker', logNameColor: MAGENTA, logTimestampFormat: 4, logLevel: debug ? "debug" : "info" });
10
- export function parentPost(message) {
11
- if (!parentPort)
12
- throw new Error(`WorkerServer ${workerData.threadName}: parentPort is not available.`);
13
- parentPort.postMessage(message);
14
- if (debug)
15
- log.debug(`Worker ${workerData.threadName}:${threadId} sent message to parent: ${debugStringify(message)}`);
16
- }
17
- export function parentLog(logName, logLevel, message) {
18
- if (!parentPort)
19
- throw new Error(`WorkerServer ${workerData.threadName}: parentPort is not available.`);
20
- const logMessage = { type: 'log', threadId, threadName: workerData.threadName, logName, logLevel, message };
21
- parentPort.postMessage(logMessage);
22
- if (debug)
23
- log.debug(`Worker ${workerData.threadName}:${threadId} sent log to parent: ${logName} ${logLevel} ${message}`);
24
- }
25
- export function threadLogger(threadName, level, message) {
26
- AnsiLogger.create({ logName: threadName, logNameColor: MAGENTA, logTimestampFormat: 4, logLevel: level }).log(level, message);
27
- if (!isMainThread && parentPort)
28
- parentLog(threadName, level, message);
29
- }
30
- export function createESMWorker(name, relativePath, workerData, argv, env, execArgv, pipedOutput = false) {
31
- const fileURL = pathToFileURL(resolve(relativePath));
32
- const options = {
33
- workerData: { ...workerData, threadName: name },
34
- type: 'module',
35
- name,
36
- argv: argv ?? process.argv.slice(2),
37
- env: env ?? process.env,
38
- execArgv,
39
- stdout: pipedOutput,
40
- stderr: pipedOutput,
41
- };
42
- if (verbose)
43
- log.debug(`Creating ESM Worker ${name} with file URL: ${fileURL.href} and options: ${debugStringify(options)}`);
44
- return new Worker(fileURL, options);
45
- }
46
- export function logWorkerInfo(log, logEnv = false) {
47
- log.debug(`${isMainThread ? 'Main thread' : 'Worker thread'}: ${workerData?.threadName}:${threadId} Pid: ${process.pid}`);
48
- log.debug(`ParentPort: ${parentPort ? 'active' : 'not active'}`);
49
- log.debug(`WorkerData: ${workerData ? inspect(workerData, true, 10, true) : 'none'}`);
50
- const argv = process.argv.slice(2);
51
- log.debug(`Argv: ${argv.length ? argv.join(' ') : 'none'}`);
52
- log.debug(`Env: ${logEnv ? inspect(process.env, true, 10, true) : 'not logged'}`);
53
- }