farmcontrol-server/src/network/websocketScanner.js
Tom Butcher 158eb39f06
All checks were successful
farmcontrol/farmcontrol-server/pipeline/head This commit looks good
Enhance WebSocketScanner and WebSocketScannerWorker for improved scanning capabilities
- Refactored WebSocketScanner to support both worker-based and in-process scanning, allowing for better performance based on the environment.
- Introduced a new WebSocketScannerWorker class to handle scanning tasks, improving modularity and separation of concerns.
- Added detailed event handling for service discovery and scan progress, enhancing real-time feedback during network scans.
- Implemented graceful shutdown and cleanup of workers, ensuring resource management and stability during scanning operations.
- Enhanced error handling and logging throughout the scanning process for better visibility and debugging.
2026-09-15 16:31:59 +01:00

241 lines
7.2 KiB
JavaScript

import { EventEmitter } from 'events';
import os from 'os';
import { Worker } from 'worker_threads';
import { WebSocketScannerWorker } from './websocketScannerWorker.js';
const isBun = typeof globalThis.Bun !== 'undefined';
const workerUrl = new URL('./websocketScannerWorker.js', import.meta.url);
export class WebSocketScanner extends EventEmitter {
constructor(options = {}) {
super();
this.scanning = false;
this.stopped = false;
this.workers = [];
this.inProcessWorker = null;
this.useWorkers = !isBun;
this.maxThreads = this.useWorkers
? (options.maxThreads || os.cpus().length)
: Math.max(1, Math.min(options.maxThreads || 16, 24));
this.totalIPs = 0;
this.scannedIPs = 0;
}
/**
* Scans the local network for services on a specified port.
* When loop is true, repeats until stopScan() is called.
*/
async scanNetwork(port, protocol, objectType = 'printer', { loop = false } = {}) {
this.stopped = false;
this.scanning = true;
let foundServices = [];
try {
do {
await this.cleanupWorkers();
if (this.stopped) {
break;
}
foundServices = await this.scanOnce(port, protocol, objectType);
if (this.stopped) {
break;
}
this.emit('scanPassComplete', foundServices);
} while (loop && !this.stopped);
} finally {
await this.cleanupWorkers();
this.scanning = false;
}
return foundServices;
}
async scanOnce(port, protocol, objectType) {
this.scannedIPs = 0;
const foundServices = [];
const { startIP, endIP } = this.getLocalNetworkRange();
const start = this.ipToNumber(startIP);
const end = this.ipToNumber(endIP);
this.totalIPs = end - start + 1;
if (this.useWorkers) {
await this.scanWithWorkers(start, end, port, protocol, objectType, foundServices);
} else {
await this.scanInProcess(startIP, endIP, port, protocol, objectType, foundServices);
}
await this.cleanupWorkers();
return foundServices;
}
handleWorkerMessage(message, foundServices) {
if (this.stopped) {
return;
}
switch (message.type) {
case 'serviceFound':
foundServices.push({ ip: message.ip, hostname: message.hostname });
this.emit('serviceFound', { ip: message.ip, hostname: message.hostname });
break;
case 'scanProgress':
this.scannedIPs += message.increment;
this.emit('scanProgress', {
currentIP: message.currentIP,
progress: (this.scannedIPs / this.totalIPs) * 100
});
break;
default:
break;
}
}
async scanInProcess(startIP, endIP, port, protocol, objectType, foundServices) {
const worker = new WebSocketScannerWorker((message) => {
this.handleWorkerMessage(message, foundServices);
});
this.inProcessWorker = worker;
await worker.scanRange(startIP, endIP, port, protocol, objectType, {
concurrency: this.maxThreads
});
}
async scanWithWorkers(start, end, port, protocol, objectType, foundServices) {
const ipsPerWorker = Math.ceil(this.totalIPs / this.maxThreads);
const workerPromises = [];
for (let i = 0; i < this.maxThreads; i++) {
if (this.stopped) {
break;
}
const workerStart = start + (i * ipsPerWorker);
const workerEnd = Math.min(workerStart + ipsPerWorker - 1, end);
if (workerStart > end) {
break;
}
const worker = new Worker(workerUrl);
this.workers.push(worker);
const workerPromise = new Promise((resolve) => {
let settled = false;
const finish = (results) => {
if (settled) {
return;
}
settled = true;
resolve(results || []);
};
worker.on('message', (message) => {
if (this.stopped) {
finish([]);
return;
}
if (message.type === 'scanComplete') {
finish(message.results);
return;
}
this.handleWorkerMessage(message, foundServices);
});
worker.on('error', () => finish([]));
worker.on('exit', () => finish([]));
});
worker.postMessage({
type: 'scan',
startIP: this.numberToIP(workerStart),
endIP: this.numberToIP(workerEnd),
port,
protocol,
objectType
});
workerPromises.push(workerPromise);
}
await Promise.all(workerPromises);
}
async cleanupWorkers() {
if (this.inProcessWorker) {
this.inProcessWorker.stop();
this.inProcessWorker = null;
}
const workers = this.workers.splice(0, this.workers.length);
await Promise.all(workers.map((worker) => this.terminateWorker(worker)));
}
terminateWorker(worker) {
return Promise.race([
Promise.resolve().then(() => worker.terminate()),
new Promise((resolve) => setTimeout(resolve, 500))
]).catch(() => {});
}
getLocalNetworkRange() {
const interfaces = os.networkInterfaces();
let localIP = null;
let subnetMask = null;
for (const name of Object.keys(interfaces)) {
for (const iface of interfaces[name]) {
const isIPv4 = iface.family === 'IPv4' || iface.family === 4;
if (isIPv4 && !iface.internal) {
localIP = iface.address;
subnetMask = iface.netmask;
break;
}
}
if (localIP) {
break;
}
}
if (!localIP) {
throw new Error('Could not determine local network IP address');
}
const ipNum = this.ipToNumber(localIP);
const maskNum = this.ipToNumber(subnetMask);
const networkNum = ipNum & maskNum;
const broadcastNum = networkNum | (~maskNum >>> 0);
return {
startIP: this.numberToIP(networkNum + 1),
endIP: this.numberToIP(broadcastNum - 1)
};
}
ipToNumber(ip) {
return ip.split('.')
.reduce((acc, octet) => (acc << 8) + parseInt(octet), 0) >>> 0;
}
numberToIP(num) {
return [
(num >>> 24) & 255,
(num >>> 16) & 255,
(num >>> 8) & 255,
num & 255
].join('.');
}
async stopScan() {
this.stopped = true;
this.scanning = false;
await this.cleanupWorkers();
}
}