import log4js from "log4js"; import { WebSocketScanner } from "../network/websocketScanner.js"; import { io } from "socket.io-client"; // Load configuration import { loadConfig, saveConfig } from "../config.js"; import { askOtp, getDeviceInfo, notPrompting } from "../utils.js"; import { sendIPC } from "../electron/ipc.js"; import { PrinterManager } from "../printer/printermanager.js"; import { HostManager } from "../host/hostmanager.js"; import { FileManager } from "../files/filemanager.js"; import { DocumentPrinterManager } from "../documentprinter/documentprintermanager.js"; import { startWaiting, stopWaiting } from "../spinner.js"; const config = loadConfig(); const logger = log4js.getLogger("Socket Client"); logger.level = config.logLevel; export class SocketClient { constructor() { this.socket = null; this.authenticated = false; this.connected = false; this.loading = false; this.host = null; this.id = null; this.reconnectTimeout = null; this.hostManager = new HostManager(this); this.fileManager = new FileManager(this); this.printerManager = new PrinterManager(this); this.documentPrinterManager = new DocumentPrinterManager(this); this.scanner = new WebSocketScanner({ maxThreads: 50 }); this.readLine = null; sendIPC("setOnline", false); sendIPC("setAuthenticated", false); sendIPC("setLoading", false); } setupSocketEventHandlers() { this.socket.on("connect", this.handleConnect.bind(this)); this.socket.on("connect_error", this.handleError.bind(this)); this.socket.on("objectUpdate", this.handleObjectUpdate.bind(this)); this.socket.on("objectAction", this.handleObjectAction.bind(this)); this.socket.on("objectEvent", this.handleObjectEvent.bind(this)); this.socket.on("disconnect", this.handleDisconnect.bind(this)); } scheduleReconnect() { // Clear any pending reconnect timeout if (this.reconnectTimeout) { clearTimeout(this.reconnectTimeout); this.reconnectTimeout = null; } startWaiting("Will attempt to reconnect in 3 seconds...", logger); this.reconnectTimeout = setTimeout(async () => { await stopWaiting(); this.connect(); }, 3000); } subscribeToObjectEvents() { this.subscribeToObjectEvent({ objectType: "host", _id: this.id, eventType: "childUpdate", }); this.subscribeToObjectEvent({ objectType: "host", _id: this.id, eventType: "childDelete", }); this.subscribeToObjectEvent({ objectType: "host", _id: this.id, eventType: "childNew", }); } unsubscribeFromObjectEvents() { this.unsubscribeFromObjectEvent({ objectType: "host", _id: this.id, eventType: "childUpdate", }); this.unsubscribeFromObjectEvent({ objectType: "host", _id: this.id, eventType: "childDelete", }); this.unsubscribeFromObjectEvent({ objectType: "host", _id: this.id, eventType: "childNew", }); } async connect() { try { await stopWaiting(); startWaiting(`Connecting to Socket.IO server: ${config.url}`, logger); this.socket = io(config.url, { auth: { type: "host" }, reconnection: false, timeout: 3000, // 3 second timeout }); this.loading = true; sendIPC("setLoading", true); this.connected = false; sendIPC("setConnected", false); this.authenticated = false; sendIPC("setAuthenticated", false); this.setupSocketEventHandlers(); } catch (error) { await stopWaiting(); logger.error("Failed to create Socket.IO connection:", error); } } disconnect() { this.unsubscribeFromObjectEvents(); this.socket.disconnect(); } async waitForConnection(timeoutMs = 10000) { if (this.socket?.connected) { return; } if (!this.socket) { throw new Error("Socket client is not connected"); } return new Promise((resolve, reject) => { const timeout = setTimeout(() => { this.socket.off("connect", onConnect); reject(new Error("Socket connection timed out")); }, timeoutMs); const onConnect = () => { clearTimeout(timeout); resolve(); }; if (this.socket.connected) { clearTimeout(timeout); resolve(); return; } this.socket.once("connect", onConnect); }); } async authenticateWithOtp(otp = undefined, options = {}) { const { retryOnFailure = otp == undefined, logs = true } = options; if (otp == undefined) { otp = await askOtp(); } await this.waitForConnection(); return this.authenticate({ otp }, { retryOnFailure, logs }); } async authenticate(authenticationData, options = {}) { const { retryOnFailure = true, logs = true } = options; await this.waitForConnection(); return new Promise((resolve, reject) => { if (!this.socket?.connected) { reject(new Error("Socket client is not connected")); return; } if (logs) { logger.debug("Host authenticating..."); } this.socket.emit( "authenticate", authenticationData, async (verifyResult) => { if (verifyResult.valid == false) { this.authenticated = false; sendIPC("setAuthenticated", false); if (logs) { logger.error("Host not authenticated:", verifyResult.error); } if (retryOnFailure) { const retryResult = await this.authenticateWithOtp(); resolve(retryResult); return; } resolve({ valid: false, error: verifyResult.error }); return; } if (logs) { logger.info("Host authenticated."); } this.authenticated = true; sendIPC("setAuthenticated", true); sendIPC("setLoading", false); this.loading = false; if (this.host) { sendIPC("setHost", { ...this.host, online: false, state: { type: "offline" }, }); } this.host = { ...verifyResult.host, state: { type: "online" }, online: true, }; sendIPC("setHost", this.host); this.id = this.host._id; config.host = { id: this.id, authCode: this.host.authCode }; await saveConfig(config); this.sendDeviceInfo(); this.subscribeToObjectEvents(); this.subscribeToObjectUpdates({ objectType: "host", _id: this.id }); this.documentPrinterManager.reloadDocumentPrinters(); this.printerManager.reloadPrinters(); await this.fileManager.updateFiles(); resolve({ valid: true, host: this.host }); }, ); }); } handleHostAction(action, callback) { switch (action.type) { case "reloadPrinters": this.printerManager.updatePrinters().catch((error) => { logger.error("Failed to reload printers:", error); }); return; } callback({ success: true }); } async handleHostUpdate(data) { if (data._id != this.id) { return; } logger.debug("Handling host update for id:", data._id); const { online, state } = this.host; this.host = { ...this.host, ...data.object, online, state }; sendIPC("setHost", this.host); if (data.object?.authCode) { config.host = { id: this.id, authCode: data.object.authCode }; await saveConfig(config); } } handleObjectAction(data, callback) { logger.debug("Running object action...", data); const id = data._id; const objectType = data.objectType; const action = data.action; if (id == this.id && objectType == "host") { this.handleHostAction(action, callback); } if (objectType == "printer") { this.printerManager.handlePrinterAction(id, action, callback); } if (objectType == "documentPrinter") { this.documentPrinterManager.handleDocumentPrinterAction( id, action, callback, ); } } handleObjectUpdate(data, callback) { logger.debug( "Got object update for type:", data.objectType, " id:", data._id, ); if (data.objectType == "host") { this.handleHostUpdate(data); } if (data.objectType == "documentPrinter") { this.documentPrinterManager.handleDocumentPrinterUpdate( data._id, data, callback, ); } if (data.objectType == "printer") { this.printerManager.handlePrinterUpdate(data._id, data, callback); } } async handleConnect() { await stopWaiting(); logger.info("Connected to FarmControl Api."); this.connected = true; sendIPC("setConnected", true); this.loading = false; sendIPC("setLoading", false); // Clear any pending reconnect timeout if (this.reconnectTimeout) { clearTimeout(this.reconnectTimeout); this.reconnectTimeout = null; } const otpRequired = config.host?.id == undefined || config.host?.id == "" || config.host?.authCode == undefined || config.host?.authCode == ""; if (otpRequired) { logger.info("An OTP code is required to setup this host."); this.authenticated = false; sendIPC("setAuthenticated", false); this.authenticateWithOtp(); return; } this.authenticate({ id: config.host.id, authCode: config.host.authCode }); } async handleError(error) { await stopWaiting(); logger.error("Connection error:", error.message); this.loading = false; sendIPC("setLoading", false); this.connected = false; sendIPC("setConnected", false); this.authenticated = false; sendIPC("setAuthenticated", false); this.socket.disconnect(); this.scheduleReconnect(); } sendDeviceInfo() { logger.debug("Sending device info..."); const deviceInfo = getDeviceInfo(); this.socket.emit("updateHost", { host: { deviceInfo: deviceInfo }, }); } async listObjects({ objectType, populate, filter, sort, order, project, cached, }) { logger.trace("Listing objects...", { objectType, populate, filter, sort, order, project, cached, }); return new Promise((resolve, reject) => { this.socket.emit( "listObjects", { objectType, populate, filter, sort, order, project, cached, }, (result) => { if (result && result.error) { reject(new Error(result.error)); } else { logger.trace("Listed objects.", { objectType, populate, filter, sort, order, project, cached, length: result.length, }); resolve(result); } }, ); }); } async newObject({ objectType, newData }) { logger.debug("Creating object...", { objectType, newData, }); return new Promise((resolve, reject) => { this.socket.emit( "newObject", { objectType, newData, }, (result) => { if (result && result.error) { reject(new Error(result.error)); } else { logger.trace("Created object.", { objectType, newData, }); resolve(result); } }, ); }); } async editObject({ objectType, _id, populate, updateData, auditLog = true }) { logger.trace("Editing object...", { objectType, _id, populate, }); return new Promise((resolve, reject) => { this.socket.emit( "editObject", { objectType, _id, populate, updateData, auditLog, }, (result) => { if (result && result.error) { reject(new Error(result.error)); } else { logger.trace("Edited object.", { objectType, _id, populate, }); resolve(result); } }, ); }); } async getObject({ objectType, _id, populate }) { logger.debug("Getting object...", { objectType, _id, populate, }); return new Promise((resolve, reject) => { this.socket.emit( "getObject", { objectType, _id, populate, }, (result) => { if (result && result.error) { reject(new Error(result.error)); } else { logger.trace("Retreived object.", { objectType, _id, populate, }); resolve(result); } }, ); }); } async objectEvent({ objectType, _id, eventType, eventData }) { logger.trace("Sending object event...", { objectType, _id, eventType, eventData, }); return new Promise((resolve, reject) => { this.socket.emit("objectEvent", { objectType, _id, event: { type: eventType, data: eventData, }, }); }); } async renderTemplatePDF(templateData) { logger.debug("Rendering template PDF...", templateData); return new Promise((resolve, reject) => { this.socket.emit("renderTemplatePDF", templateData, (result) => { if (result && result.error) { logger.error("Failed to render template PDF:", result.error); reject(new Error(result.error)); } else { logger.trace("Template PDF rendered successfully."); resolve(result); } }); }); } async subscribeToObjectActions({ objectType, _id }) { logger.debug("Suscribing to object actions...", { objectType, _id, }); this.socket.emit("subscribeToObjectActions", { objectType, _id, }); } async subscribeToObjectUpdates({ objectType, _id }) { logger.debug("Suscribing to object updates...", { objectType, _id, }); this.socket.emit("subscribeToObjectUpdates", { objectType, _id, }); } async unsubscribeFromObjectUpdates({ objectType, _id }) { logger.debug("Unsuscribing from object updates...", { objectType, _id, }); this.socket.emit("unsubscribeFromObjectUpdates", { objectType, _id, }); } async subscribeToObjectEvent({ objectType, _id, eventType }) { logger.debug("Suscribing to object event...", { objectType, _id, eventType, }); this.socket.emit("subscribeToObjectEvent", { objectType, _id, eventType, }); } async unsubscribeFromObjectEvent({ objectType, _id, eventType }) { logger.debug("Unsuscribing from object event...", { objectType, _id, eventType, }); this.socket.emit("unsubscribeFromObjectEvent", { objectType, _id, eventType, }); } async handleHostChildUpdate(data) { if (data.parentType == "printer") { this.printerManager.reloadPrinters(); } else if (data.parentType == "documentPrinter") { this.documentPrinterManager.reloadDocumentPrinters(); } } async handleObjectEvent({ objectType, _id, event }) { logger.debug("Received object event...", { objectType, _id, event, }); const data = event.data; if ( (event.type == "childUpdate" || event.type == "childDelete" || event.type == "childNew") && objectType == "host" && _id == this.id ) { await this.handleHostChildUpdate(data); } } //-------------------------------------- RE-WRITE ENDS HERE --------------------------------------- async handleScanNetworkStart(data, callback) { if (this.scanner.scanning == false) { try { this.scanner = new WebSocketScanner({ maxThreads: 50 }); // Listen for found services this.scanner.on("serviceFound", (data) => { logger.info( `Found websocket service at ${data.hostname} (${data.ip})`, ); this.socket.emit("notify_scan_network_found", data); }); // Listen for scan progress this.scanner.on("scanProgress", ({ currentIP, progress }) => { logger.info( `Scanning ${currentIP} (${progress.toFixed(2)}% complete)`, ); this.socket.emit("notify_scan_network_progress", { currentIP: currentIP, progress: progress, }); }); // Start scanning on port logger.info( "Scanning network for websocket services on port:", data?.port || 7125, "using protocol:", data?.protocol || "ws", ); this.scanner .scanNetwork(data?.port || 7125, data?.protocol || "ws") .then((foundServices) => { logger.info("Scan complete. Found services:", foundServices); this.socket.emit("notify_scan_network_complete", foundServices); }) .catch((error) => { logger.error("Scan error:", error); this.socket.emit("notify_scan_network_complete", false); }); } catch (error) { logger.error("Scan error:", error); this.socket.emit("notify_scan_network_complete", false); } } } handleScanNetworkStop(callback) { if (this.scanner.scanning == true) { logger.info("Stopping network scan"); this.scanner.removeAllListeners("serviceFound"); this.scanner.removeAllListeners("scanProgress"); this.scanner.removeAllListeners("scanComplete"); this.scanner.stopScan(); callback(true); } else { logger.info("Scan not in progress"); callback(false); } } async handlePrinterObjectsQuery(data, callback) { logger.debug("Received printer.objects.query event:", data); try { const result = await this.printerManager.processPrinterCommand({ method: "printer.objects.query", params: data, }); if (callback) { callback(result); } } catch (e) { logger.error("Error processing printer objects query request:", e); if (callback) { callback({ error: e.message }); } } } async handleEmergencyStop(data, callback) { logger.debug("Received printer.gcode.script event:", data); try { const result = await this.printerManager.processPrinterCommand({ method: "printer.emergency_stop", params: data, }); if (callback) { callback(result); } } catch (e) { logger.error("Error processing gcode script request:", e); if (callback) { callback({ error: e.message }); } } } async handleFilamentStockLoad(data, callback) { logger.debug("Received printer.filamentstock.load event:", data); try { if (!data || !data.printerId) { throw new Error("Missing required printer ID"); } if (!data || !data.filamentStockId) { throw new Error("Missing required filament stock ID"); } // Get the printer client const printerClient = this.printerManager.getPrinterClient( data.printerId, ); if (!printerClient) { throw new Error(`Printer with ID ${data.printerId} not found`); } // Load the filament stock const result = await printerClient.loadFilamentStock( data.filamentStockId, ); if (callback) { callback(result); } } catch (e) { logger.error("Error processing filament load request:", e); if (callback) { callback({ error: e.message }); } } } async handleDisconnect() { logger.info("Disconnected from FarmControl Api."); await this.printerManager.closeAllConnections(); await this.documentPrinterManager.closeAllConnections(); this.connected = false; sendIPC("setConnected", false); this.authenticated = false; sendIPC("setAuthenticated", false); notPrompting(); this.scheduleReconnect(); } }