170 lines
5.0 KiB
JavaScript
170 lines
5.0 KiB
JavaScript
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}`
|
|
);
|
|
});
|