Files
doslang-mirror/.qemu/serial-relay.mjs
T
2026-08-16 06:12:59 +09:00

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);