From ee9895d562ef9c0f93c8a925103cd8d28881e7da Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Mon, 27 Jul 2026 02:03:17 +0100 Subject: [PATCH] Update configuration and enhance CLI logging functionality - Change log level in development config from "debug" to "info" for improved clarity. - Add new dependencies: chalk and cli-spinners for enhanced CLI output. - Introduce command line utility functions for better logging and spinner management. - Refactor various components to utilize the new CLI logging methods, improving user feedback during operations. - Update package and lock files to reflect new dependencies. --- package.json | 4 +- pnpm-lock.yaml | 18 ++ src/commandlineutils.js | 203 ++++++++++++++++++ src/config.js | 23 +- src/documentprinter/documentprinterclient.js | 56 ++--- src/documentprinter/documentprintermanager.js | 5 +- .../interfaces/cupsinterface.js | 10 +- src/electron/ipc.js | 14 +- src/electron/window.js | 13 +- src/headless.js | 13 +- src/index.js | 188 +++++++--------- src/localserver/localserver.js | 20 +- src/printer/database.js | 13 +- src/printer/printerclient.js | 156 +++++++------- src/printer/printermanager.js | 68 +++--- src/socket/socketclient.js | 46 ++-- src/spinner.js | 122 +++++++++++ 17 files changed, 676 insertions(+), 296 deletions(-) create mode 100644 src/commandlineutils.js create mode 100644 src/spinner.js diff --git a/package.json b/package.json index 348558d..7878139 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,8 @@ "dependencies": { "axios": "^1.16.1", "canvas": "^3.2.3", + "chalk": "^6.0.0", + "cli-spinners": "^3.4.0", "etcd3": "^1.1.2", "express": "^5.2.1", "form-data": "^4.0.5", @@ -50,9 +52,9 @@ "antd": "^5.29.2", "concurrently": "^9.2.1", "cross-env": "^10.1.0", - "esbuild": "^0.25.12", "electron": "^38.7.1", "electron-builder": "^26.0.12", + "esbuild": "^0.25.12", "jest": "^30.4.2", "nodemon": "^3.1.14", "pkg": "^5.8.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08f5784..6154520 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,12 @@ importers: canvas: specifier: ^3.2.3 version: 3.2.3 + chalk: + specifier: ^6.0.0 + version: 6.0.0 + cli-spinners: + specifier: ^3.4.0 + version: 3.4.0 etcd3: specifier: ^1.1.2 version: 1.1.2 @@ -1765,6 +1771,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@6.0.0: + resolution: {integrity: sha512-2uNTXIuTTxk7ciZgAU1BQcgnchcG0xXnrs6jzkQfj9SsRa9M2s5zE8WT96hS6KmG4MzWHSrvH43DF1m4XRkrFg==} + engines: {node: '>=22'} + char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -1802,6 +1812,10 @@ packages: classnames@2.5.1: resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + cli-spinners@3.4.0: + resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==} + engines: {node: '>=18.20'} + cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} @@ -6402,6 +6416,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@6.0.0: {} + char-regex@1.0.2: {} chokidar@3.6.0: @@ -6445,6 +6461,8 @@ snapshots: classnames@2.5.1: {} + cli-spinners@3.4.0: {} + cliui@7.0.4: dependencies: string-width: 4.2.3 diff --git a/src/commandlineutils.js b/src/commandlineutils.js new file mode 100644 index 0000000..ad97c8a --- /dev/null +++ b/src/commandlineutils.js @@ -0,0 +1,203 @@ +import axios from "axios"; +import chalk from "chalk"; +import { LOCAL_SERVER_PORT } from "./localserver/localserver.js"; +import { + createSpinner, + registerSpinner, + stopWaiting as stopSpinnerWaiting, +} from "./spinner.js"; + +const LOG_SECTION_WIDTH = 55; + +let sharedCli = null; + +export function getCli() { + return sharedCli; +} + +export { startWaiting, stopWaiting } from "./spinner.js"; + +export function formatState(stateType) { + switch (stateType) { + case "online": + return chalk.green("Online"); + case "offline": + return chalk.dim("Offline"); + case "error": + return chalk.red("Error"); + case "inactive": + return chalk.dim("Inactive"); + case "connecting": + return chalk.yellow("Connecting"); + case "printing": + return chalk.blue("Printing"); + case "paused": + return chalk.yellow("Paused"); + case "cancelled": + return chalk.red("Cancelled"); + case "completed": + return chalk.green("Completed"); + default: + return chalk.dim("Unknown"); + } +} + +export function createCliLogger(logger) { + const spinner = createSpinner(); + registerSpinner(spinner); + + function withSpinnerStopped(fn) { + return async (...args) => { + await stopSpinnerWaiting(); + return fn(...args); + }; + } + + function logSectionHeader(title) { + const padding = LOG_SECTION_WIDTH - title.length; + const left = Math.floor(padding / 2); + const right = padding - left; + logger.info( + chalk.dim.cyan("-".repeat(left)) + + chalk.bold.white(title) + + chalk.dim.cyan("-".repeat(right)), + ); + } + + function logSectionFooter() { + logger.info(chalk.dim.cyan("-".repeat(LOG_SECTION_WIDTH))); + } + + const cli = { + waiting(message) { + spinner.start(message, logger); + }, + + async stopWaiting() { + await stopSpinnerWaiting(); + }, + + blank: withSpinnerStopped(() => { + process.stdout.write(" \n"); + }), + + success: withSpinnerStopped((message, ...rest) => { + logger.info(chalk.bold.white(`✔ `) + chalk.bold.green(message), ...rest); + }), + + failure: withSpinnerStopped((message, ...rest) => { + logger.error(chalk.bold.white(`✘ `) + chalk.bold.red(message), ...rest); + }), + + warn: withSpinnerStopped((message, ...rest) => { + logger.warn(chalk.yellow(message), ...rest); + }), + + info: withSpinnerStopped((message, ...rest) => { + logger.info(message, ...rest); + }), + + secure: withSpinnerStopped((message) => { + logger.info(chalk.cyan(`🔒 ${message}`)); + }), + + label: withSpinnerStopped((key, value) => { + logger.info(chalk.bold(key), value); + }), + + yesNo(value) { + return value ? chalk.green("Yes") : chalk.yellow("No"); + }, + + formatState, + + sectionHeader: withSpinnerStopped(logSectionHeader), + sectionFooter: withSpinnerStopped(logSectionFooter), + }; + + sharedCli = cli; + return cli; +} + +export function getArgValue(flag) { + const withEquals = process.argv.find((arg) => arg.startsWith(`${flag}=`)); + if (withEquals) { + return withEquals.slice(flag.length + 1); + } + + const index = process.argv.indexOf(flag); + if (index !== -1 && index + 1 < process.argv.length) { + return process.argv[index + 1]; + } + + return undefined; +} + +export const isHeadless = process.argv.includes("--headless"); +export const isInfo = process.argv.includes("--info"); +export const otpCode = getArgValue("--otp") || undefined; + +export function printerHostInfo(host, cli) { + let ref = host?._reference || "Unknown"; + + if (ref !== "Unknown") { + ref = `HST:${ref}`; + } + + cli.label("Reference:", ref); + cli.label("Name:", host?.name || "Unknown"); + + if (host?.tags?.length > 0) { + cli.label("Tags:", chalk.dim(`- `) + chalk.blue(`${host.tags[0]}`)); + for (let i = 0; i < host.tags.length - 1; i++) { + const tag = host.tags[i + 1]; + cli.info(chalk.dim(` - `) + chalk.blue(`${tag}`)); + } + } else { + cli.label("Tags:", chalk.dim("n/a")); + } + + cli.label("State:", cli.formatState(host?.state?.type)); +} + +export function printAuthCommand(cli) { + cli.info( + "Run '" + + chalk.bold.white("farmcontrol-server") + + " " + + chalk.bold.magentaBright("--otp") + + " " + + chalk.dim.white("") + + "' to authenticate.", + ); +} + +export async function checkRunning() { + try { + const response = await axios.get( + `http://127.0.0.1:${LOCAL_SERVER_PORT}/info`, + { timeout: 1000 }, + ); + if (response.data?.running === true) { + return response.data; + } + } catch { + return false; + } +} + +export async function authenticateWithOtp(code) { + try { + const response = await axios.post( + `http://127.0.0.1:${LOCAL_SERVER_PORT}/otpAuth`, + { otp: code }, + { timeout: 30000 }, + ); + return response.data; + } catch (err) { + if (err.response?.data) { + return err.response.data; + } + throw new Error("Failed to authenticate with OTP"); + } +} diff --git a/src/config.js b/src/config.js index e7ac26c..1376f49 100644 --- a/src/config.js +++ b/src/config.js @@ -3,6 +3,7 @@ import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import log4js from "log4js"; +import { startWaiting, stopWaiting } from "./spinner.js"; // Determine environment const __filename = fileURLToPath(import.meta.url); @@ -22,7 +23,7 @@ function getProductionConfigPath() { return path.join( process.env.PROGRAMDATA || "C:\\ProgramData", "farmcontrol-server", - "config.json" + "config.json", ); case "darwin": return path.join( @@ -30,7 +31,7 @@ function getProductionConfigPath() { "Library", "Application Support", "farmcontrol-server", - "config.json" + "config.json", ); case "linux": default: @@ -48,7 +49,7 @@ function getDevelopmentConfigPath() { return path.join( process.env.APPDATA || path.join(homeDir, "AppData", "Roaming"), "farmcontrol-server", - "config.json" + "config.json", ); case "darwin": return path.join( @@ -56,14 +57,14 @@ function getDevelopmentConfigPath() { "Library", "Application Support", "farmcontrol-server", - "config.json" + "config.json", ); default: return path.join( homeDir, ".config", "farmcontrol-server", - "config.json" + "config.json", ); } } @@ -104,7 +105,7 @@ export function loadConfig() { try { if (!fs.existsSync(CONFIG_PATH)) { logger.info( - `Configuration file not found at ${CONFIG_PATH}. Creating a new one.` + `Configuration file not found at ${CONFIG_PATH}. Creating a new one.`, ); const configDir = path.dirname(CONFIG_PATH); if (!fs.existsSync(configDir)) { @@ -113,7 +114,7 @@ export function loadConfig() { fs.writeFileSync( CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2), - "utf8" + "utf8", ); } @@ -122,7 +123,7 @@ export function loadConfig() { if (!config[NODE_ENV]) { throw new Error( - `Configuration for environment '${NODE_ENV}' not found in config.json` + `Configuration for environment '${NODE_ENV}' not found in config.json`, ); } @@ -134,9 +135,9 @@ export function loadConfig() { } // Save config file -export function saveConfig(newConfig) { +export async function saveConfig(newConfig) { + startWaiting("Saving...", logger); try { - logger.info("Saving..."); let config = {}; if (fs.existsSync(CONFIG_PATH)) { const configData = fs.readFileSync(CONFIG_PATH, "utf8"); @@ -153,8 +154,10 @@ export function saveConfig(newConfig) { // Write back to file with 2-space indentation fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf8"); + await stopWaiting(); logger.info(`Configuration for '${NODE_ENV}' saved successfully.`); } catch (err) { + await stopWaiting(); logger.error("Error saving config:", err); throw err; } diff --git a/src/documentprinter/documentprinterclient.js b/src/documentprinter/documentprinterclient.js index 8df665f..040a863 100644 --- a/src/documentprinter/documentprinterclient.js +++ b/src/documentprinter/documentprinterclient.js @@ -1,6 +1,7 @@ // documentprinterclient.js - Handles connection to a single document printer import { loadConfig } from "../config.js"; import log4js from "log4js"; +import { startWaiting, stopWaiting } from "../spinner.js"; import CupsPrinterInterface from "./interfaces/cupsinterface.js"; import ReceiptInterface from "./interfaces/receiptinterface.js"; import { sendIPC } from "../electron/ipc.js"; @@ -138,9 +139,6 @@ export class DocumentPrinterClient { async connect() { if (this.active == false) { - logger.info( - `Document printer ${this.id} is not active, skipping connection`, - ); this.shouldReconnect = false; this.isOnline = false; this.state = { type: "inactive" }; @@ -234,29 +232,37 @@ export class DocumentPrinterClient { } async initialize() { - logger.info("Running document printer initialization..."); - this.state = { type: "initializing", message: null }; - this.isOnline = true; - this.connectedAt = new Date(); - await this.updateDocumentPrinterState(); - if (this.printerInterface && this.printerInterface.initialize) { - const result = await this.printerInterface.initialize(); - if (result.error) { - logger.error( - `Error initializing document printer ${this.documentPrinter.name}:`, - result.error, - ); - this.state = { type: "offline", message: result.error }; - await this.updateDocumentPrinterState(); - return false; - } - this.state = { type: "standby", message: null }; + startWaiting("Running document printer initialization...", logger); + try { + this.state = { type: "initializing", message: null }; + this.isOnline = true; + this.connectedAt = new Date(); await this.updateDocumentPrinterState(); - this.eventUpdateInterval = setInterval( - this.handleEventUpdate.bind(this), - 3000, - ); - return true; + if (this.printerInterface && this.printerInterface.initialize) { + const result = await this.printerInterface.initialize(); + if (result.error) { + logger.error( + `Error initializing document printer ${this.documentPrinter.name}:`, + result.error, + ); + this.state = { type: "offline", message: result.error }; + await this.updateDocumentPrinterState(); + await stopWaiting(); + return false; + } + this.state = { type: "standby", message: null }; + await this.updateDocumentPrinterState(); + this.eventUpdateInterval = setInterval( + this.handleEventUpdate.bind(this), + 3000, + ); + await stopWaiting(); + return true; + } + await stopWaiting(); + } catch (error) { + await stopWaiting(); + throw error; } } diff --git a/src/documentprinter/documentprintermanager.js b/src/documentprinter/documentprintermanager.js index 40e0bd9..1b67a93 100644 --- a/src/documentprinter/documentprintermanager.js +++ b/src/documentprinter/documentprintermanager.js @@ -2,6 +2,7 @@ import { loadConfig } from "../config.js"; import log4js from "log4js"; import { sendIPC } from "../electron/ipc.js"; import { DocumentPrinterClient } from "./documentprinterclient.js"; +import { startWaiting, stopWaiting } from "../spinner.js"; const config = loadConfig(); @@ -16,7 +17,7 @@ export class DocumentPrinterManager { } async reloadDocumentPrinters() { - logger.info("Reloading document printers..."); + startWaiting("Reloading document printers...", logger); try { this.documentPrinters = await this.socketClient.listObjects({ objectType: "documentPrinter", @@ -62,7 +63,9 @@ export class DocumentPrinterManager { ); } } + await stopWaiting(); } catch (error) { + await stopWaiting(); logger.error("Failed to update document printers:", error); this.documentPrinters = []; } diff --git a/src/documentprinter/interfaces/cupsinterface.js b/src/documentprinter/interfaces/cupsinterface.js index 2530bb5..29bc486 100644 --- a/src/documentprinter/interfaces/cupsinterface.js +++ b/src/documentprinter/interfaces/cupsinterface.js @@ -2,6 +2,7 @@ import log4js from "log4js"; import { loadConfig } from "../../config.js"; import ipp from "ipp"; +import { startWaiting, stopWaiting } from "../../spinner.js"; const config = loadConfig(); const logger = log4js.getLogger("CUPS Printer Interface"); @@ -119,16 +120,18 @@ export default class CupsInterface { } async disconnect() { - logger.info(`Disconnecting from CUPS printer...`); + startWaiting("Disconnecting from CUPS printer...", logger); this.isConnected = false; this.cupsPrinter = null; + await stopWaiting(); return { success: true }; } async initialize() { - logger.info(`Initializing CUPS printer...`); + startWaiting("Initializing CUPS printer...", logger); if (!this.isConnected || !this.cupsPrinter) { + await stopWaiting(); return { error: "Printer is not connected" }; } @@ -157,14 +160,17 @@ export default class CupsInterface { // Printer states: 3 = idle, 4 = processing, 5 = stopped if (printerState === 5) { logger.warn(`Printer ${this.name} is stopped: ${stateMessage}`); + await stopWaiting(); return { error: `Printer is stopped: ${stateMessage}` }; } logger.info( `CUPS printer ${this.name} initialized successfully. State: ${printerState}` ); + await stopWaiting(); return true; } catch (error) { + await stopWaiting(); logger.error(`Failed to initialize CUPS printer ${this.name}:`, error); return { error: error.message || "Failed to initialize CUPS printer" }; } diff --git a/src/electron/ipc.js b/src/electron/ipc.js index caec29a..750f5bf 100644 --- a/src/electron/ipc.js +++ b/src/electron/ipc.js @@ -1,5 +1,6 @@ import log4js from "log4js"; import { notPrompting } from "../utils.js"; +import { startWaiting, stopWaiting } from "../spinner.js"; const logger = log4js.getLogger("IPC"); let mainWindow = null; @@ -22,8 +23,8 @@ export async function setupIPC() { return; } - ipcMain.on("getData", (event) => { - logger.info("Getting data..."); + ipcMain.on("getData", async () => { + startWaiting("Getting data...", logger); try { // Get the global socket client instance const socketClient = global.socketClient; @@ -35,6 +36,7 @@ export async function setupIPC() { sendIPC("setHost", {}); sendIPC("setPrinters", []); sendIPC("setDocumentPrinters", []); + await stopWaiting(); return; } @@ -46,8 +48,9 @@ export async function setupIPC() { sendIPC("setPrinters", socketClient.printerManager.printers || []); sendIPC( "setDocumentPrinters", - socketClient.documentPrinterManager.documentPrinters || [] + socketClient.documentPrinterManager.documentPrinters || [], ); + await stopWaiting(); } catch (error) { logger.error("Error getting printer data:", error); sendIPC("setAuthenticated", false); @@ -55,6 +58,7 @@ export async function setupIPC() { sendIPC("setLoading", false); sendIPC("setHost", {}); sendIPC("setPrinters", []); + await stopWaiting(); } }); @@ -83,7 +87,7 @@ export async function setupIPC() { // OTP authentication handler ipcMain.on("authenticateOTP", async (event, otp) => { - logger.info("Authenticating with OTP..."); + startWaiting("Authenticating with OTP...", logger); try { const socketClient = global.socketClient; if (socketClient) { @@ -92,7 +96,9 @@ export async function setupIPC() { } else { logger.error("Socket client not available for OTP authentication"); } + await stopWaiting(); } catch (error) { + await stopWaiting(); logger.error("Error during OTP authentication:", error); } }); diff --git a/src/electron/window.js b/src/electron/window.js index 555f35b..aeaf48a 100644 --- a/src/electron/window.js +++ b/src/electron/window.js @@ -4,6 +4,7 @@ import path from "path"; import { loadConfig } from "../config.js"; import log4js from "log4js"; import { fileURLToPath } from "url"; +import { startWaiting, stopWaiting } from "../spinner.js"; // Load configuration const config = loadConfig(); @@ -21,7 +22,7 @@ export async function createElectronWindow() { } catch (error) { logger.warn( "Electron not available, skipping window creation. Error:", - error + error, ); return; } @@ -49,8 +50,8 @@ export async function createElectronWindow() { process.env.NODE_ENV === "development" ? path.join(__dirname, "preload.js") : isRunningFromBuild - ? path.join(__dirname, "preload.js") - : path.join(__dirname, "..", "build", "electron", "preload.js"); + ? path.join(__dirname, "preload.js") + : path.join(__dirname, "..", "build", "electron", "preload.js"); const win = new BrowserWindow({ width: 900, @@ -71,7 +72,7 @@ export async function createElectronWindow() { logger.info("Preload Script", preloadPath); if (process.env.NODE_ENV === "development") { - logger.info("Loading development url..."); + startWaiting("Loading development url...", logger); win.loadURL("http://localhost:5287"); // Vite dev server } else { // In production, the built files will be in the build/electron directory @@ -79,12 +80,14 @@ export async function createElectronWindow() { ? path.join(__dirname, "index.html") : path.join(__dirname, "..", "build", "electron", "index.html"); + startWaiting("Loading production file...", logger); logger.info("Loading production file:", htmlPath); win.loadFile(htmlPath); } // Resolve the promise when the window is ready - win.webContents.on("did-finish-load", () => { + win.webContents.on("did-finish-load", async () => { + await stopWaiting(); resolve(win); }); } diff --git a/src/headless.js b/src/headless.js index fd2d96e..0ce8ccc 100644 --- a/src/headless.js +++ b/src/headless.js @@ -1,9 +1,8 @@ process.env.FC_HEADLESS_ENTRY = "1"; -import("./index.js") - .then(({ init }) => - init({ headless: true }).catch((err) => { - console.error(err.message); - process.exit(1); - }), - ); +import("./index.js").then(({ init }) => + init({ headless: true }).catch((err) => { + console.error(err.message); + process.exit(1); + }), +); diff --git a/src/index.js b/src/index.js index 3e61e26..0666ce8 100644 --- a/src/index.js +++ b/src/index.js @@ -1,163 +1,130 @@ import { fileURLToPath } from "node:url"; import path from "node:path"; import { loadConfig } from "./config.js"; -import axios from "axios"; import log4js from "log4js"; import { createElectronWindow } from "./electron/window.js"; import { setupIPC } from "./electron/ipc.js"; -import { LOCAL_SERVER_PORT, LocalServer } from "./localserver/localserver.js"; +import { LocalServer } from "./localserver/localserver.js"; import { SocketClient } from "./socket/socketclient.js"; +import { + authenticateWithOtp, + checkRunning, + createCliLogger, + isHeadless, + isInfo, + otpCode, + printAuthCommand, + printerHostInfo, + startWaiting, + stopWaiting, +} from "./commandlineutils.js"; if (!process.env.NODE_ENV) { process.env.NODE_ENV = "production"; } -// Load configuration const config = loadConfig(); const logger = log4js.getLogger("App"); logger.level = config.logLevel; -const isHeadless = process.argv.includes("--headless"); -const isInfo = process.argv.includes("--info"); +const cli = createCliLogger(logger); -const printerHostInfo = (host) => { - var ref = host?._reference || "Unknown"; - - if (ref != "Unknown") { - ref = "HST:" + ref; - } - - logger.info("Reference:", ref); - logger.info("Name:", host?.name || "Unknown"); - if (host?.tags?.length > 0) { - logger.info("Tags: -", host?.tags?.[0]); - for (var i = 0; i < host?.tags?.length - 1; i++) { - const tag = host?.tags[i + 1]; - logger.info(" -", tag); - } - } else { - logger.info("Tags: n/a"); - } - logger.info("State:", host?.state?.type || "Unknown"); -}; - -function getArgValue(flag) { - const withEquals = process.argv.find((arg) => arg.startsWith(`${flag}=`)); - if (withEquals) { - return withEquals.slice(flag.length + 1); - } - - const index = process.argv.indexOf(flag); - if (index !== -1 && index + 1 < process.argv.length) { - return process.argv[index + 1]; - } - - return undefined; -} - -const otpCode = getArgValue("--otp") || undefined; - -async function checkRunning() { - try { - const response = await axios.get( - `http://127.0.0.1:${LOCAL_SERVER_PORT}/info`, - { timeout: 1000 }, - ); - if (response.data?.running === true) { - return response.data; - } - } catch (err) { - return false; - } -} - -async function authenticateWithOtp(otpCode) { - try { - const response = await axios.post( - `http://127.0.0.1:${LOCAL_SERVER_PORT}/otpAuth`, - { otp: otpCode }, - { timeout: 30000 }, - ); - return response.data; - } catch (err) { - if (err.response?.data) { - return err.response.data; - } - throw new Error("Failed to authenticate with OTP"); - } -} export async function init(options = {}) { const headless = options.headless ?? isHeadless; - - logger.info("⌛ Checking if Farm Control Server is running..."); - - const runningServer = await checkRunning(); + cli.blank(); + startWaiting("Checking if Farm Control Server is running...", logger); + let runningServer; + try { + runningServer = await checkRunning(); + } catch (err) { + await stopWaiting(); + throw err; + } if (otpCode != undefined || isInfo == true) { - logger.info("✔ Farm Control Server is running."); + cli.success("Farm Control Server is running."); } if (isInfo) { const buildNumber = runningServer.buildNumber ?? runningServer.build ?? "n/a"; - - logger.info("-----FarmControl Server-----"); - logger.info( + cli.blank(); + cli.sectionHeader("FarmControl Server"); + cli.label( "Version:", runningServer.version ? `v${runningServer.version}` : "n/a", ); - logger.info("Build:", buildNumber === "dev" ? "dev" : `b${buildNumber}`); - logger.info( - "Authenticated:", - runningServer.authenticated == true ? "Yes" : "No", - ); - logger.info("------------Host------------"); - printerHostInfo(runningServer.host); - logger.info("----------------------------"); + cli.label("Build:", buildNumber === "dev" ? "dev" : `b${buildNumber}`); + cli.label("Authenticated:", cli.yesNo(runningServer.authenticated == true)); + cli.sectionFooter(); + cli.blank(); + if (runningServer.authenticated == false) { + cli.sectionHeader("Authenticate"); + printAuthCommand(cli); + cli.sectionFooter(); + } else { + cli.sectionHeader("Host"); + printerHostInfo(runningServer.host, cli); + cli.sectionFooter(); + } + cli.blank(); return runningServer; } if (otpCode != undefined) { - logger.info("🔒 Authenticating with OTP..."); - const authenticatedServer = await authenticateWithOtp(otpCode); + cli.blank(); + startWaiting("Authenticating with OTP...", logger); + let authenticatedServer; + try { + authenticatedServer = await authenticateWithOtp(otpCode); + await stopWaiting(); + } catch (err) { + await stopWaiting(); + throw err; + } if (authenticatedServer.valid == false) { - logger.error( - "✘ Failed to authenticate with OTP -", - authenticatedServer.error, - ); + cli.failure("Failed to authenticate!"); + cli.failure(authenticatedServer.error); } else { - logger.info("✔ Authenticated with OTP."); - logger.info("------------Host------------"); - printerHostInfo(authenticatedServer.host); - logger.info("----------------------------"); + cli.success("Authenticated with OTP."); + cli.blank(); + cli.sectionHeader("Host"); + printerHostInfo(authenticatedServer.host, cli); + cli.sectionFooter(); + cli.blank(); return authenticatedServer; } + cli.blank(); return authenticatedServer; } if (runningServer != false) { - logger.warn("✘ Farm Control Server is already running."); + cli.warn("✘ Farm Control Server is already running."); + cli.blank(); return runningServer; } if (!headless) { - // Create Electron window first - logger.info("Creating electron window..."); - await createElectronWindow().catch((err) => { - logger.warn("Failed to create Electron window:", err); - }); + startWaiting("Creating electron window...", logger); + try { + await createElectronWindow().catch((err) => { + cli.warn("Failed to create Electron window:", err); + }); + await stopWaiting(); + } catch (err) { + await stopWaiting(); + throw err; + } - // Setup IPC communication after window is created setupIPC().catch((err) => { - logger.warn("Failed to setup IPC:", err); + cli.warn("Failed to setup IPC:", err); }); } else { - logger.info("Running in headless mode. Skipping window creation."); + cli.info("Running in headless mode. Skipping window creation."); } const socketClient = new SocketClient(); - // Make socket client globally accessible for IPC handlers global.socketClient = socketClient; const localServer = new LocalServer(socketClient); @@ -165,10 +132,12 @@ export async function init(options = {}) { socketClient.connect(); - process.on("SIGINT", () => { - logger.info("Shutting down..."); + process.on("SIGINT", async () => { + startWaiting("Shutting down...", logger); + cli.blank(); socketClient.disconnect(); localServer.stop(); + await stopWaiting(); process.exit(0); }); } @@ -180,6 +149,7 @@ const isMainModule = if (isMainModule && !process.env.FC_HEADLESS_ENTRY) { init().catch((err) => { logger.error(err.message); + cli.blank(); process.exit(1); }); } diff --git a/src/localserver/localserver.js b/src/localserver/localserver.js index 062dd5f..c3021cc 100644 --- a/src/localserver/localserver.js +++ b/src/localserver/localserver.js @@ -3,6 +3,7 @@ import log4js from "log4js"; import { notPrompting } from "../utils.js"; import { loadConfig } from "../config.js"; import { getServerVersionInfo } from "../serverVersion.js"; +import { startWaiting, stopWaiting } from "../spinner.js"; export const LOCAL_SERVER_PORT = 47291; @@ -41,13 +42,22 @@ export class LocalServer { } try { - logger.info("Authenticating with OTP via local server..."); - notPrompting(); - const result = await this.socketClient.authenticateWithOtp(otp, { - retryOnFailure: false, - }); + startWaiting("Authenticating with OTP via local server...", logger); + let result; + try { + notPrompting(); + result = await this.socketClient.authenticateWithOtp(otp, { + retryOnFailure: false, + logs: false, + }); + await stopWaiting(); + } catch (err) { + await stopWaiting(); + throw err; + } if (!result.valid) { + logger.error("OTP authentication failed:", result.error); res.status(401).json(result); return; } diff --git a/src/printer/database.js b/src/printer/database.js index 64986d3..eef7e89 100644 --- a/src/printer/database.js +++ b/src/printer/database.js @@ -3,7 +3,8 @@ import log4js from "log4js"; import NodeCache from "node-cache"; import { loadConfig } from "../config.js"; import { sendIPC } from "../electron/ipc.js"; - +import chalk from "chalk"; +import { formatState } from "../commandlineutils.js"; const config = loadConfig(); const logger = log4js.getLogger("Printer Database"); logger.level = config.logLevel; @@ -15,7 +16,6 @@ export class PrinterDatabase { this.id = this.printer._id; // Initialize cache with 30 second TTL this.printerCache = new NodeCache({ stdTTL: 30 }); - logger.info("Initialized PrinterDatabase with socket manager"); } async getPrinter() { @@ -146,12 +146,9 @@ export class PrinterDatabase { connectedAt, }); - logger.info(`Updated printer ${this.printer.name} state:`, { - type: state.type, - progress: state.progress, - online, - previousState: updatedPrinter.state, - }); + logger.info( + `Updated printer: ${chalk.bold.white(this.printer.name)}, State: ${chalk.bold.white(formatState(this.printer.state.type))}${state?.progress ? ` (${state.progress})` : ""} (${online ? chalk.green("Online") : chalk.dim.white("Offline")})`, + ); return updatedPrinter; } catch (error) { diff --git a/src/printer/printerclient.js b/src/printer/printerclient.js index a5c7044..76f6f1f 100644 --- a/src/printer/printerclient.js +++ b/src/printer/printerclient.js @@ -5,6 +5,7 @@ import { loadConfig } from "../config.js"; import { PrinterDatabase } from "./database.js"; import { PrinterFileManager } from "./printerfilemanager.js"; import log4js from "log4js"; +import { startWaiting, stopWaiting } from "../spinner.js"; import axios from "axios"; import FormData from "form-data"; import _ from "lodash"; @@ -151,7 +152,6 @@ export class PrinterClient { async connect() { if (this.active == false) { - logger.info(`Printer ${this.id} is not active, skipping connection`); this.shouldReconnect = false; this.isOnline = false; this.state = { type: "inactive" }; @@ -238,96 +238,108 @@ export class PrinterClient { } async initialize() { - logger.info("Running printer initialization..."); - this.state = { type: "initializing", message: null }; - this.isOnline = true; - this.connectedAt = new Date(); - await this.getInfo(); - await this.getFiles(); - await this.getCurrentFilament(); - await this.loadCurrentFilament(); - await this.updateSubscriptions(); - await this.getPrinterState(); - await this.syncSubJobs(); - this.eventUpdateInterval = setInterval( - this.handleEventUpdate.bind(this), - 500, - ); + startWaiting("Running printer initialization...", logger); + try { + this.state = { type: "initializing", message: null }; + this.isOnline = true; + this.connectedAt = new Date(); + await this.getInfo(); + await this.getFiles(); + await this.getCurrentFilament(); + await this.loadCurrentFilament(); + await this.updateSubscriptions(); + await this.getPrinterState(); + await this.syncSubJobs(); + this.eventUpdateInterval = setInterval( + this.handleEventUpdate.bind(this), + 500, + ); + await stopWaiting(); + } catch (error) { + await stopWaiting(); + throw error; + } } async getInfo() { - logger.info("Getting printer info..."); + startWaiting("Getting printer info...", logger); try { - // Get server info - const serverResult = await this.jsonRpc.callMethod("server.info"); - this.isOnline = true; - this.klippyState = { type: serverResult.klippy_state }; - logger.info( - "Server:", - `Moonraker ${serverResult.moonraker_version} (${this.printer.name})`, - `State: ${this.klippyState.type}`, - ); - try { - const klippyResult = await this.jsonRpc.callMethod("printer.info"); + // Get server info + const serverResult = await this.jsonRpc.callMethod("server.info"); + this.isOnline = true; + this.klippyState = { type: serverResult.klippy_state }; logger.info( - `Klippy info for ${this.printer.name}: ${klippyResult.hostname}, ${klippyResult.software_version}`, - ); - // Update firmware version in database - await this.database.updatePrinterFirmware( - klippyResult.software_version, - ); - logger.info( - `Updated firmware version for ${this.printer.name} to ${klippyResult.software_version}`, + "Server:", + `Moonraker ${serverResult.moonraker_version} (${this.printer.name})`, + `State: ${this.klippyState.type}`, ); - if (klippyResult.state === "error" && klippyResult.state_message) { - logger.error( - `Klippy error for ${this.printer.name}: ${klippyResult.state_message}`, - this.database.addAlert({ - code: "klippyError", - type: "error", - message: klippyResult.state_message, - actions: ["restart", "restartFirmware"], - }), + try { + const klippyResult = await this.jsonRpc.callMethod("printer.info"); + logger.info( + `Klippy info for ${this.printer.name}: ${klippyResult.hostname}, ${klippyResult.software_version}`, ); - } - - if (klippyResult.state === "shutdown" && klippyResult.state_message) { - logger.error( - `Klippy error for ${this.printer.name}: ${klippyResult.state_message}`, - this.database.addAlert({ - code: "klippyError", - type: "error", - message: klippyResult.state_message, - actions: ["restart", "restartFirmware"], - }), + // Update firmware version in database + await this.database.updatePrinterFirmware( + klippyResult.software_version, + ); + logger.info( + `Updated firmware version for ${this.printer.name} to ${klippyResult.software_version}`, ); - } - if (klippyResult.state === "startup" && klippyResult.state_message) { - logger.warn( - `Klippy startup message for ${this.printer.name}: ${klippyResult.state_message}`, - this.database.addAlert({ - type: "info", - code: "klippyStartup", - message: klippyResult.state_message, - priority: 8, - timestamp: new Date(), - }), + if (klippyResult.state === "error" && klippyResult.state_message) { + logger.error( + `Klippy error for ${this.printer.name}: ${klippyResult.state_message}`, + this.database.addAlert({ + code: "klippyError", + type: "error", + message: klippyResult.state_message, + actions: ["restart", "restartFirmware"], + }), + ); + } + + if (klippyResult.state === "shutdown" && klippyResult.state_message) { + logger.error( + `Klippy error for ${this.printer.name}: ${klippyResult.state_message}`, + this.database.addAlert({ + code: "klippyError", + type: "error", + message: klippyResult.state_message, + actions: ["restart", "restartFirmware"], + }), + ); + } + + if (klippyResult.state === "startup" && klippyResult.state_message) { + logger.warn( + `Klippy startup message for ${this.printer.name}: ${klippyResult.state_message}`, + this.database.addAlert({ + type: "info", + code: "klippyStartup", + message: klippyResult.state_message, + priority: 8, + timestamp: new Date(), + }), + ); + } + } catch (error) { + logger.error( + `Error getting Klippy info for ${this.printer.name}:`, + error, ); } } catch (error) { logger.error( - `Error getting Klippy info for ${this.printer.name}:`, + `Error getting server info for ${this.printer.name}:`, error, ); } + await stopWaiting(); } catch (error) { - logger.error( - `Error getting server info for ${this.printer.name}:`, - error, - ); + await stopWaiting(); + throw error; } } diff --git a/src/printer/printermanager.js b/src/printer/printermanager.js index 0dcdfdc..bd06bf6 100644 --- a/src/printer/printermanager.js +++ b/src/printer/printermanager.js @@ -3,6 +3,7 @@ import { PrinterClient } from "./printerclient.js"; import { loadConfig } from "../config.js"; import log4js from "log4js"; import { sendIPC } from "../electron/ipc.js"; +import { startWaiting, stopWaiting } from "../spinner.js"; // Load configuration const config = loadConfig(); @@ -17,7 +18,7 @@ export class PrinterManager { } async reloadPrinters() { - logger.info("Reloading printers..."); + startWaiting("Reloading printers...", logger); try { this.printers = await this.socketClient.listObjects({ objectType: "printer", @@ -64,7 +65,9 @@ export class PrinterManager { logger.debug("Printers added:", addedPrintersCount); logger.debug("Printers removed:", removedPrintersCount); + await stopWaiting(); } catch (error) { + await stopWaiting(); logger.error("Failed to update printers:", error); this.printers = []; } @@ -155,39 +158,44 @@ export class PrinterManager { // Close all printer connections async closeAllConnections() { - logger.info( + startWaiting( `Closing all printer connections... current count: ${this.printerClients.size}`, + logger, ); + try { + const clients = Array.from(this.printerClients.values()); - // Take a snapshot so any mutations during disconnects don't affect iteration - const clients = Array.from(this.printerClients.values()); - - for (const printerClient of clients) { - try { - // Ensure we never auto-reconnect after a manual close-all - printerClient.shouldReconnect = false; - await printerClient.disconnect(); - logger.info( - `Disconnected printer client ${printerClient?.id || "unknown"}`, - ); - } catch (error) { - logger.error( - `Failed to disconnect printer client ${ - printerClient?.id || "unknown" - }:`, - error, - ); + for (const printerClient of clients) { + try { + // Ensure we never auto-reconnect after a manual close-all + printerClient.shouldReconnect = false; + await printerClient.disconnect(); + logger.info( + `Disconnected printer client ${printerClient?.id || "unknown"}`, + ); + } catch (error) { + logger.error( + `Failed to disconnect printer client ${ + printerClient?.id || "unknown" + }:`, + error, + ); + } } + + console.log("Printer clients:", this.printerClients); + + // Clear local references so no stale clients remain + this.printerClients.clear(); + this.printers = []; + + logger.info( + `All printer connections closed. Remaining clients: ${this.printerClients.size}`, + ); + await stopWaiting(); + } catch (error) { + await stopWaiting(); + throw error; } - - console.log("Printer clients:", this.printerClients); - - // Clear local references so no stale clients remain - this.printerClients.clear(); - this.printers = []; - - logger.info( - `All printer connections closed. Remaining clients: ${this.printerClients.size}`, - ); } } diff --git a/src/socket/socketclient.js b/src/socket/socketclient.js index a089575..a8fecd7 100644 --- a/src/socket/socketclient.js +++ b/src/socket/socketclient.js @@ -9,6 +9,7 @@ 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(); @@ -51,9 +52,9 @@ export class SocketClient { this.reconnectTimeout = null; } - logger.info("Will attempt to reconnect in 3 seconds..."); - this.reconnectTimeout = setTimeout(() => { - logger.info("Attempting to reconnect..."); + startWaiting("Will attempt to reconnect in 3 seconds...", logger); + this.reconnectTimeout = setTimeout(async () => { + await stopWaiting(); this.connect(); }, 3000); } @@ -94,9 +95,10 @@ export class SocketClient { }); } - connect() { + async connect() { try { - logger.info(`Connecting to Socket.IO server: ${config.url}`); + await stopWaiting(); + startWaiting(`Connecting to Socket.IO server: ${config.url}`, logger); this.socket = io(config.url, { auth: { type: "host" }, reconnection: false, @@ -110,6 +112,7 @@ export class SocketClient { sendIPC("setAuthenticated", false); this.setupSocketEventHandlers(); } catch (error) { + await stopWaiting(); logger.error("Failed to create Socket.IO connection:", error); } } @@ -150,18 +153,18 @@ export class SocketClient { } async authenticateWithOtp(otp = undefined, options = {}) { - const { retryOnFailure = otp == undefined } = options; + const { retryOnFailure = otp == undefined, logs = true } = options; if (otp == undefined) { otp = await askOtp(); } await this.waitForConnection(); - return this.authenticate({ otp }, { retryOnFailure }); + return this.authenticate({ otp }, { retryOnFailure, logs }); } async authenticate(authenticationData, options = {}) { - const { retryOnFailure = true } = options; + const { retryOnFailure = true, logs = true } = options; await this.waitForConnection(); @@ -171,7 +174,9 @@ export class SocketClient { return; } - logger.debug("Host authenticating..."); + if (logs) { + logger.debug("Host authenticating..."); + } this.socket.emit( "authenticate", authenticationData, @@ -179,7 +184,9 @@ export class SocketClient { if (verifyResult.valid == false) { this.authenticated = false; sendIPC("setAuthenticated", false); - logger.error("Host not authenticated:", verifyResult.error); + if (logs) { + logger.error("Host not authenticated:", verifyResult.error); + } if (retryOnFailure) { const retryResult = await this.authenticateWithOtp(); @@ -191,7 +198,9 @@ export class SocketClient { return; } - logger.info("Host authenticated."); + if (logs) { + logger.info("Host authenticated."); + } this.authenticated = true; sendIPC("setAuthenticated", true); sendIPC("setLoading", false); @@ -212,7 +221,9 @@ export class SocketClient { this.id = this.host._id; config.host = { id: this.id, authCode: this.host.authCode }; - saveConfig(config); + if (logs) { + await saveConfig(config); + } this.sendDeviceInfo(); this.subscribeToObjectEvents(); this.subscribeToObjectUpdates({ objectType: "host", _id: this.id }); @@ -226,7 +237,6 @@ export class SocketClient { } handleHostAction(action, callback) { - console.log("RUNNING HOST ACTION"); switch (action.type) { case "reloadPrinters": this.printerManager.updatePrinters().catch((error) => { @@ -237,7 +247,7 @@ export class SocketClient { callback({ success: true }); } - handleHostUpdate(data) { + async handleHostUpdate(data) { if (data._id != this.id) { return; } @@ -249,7 +259,7 @@ export class SocketClient { if (data.object?.authCode) { config.host = { id: this.id, authCode: data.object.authCode }; - saveConfig(config); + await saveConfig(config); } } @@ -298,7 +308,8 @@ export class SocketClient { } } - handleConnect() { + async handleConnect() { + await stopWaiting(); logger.info("Connected to FarmControl Api."); this.connected = true; sendIPC("setConnected", true); @@ -326,7 +337,8 @@ export class SocketClient { this.authenticate({ id: config.host.id, authCode: config.host.authCode }); } - handleError(error) { + async handleError(error) { + await stopWaiting(); logger.error("Connection error:", error.message); this.loading = false; sendIPC("setLoading", false); diff --git a/src/spinner.js b/src/spinner.js new file mode 100644 index 0000000..0df4bb1 --- /dev/null +++ b/src/spinner.js @@ -0,0 +1,122 @@ +import chalk from "chalk"; +import cliSpinners from "cli-spinners"; +import log4js from "log4js"; +import { writeSync } from "node:fs"; +import { colouredLayout } from "log4js/lib/layouts.js"; + +let spinner = null; +let consoleLogPatched = false; + +function patchConsoleLog() { + if (consoleLogPatched) { + return; + } + const originalLog = console.log.bind(console); + console.log = (...args) => { + spinner?.stopSync(); + originalLog(...args); + }; + consoleLogPatched = true; +} + +function formatLogPrefix(logger) { + const categoryName = logger?.category ?? "App"; + return colouredLayout({ + startTime: new Date(), + level: log4js.levels.INFO, + categoryName, + data: [], + }); +} + +function clearSpinnerLineSync() { + if (!process.stderr.isTTY) { + return; + } + + try { + writeSync(process.stderr.fd, "\r\x1b[2K"); + } catch { + process.stderr.write("\r\x1b[2K"); + } +} + +export function createSpinner() { + const { frames, interval } = cliSpinners.dots; + let timer = null; + let frameIndex = 0; + let message = ""; + let active = false; + let activeLogger = null; + + function render() { + if (!active) { + return; + } + const frame = frames[frameIndex]; + frameIndex = (frameIndex + 1) % frames.length; + const prefix = formatLogPrefix(activeLogger); + const line = `${prefix}${frame} ${chalk.bold.cyan(message)}`; + process.stderr.write(`\r\x1b[2K${line}`); + } + + function stopSync() { + if (timer) { + clearInterval(timer); + timer = null; + } + + const wasActive = active; + active = false; + activeLogger = null; + + if (wasActive) { + clearSpinnerLineSync(); + } + } + + async function stop() { + await new Promise((resolve) => setTimeout(resolve, 15)); + stopSync(); + } + + function start(text, logger, timeout = true) { + if (timeout == true) { + setTimeout(() => { + start(text, logger, false); + }, 10); + return; + } + stopSync(); + message = text; + frameIndex = 0; + activeLogger = logger; + + if (!process.stderr.isTTY) { + const prefix = formatLogPrefix(activeLogger); + process.stderr.write( + `${prefix}${frames[0]} ${chalk.bold.cyan(message)}\n`, + ); + return; + } + + active = true; + render(); + timer = setInterval(render, interval); + } + + return { start, stop, stopSync }; +} + +export function registerSpinner(instance) { + spinner = instance; + patchConsoleLog(); +} + +export function startWaiting(message, logger) { + spinner?.start(message, logger); +} + +export async function stopWaiting() { + await spinner?.stop(); +}