import net from "node:net"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import dotenv from "dotenv"; import { parse, renderTokens, serialize } from "./parser.js"; dotenv.config(); const __dirname = path.dirname(fileURLToPath(import.meta.url)); const num = (v, d) => (v === undefined || v === "" ? d : Number(v)); const str = (v, d) => (v === undefined || v === "" ? d : v); const bool = (v, d) => v === undefined || v === "" ? d : v.toLowerCase() === "true" || v === "1"; // Parse CUT_PREFIX (hex, space-separated) into a byte array. function parseCutPrefix(v, d) { if (v === undefined || v === "") return d; return v .trim() .split(/\s+/) .map((h) => parseInt(h, 16)); } const config = { listenHost: str(process.env.LISTEN_HOST, "0.0.0.0"), listenPort: num(process.env.LISTEN_PORT, 9100), targetHost: str(process.env.TARGET_HOST, "10.10.10.24"), targetPort: num(process.env.TARGET_PORT, 9100), logDir: str(process.env.LOG_DIR, "./logs"), forward: bool(process.env.FORWARD, true), cutPrefix: parseCutPrefix(process.env.CUT_PREFIX, [0x1d, 0x56]), keepAfterCut: bool(process.env.KEEP_AFTER_CUT, true), }; const logDir = path.resolve(__dirname, config.logDir); fs.mkdirSync(logDir, { recursive: true }); let seq = 0; function timestamp() { // ISO timestamp without colons (filesystem friendly), with ms. return new Date().toISOString().replace(/[:.]/g, "-"); } function logJob(buffer) { if (!buffer || buffer.length === 0) return; const id = `${timestamp()}-${String(++seq).padStart(4, "0")}`; const parsedPath = path.join(logDir, `job-${id}.log`); // Only the parsed, lossless token stream is logged. let parsedLines = ""; try { const events = parse(buffer); parsedLines = renderTokens(events); // Sanity: the parser must be lossless (rebuild == original). const rebuilt = serialize(events); if (!rebuilt.equals(buffer)) { throw new Error("parser round-trip mismatch"); } } catch (err) { parsedLines = `[parse error] ${err.message}\n${buffer.toString("latin1")}`; } fs.writeFileSync(parsedPath, parsedLines); console.log(`[log] job ${id} (${buffer.length} bytes) -> ${parsedPath}`); } // Find the end offset (exclusive) of the first complete cut command in `buf`. // Returns -1 if no complete cut sequence is present yet. // A cut is: cutPrefix (0x1D 0x56) followed by 1 mode byte. function findCutEnd(buf, cutPrefix) { const n = cutPrefix.length; let i = 0; while (i <= buf.length - (n + 1)) { let match = true; for (let k = 0; k < n; k++) { if (buf[i + k] !== cutPrefix[k]) { match = false; break; } } if (match) { // cut + mode byte => end is i + n + 1 return i + n + 1; } i++; } return -1; } // Streams received bytes to the target printer (unless forwarding is disabled). function connectTarget() { if (!config.forward) return null; const target = net.createConnection( { host: config.targetHost, port: config.targetPort }, () => console.log(`[target] connected to ${config.targetHost}:${config.targetPort}`) ); target.on("error", (err) => { console.error(`[target] error: ${err.message}`); target.destroy(); }); target.on("close", () => console.log("[target] connection closed")); return target; } const server = net.createServer((clientSocket) => { const clientAddr = `${clientSocket.remoteAddress}:${clientSocket.remotePort}`; console.log(`[client] connected ${clientAddr}`); let buffer = Buffer.alloc(0); const target = connectTarget(); clientSocket.on("data", (chunk) => { // Forward raw bytes straight to the printer (if forwarding is enabled). if (target && target.writable) target.write(chunk); buffer = Buffer.concat([buffer, chunk]); // Finalize any complete jobs delimited by a cut command. let cutEnd = findCutEnd(buffer, config.cutPrefix); while (cutEnd !== -1) { const job = buffer.subarray(0, cutEnd); logJob(job); if (config.keepAfterCut) { buffer = buffer.subarray(cutEnd); cutEnd = findCutEnd(buffer, config.cutPrefix); } else { buffer = Buffer.alloc(0); break; } } }); const finalize = () => { // Connection drop: any remaining bytes form the last job. if (buffer.length > 0) { logJob(buffer); buffer = Buffer.alloc(0); } if (target) target.end(); }; clientSocket.on("end", () => { console.log(`[client] end ${clientAddr}`); finalize(); }); clientSocket.on("close", () => { console.log(`[client] closed ${clientAddr}`); finalize(); }); clientSocket.on("error", (err) => { console.error(`[client] error ${clientAddr}: ${err.message}`); finalize(); }); }); server.listen(config.listenPort, config.listenHost, () => { console.log( `[server] listening on ${config.listenHost}:${config.listenPort}, ` + (config.forward ? `forwarding to ${config.targetHost}:${config.targetPort}, ` : `forwarding DISABLED (receive + log only), `) + `logging to ${logDir}` ); });