111 lines
3.7 KiB
JavaScript
111 lines
3.7 KiB
JavaScript
import net from "node:net";
|
|
|
|
const HOST = "127.0.0.1";
|
|
const QEMU_PORT = 5556;
|
|
const TOOL_PORT = 5555;
|
|
const OBSERVER_PORT = 5557;
|
|
|
|
let qemu = null;
|
|
let controller = null;
|
|
const observers = new Set();
|
|
|
|
function safeWrite(socket, data) {
|
|
if (socket && !socket.destroyed && socket.writable) socket.write(data);
|
|
}
|
|
|
|
function decodeHex(hex) {
|
|
if (!hex || hex.length % 2 || !/^[0-9a-f]+$/i.test(hex)) return null;
|
|
return Buffer.from(hex, "hex").toString("utf8");
|
|
}
|
|
|
|
function visible(text) {
|
|
const shortened = text.length > 400 ? `${text.slice(0, 400)}…` : text;
|
|
return shortened
|
|
.replace(/\r\n?/g, "\n")
|
|
.replace(/[^\x20-\x7E\n]/g, (char) => `[0x${char.charCodeAt(0).toString(16).padStart(2, "0")}]`)
|
|
.replace(/\n/g, "\r\n ");
|
|
}
|
|
|
|
function formatLine(source, line) {
|
|
if (!line) return null;
|
|
if (line === "PING" || line === "QUIT" || line === "DOSAGENT READY") return `${source} ${line}`;
|
|
|
|
const [command, ...args] = line.split(" ");
|
|
if (["READ", "LIST"].includes(command) && args[0]) {
|
|
return `${source} ${command} ${visible(decodeHex(args[0]) ?? args[0])}${args[1] ? ` @${args[1]}` : ""}`;
|
|
}
|
|
if (command === "WRITE" && args.length >= 3) {
|
|
const path = decodeHex(args[0]) ?? args[0];
|
|
const body = decodeHex(args.slice(2).join(" ")) ?? args.slice(2).join(" ");
|
|
return `${source} WRITE ${visible(path)} (${args[1] === "T" ? "replace" : "append"}) ${visible(body)}`;
|
|
}
|
|
if (command === "EXEC" && args[0]) return `${source} EXEC ${visible(decodeHex(args.join(" ")) ?? args.join(" "))}`;
|
|
if (command === "ERR" && args[0]) return `${source} ERROR ${visible(decodeHex(args.join(" ")) ?? args.join(" "))}`;
|
|
if (command === "OK") {
|
|
const singlePayload = args.length === 1 ? decodeHex(args[0]) : null;
|
|
const payload = singlePayload ?? (args.length > 1 ? decodeHex(args.slice(1).join(" ")) : null);
|
|
const status = singlePayload !== null ? "" : (args[0] ? ` (${args[0]})` : "");
|
|
return `${source} OK${status}${payload !== null ? ` ${visible(payload)}` : ""}`;
|
|
}
|
|
return `${source} ${line}`;
|
|
}
|
|
|
|
const traceBuffers = new Map();
|
|
function trace(source, data) {
|
|
const pending = (traceBuffers.get(source) ?? "") + data.toString("ascii");
|
|
const parts = pending.split(/\r?\n/);
|
|
traceBuffers.set(source, parts.pop());
|
|
for (const line of parts) {
|
|
const formatted = formatLine(source, line);
|
|
if (formatted) for (const observer of observers) safeWrite(observer, `${formatted}\r\n`);
|
|
}
|
|
}
|
|
|
|
const qemuServer = net.createServer((socket) => {
|
|
if (qemu && !qemu.destroyed) {
|
|
socket.end();
|
|
return;
|
|
}
|
|
qemu = socket;
|
|
socket.setNoDelay(true);
|
|
socket.on("data", (data) => {
|
|
safeWrite(controller, data);
|
|
trace("DOS -> Pi", data);
|
|
});
|
|
socket.on("close", () => {
|
|
if (qemu === socket) qemu = null;
|
|
if (controller && !controller.destroyed) controller.destroy();
|
|
});
|
|
socket.on("error", () => {});
|
|
});
|
|
|
|
const toolServer = net.createServer((socket) => {
|
|
if (controller && !controller.destroyed) {
|
|
socket.end("ERR 53455249414C20434F4E54524F4C4C45522042555359\r\n");
|
|
return;
|
|
}
|
|
controller = socket;
|
|
socket.setNoDelay(true);
|
|
socket.on("data", (data) => {
|
|
safeWrite(qemu, data);
|
|
trace("Pi -> DOS", data);
|
|
});
|
|
socket.on("close", () => {
|
|
if (controller === socket) controller = null;
|
|
});
|
|
socket.on("error", () => {});
|
|
});
|
|
|
|
const observerServer = net.createServer((socket) => {
|
|
observers.add(socket);
|
|
socket.setNoDelay(true);
|
|
socket.write("DOS serial observer - decoded protocol (read-only)\r\n");
|
|
socket.on("data", () => {});
|
|
socket.on("close", () => observers.delete(socket));
|
|
socket.on("error", () => observers.delete(socket));
|
|
});
|
|
|
|
qemuServer.listen(QEMU_PORT, HOST);
|
|
toolServer.listen(TOOL_PORT, HOST);
|
|
observerServer.listen(OBSERVER_PORT, HOST);
|