Update configuration and enhance CLI logging functionality
Some checks failed
farmcontrol/farmcontrol-server/pipeline/head There was a failure building this commit

- 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.
This commit is contained in:
Tom Butcher 2026-07-27 02:03:17 +01:00
parent 20080929f9
commit ee9895d562
17 changed files with 676 additions and 296 deletions

View File

@ -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",

18
pnpm-lock.yaml generated
View File

@ -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

203
src/commandlineutils.js Normal file
View File

@ -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("<otp>") +
"' 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");
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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 = [];
}

View File

@ -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" };
}

View File

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

View File

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

View File

@ -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);
}),
);

View File

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

View File

@ -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;
}

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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}`,
);
}
}

View File

@ -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);

122
src/spinner.js Normal file
View File

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