#!/usr/bin/env node import os from "node:os" import path from "node:path" import { rm } from "node:fs/promises" import { FedeoApi } from "./api.js" import { loadConfig } from "./config.js" import { log } from "./logger.js" import { listPrinters } from "./print/cups.js" import { hasSane, listScanners, runScan } from "./scan/sane.js" import { AgentHeartbeat } from "./types.js" const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) const stringifyError = (error: unknown) => { if (error instanceof Error) return error.message return String(error) } const main = async () => { const config = loadConfig() const api = new FedeoApi(config) log.info("FEDEO Geräte-Agent startet", { platform: process.platform, workDir: config.workDir, pollSeconds: config.pollSeconds, }) while (true) { try { const scannerNames = await listScanners().catch((error) => { log.warn("Scanner-Erkennung fehlgeschlagen", stringifyError(error)) return [] }) const printerNames = await listPrinters().catch(() => []) const scanAvailable = await hasSane() const heartbeatPayload: AgentHeartbeat = { capabilities: { scan: scanAvailable, print: printerNames.length > 0, platform: process.platform, }, scannerNames, printerNames, debugInfo: { hostname: os.hostname(), release: os.release(), arch: os.arch(), node: process.version, uptimeSeconds: Math.round(os.uptime()), }, } const heartbeat = await api.heartbeat(heartbeatPayload) if (heartbeat.pendingScanJobs > 0) { log.info(`${heartbeat.pendingScanJobs} Scan-Auftrag/Aufträge warten`) } const next = await api.nextScanJob() if (!next.job) { await sleep(config.pollSeconds * 1000) continue } log.info("Scan-Auftrag wird ausgeführt", { jobId: next.job.id, tenantId: next.job.tenantId, scannerName: next.job.scannerName || config.scannerName || "default", }) // Keep online status fresh during long scans without probing an occupied USB scanner. let heartbeatRunning = false const heartbeatTimer = setInterval(async () => { if (heartbeatRunning) return heartbeatRunning = true try { await api.heartbeat(heartbeatPayload) } catch (error) { log.warn("Heartbeat während Scan fehlgeschlagen", stringifyError(error)) } finally { heartbeatRunning = false } }, 30_000) try { await api.updateScanJobStatus(next.job.id, "running") const jobId = next.job.id const reportProgress = async (message: string) => { await api.updateScanJobStatus(jobId, "running", message).catch((error) => { log.warn("Scan-Status konnte nicht übertragen werden", stringifyError(error)) }) } const scanResult = await runScan(config, next.job, reportProgress) await reportProgress(`${scanResult.pageCount} Seiten gescannt. ${scanResult.warnings.join(" ")}`.trim()) try { await api.uploadScan(jobId, scanResult) } catch (error) { throw new Error(`${stringifyError(error)} Scan-Datei bleibt zur Wiederherstellung unter ${scanResult.path}.`) } await rm(path.dirname(scanResult.path), { recursive: true, force: true }).catch((error) => { log.warn("Scan gespeichert; temporäre Dateien konnten nicht entfernt werden", stringifyError(error)) }) log.info("Scan-Auftrag abgeschlossen", { jobId: next.job.id, file: scanResult.filename, pages: scanResult.pageCount, }) } catch (error) { const message = stringifyError(error) log.error("Scan-Auftrag fehlgeschlagen", { jobId: next.job.id, message, }) await api.updateScanJobStatus(next.job.id, "failed", message) } finally { clearInterval(heartbeatTimer) } } catch (error) { log.error("Agent-Schleife fehlgeschlagen", stringifyError(error)) await sleep(config.pollSeconds * 1000) } } } main().catch((error) => { log.error("Agent konnte nicht gestartet werden", stringifyError(error)) process.exit(1) })