farmcontrol-server/src/socket/socketclient.js
Tom Butcher e2600f8242 Refactor logging in PrinterDatabase and PrinterManager, and streamline config saving in SocketClient
- Update logging in PrinterDatabase to correctly reference the printer state type.
- Remove redundant call to stopWaiting() in PrinterManager for cleaner code execution.
- Ensure config saving in SocketClient occurs unconditionally for improved reliability.
2026-07-27 23:15:20 +01:00

767 lines
20 KiB
JavaScript

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();
}
}