Genesis
This commit is contained in:
169
index.js
Normal file
169
index.js
Normal file
@@ -0,0 +1,169 @@
|
||||
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}`
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user