feat: replace serial automation with resident TCP agent

This commit is contained in:
2026-08-16 15:51:42 +09:00
parent e5781fc168
commit 8e8ab22d30
16 changed files with 521 additions and 699 deletions
+68
View File
@@ -0,0 +1,68 @@
param(
[int] $ReadyTimeoutSeconds = 45
)
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
function Stop-Listener([int] $Port) {
$listeners = Get-NetTCPConnection -State Listen -LocalPort $Port -ErrorAction SilentlyContinue
foreach ($listener in $listeners) {
Stop-Process -Id $listener.OwningProcess -Force -ErrorAction SilentlyContinue
}
}
# Quit through QEMU's monitor so qcow2/FAT writes are flushed. Forced VM
# termination is deliberately forbidden because it previously lost files.
$monitor = Get-NetTCPConnection -State Listen -LocalPort 4444 -ErrorAction SilentlyContinue
if ($monitor) {
& (Join-Path $root 'monitor.ps1') 'quit' | Out-Null
$deadline = (Get-Date).AddSeconds(10)
do {
Start-Sleep -Milliseconds 250
$monitor = Get-NetTCPConnection -State Listen -LocalPort 4444 -ErrorAction SilentlyContinue
} while ($monitor -and (Get-Date) -lt $deadline)
if ($monitor) { throw 'QEMU did not quit cleanly; refusing a forced reset.' }
}
Stop-Listener 5555
Start-Sleep -Milliseconds 500
$runner = Join-Path $root 'run.ps1'
Start-Process powershell -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File',$runner) -WorkingDirectory $root
$deadline = (Get-Date).AddSeconds(15)
do {
Start-Sleep -Milliseconds 250
$monitor = Get-NetTCPConnection -State Listen -LocalPort 4444 -ErrorAction SilentlyContinue
} while (-not $monitor -and (Get-Date) -lt $deadline)
if (-not $monitor) { throw 'QEMU monitor did not start.' }
# Select the default boot entry. TCPAGENT is started by FDAUTO.BAT after the
# packet driver, so readiness is the agent connection/PONG rather than sleep.
Start-Sleep -Seconds 2
& (Join-Path $root 'monitor.ps1') 'sendkey ret' | Out-Null
$deadline = (Get-Date).AddSeconds($ReadyTimeoutSeconds)
$ready = $false
do {
Start-Sleep -Milliseconds 500
$connection = Get-NetTCPConnection -State Established -LocalPort 5558 -ErrorAction SilentlyContinue
if ($connection) {
try {
$client = [Net.Sockets.TcpClient]::new('127.0.0.1',5555)
$stream = $client.GetStream()
$stream.ReadTimeout = 1500
$bytes = [Text.Encoding]::ASCII.GetBytes("PING`n")
$stream.Write($bytes,0,$bytes.Length)
$buffer = New-Object byte[] 128
$reply = ''
while ($reply -notmatch "`n") {
$count = $stream.Read($buffer,0,$buffer.Length)
if ($count -le 0) { break }
$reply += [Text.Encoding]::ASCII.GetString($buffer,0,$count)
}
$client.Dispose()
$ready = $reply -match '^OK 504F4E47'
} catch { $ready = $false }
}
} while (-not $ready -and (Get-Date) -lt $deadline)
if (-not $ready) { throw 'TCPAGENT did not become ready.' }
Write-Host 'QEMU reset complete; TCPAGENT answered PONG.'
+4 -11
View File
@@ -4,26 +4,21 @@ $ErrorActionPreference = 'Stop'
$qemu = Get-Command qemu-system-i386.exe -ErrorAction Stop
$node = Get-Command node.exe -ErrorAction Stop
$disk = Join-Path $PSScriptRoot 'freedos.qcow2'
$share = Join-Path $PSScriptRoot 'share'
$relay = Join-Path $PSScriptRoot 'serial-relay.mjs'
# QEMU's FAT-directory drive is a convenient exchange volume, but it is not a
# reliable live-sync mechanism. Restart QEMU after host-side source changes.
New-Item -ItemType Directory -Force -Path $share | Out-Null
$relay = Join-Path $PSScriptRoot 'tcp-agent-relay.mjs'
if (-not (Test-Path -LiteralPath $disk)) {
throw "Missing $disk. Run .qemu\\setup.ps1, then .qemu\\install.ps1."
}
$relayListening = Get-NetTCPConnection -State Listen -LocalPort 5556 -ErrorAction SilentlyContinue
$relayListening = Get-NetTCPConnection -State Listen -LocalPort 5558 -ErrorAction SilentlyContinue
if (-not $relayListening) {
Start-Process -FilePath $node.Source -ArgumentList @($relay) -WorkingDirectory $PSScriptRoot -WindowStyle Hidden
$deadline = [DateTime]::UtcNow.AddSeconds(5)
do {
Start-Sleep -Milliseconds 100
$relayListening = Get-NetTCPConnection -State Listen -LocalPort 5556 -ErrorAction SilentlyContinue
$relayListening = Get-NetTCPConnection -State Listen -LocalPort 5558 -ErrorAction SilentlyContinue
} while (-not $relayListening -and [DateTime]::UtcNow -lt $deadline)
if (-not $relayListening) { throw 'Serial relay did not start.' }
if (-not $relayListening) { throw 'TCP agent relay did not start.' }
}
$monitorListening = Get-NetTCPConnection -State Listen -LocalPort 4444 -ErrorAction SilentlyContinue
@@ -36,9 +31,7 @@ if ($monitorListening) {
-smp 1 `
-m 64 `
-drive "file=$disk,format=qcow2,if=ide,index=0,media=disk" `
-hdb "fat:rw:$share" `
-nic user,model=ne2k_isa `
-monitor "tcp:127.0.0.1:4444,server=on,wait=off" `
-serial "tcp:127.0.0.1:5556" `
-boot order=c `
-display default
-110
View File
@@ -1,110 +0,0 @@
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);
-240
View File
@@ -1,240 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dos.h>
#include <conio.h>
#include <bios.h>
#define LINE_SIZE 9000
#define CHUNK_SIZE 4096
static char linebuf[LINE_SIZE];
static unsigned char data[CHUNK_SIZE + 1];
#define COM1_BASE 0x3F8
static void serial_init(void) {
outp(COM1_BASE + 1, 0x00);
outp(COM1_BASE + 3, 0x80);
outp(COM1_BASE + 0, 12);
outp(COM1_BASE + 1, 0);
outp(COM1_BASE + 3, 0x03);
outp(COM1_BASE + 2, 0xC7);
outp(COM1_BASE + 4, 0x0B);
}
static int console_break_requested(void) {
unsigned short key = _bios_keybrd(_KEYBRD_READY);
if ((key & 0xFF) != 3) return 0;
_bios_keybrd(_KEYBRD_READ);
return 1;
}
static int serial_getc(void) {
while ((inp(COM1_BASE + 5) & 0x01) == 0) {
if (console_break_requested()) return -1;
}
return inp(COM1_BASE);
}
static void serial_putc(int c) {
while ((inp(COM1_BASE + 5) & 0x20) == 0) { }
outp(COM1_BASE, c);
}
static void serial_write(const char *text) {
while (*text) serial_putc((unsigned char)*text++);
}
static int serial_readline(char *buffer, int cap) {
int used = 0;
int c;
for (;;) {
c = serial_getc();
if (c < 0) return -1;
if (c == '\r' || c == '\n') {
if (used == 0) continue;
buffer[used] = '\0';
return used;
}
if (used < cap - 1) buffer[used++] = (char)c;
}
}
static int hexval(int c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
return -1;
}
static int decode_hex(const char *src, unsigned char *dst, int cap) {
int n = 0;
int hi, lo;
while (*src && src[1]) {
if (n >= cap) return -1;
hi = hexval((unsigned char)src[0]);
lo = hexval((unsigned char)src[1]);
if (hi < 0 || lo < 0) return -1;
dst[n++] = (unsigned char)((hi << 4) | lo);
src += 2;
}
return *src ? -1 : n;
}
static void print_hex(const unsigned char *src, unsigned count) {
static const char digits[] = "0123456789ABCDEF";
unsigned i;
for (i = 0; i < count; ++i) {
serial_putc(digits[src[i] >> 4]);
serial_putc(digits[src[i] & 15]);
}
}
static void ok_data(const unsigned char *src, unsigned count) {
serial_write("OK ");
print_hex(src, count);
serial_write("\r\n");
}
static void error_text(const char *message) {
serial_write("ERR ");
print_hex((const unsigned char *)message, strlen(message));
serial_write("\r\n");
}
static int decode_path(const char *hex, char *path, int cap) {
int n = decode_hex(hex, (unsigned char *)path, cap - 1);
if (n < 0) return 0;
path[n] = '\0';
return 1;
}
static void command_read(char *args) {
char path[260];
char *offset_text = strchr(args, ' ');
FILE *file;
long offset;
size_t count;
int eof;
if (!offset_text) { error_text("READ requires path and offset"); return; }
*offset_text++ = '\0';
if (!decode_path(args, path, sizeof(path))) { error_text("Invalid path encoding"); return; }
offset = atol(offset_text);
file = fopen(path, "rb");
if (!file) { error_text("Cannot open file"); return; }
if (fseek(file, offset, SEEK_SET) != 0) { fclose(file); error_text("Cannot seek file"); return; }
count = fread(data, 1, CHUNK_SIZE, file);
eof = feof(file) ? 1 : 0;
if (count < CHUNK_SIZE) eof = 1;
fclose(file);
sprintf(linebuf, "OK %d ", eof);
serial_write(linebuf);
print_hex(data, count);
serial_write("\r\n");
}
static void command_write(char *args) {
char path[260];
char *mode_text = strchr(args, ' ');
char *payload;
FILE *file;
int count;
if (!mode_text) { error_text("WRITE requires path, mode and data"); return; }
*mode_text++ = '\0';
payload = strchr(mode_text, ' ');
if (!payload) { error_text("WRITE requires data"); return; }
*payload++ = '\0';
if (!decode_path(args, path, sizeof(path))) { error_text("Invalid path encoding"); return; }
count = decode_hex(payload, data, CHUNK_SIZE);
if (count < 0) { error_text("Invalid data encoding"); return; }
file = fopen(path, mode_text[0] == 'A' ? "ab" : "wb");
if (!file) { error_text("Cannot write file"); return; }
if (count && fwrite(data, 1, count, file) != (size_t)count) {
fclose(file); error_text("Short write"); return;
}
fclose(file);
ok_data((const unsigned char *)"", 0);
}
static void command_list(char *args) {
char path[260];
char pattern[300];
char output[CHUNK_SIZE];
struct find_t found;
unsigned used = 0;
int rc;
if (!decode_path(args, path, sizeof(path))) { error_text("Invalid path encoding"); return; }
strcpy(pattern, path);
if (pattern[0] && pattern[strlen(pattern) - 1] != '\\' && pattern[strlen(pattern) - 1] != '/') strcat(pattern, "\\");
strcat(pattern, "*.*");
rc = _dos_findfirst(pattern, _A_NORMAL | _A_RDONLY | _A_HIDDEN | _A_SYSTEM | _A_SUBDIR | _A_ARCH, &found);
while (rc == 0) {
char entry[100];
int len;
if (strcmp(found.name, ".") && strcmp(found.name, "..")) {
sprintf(entry, "%s\t%lu\t%s\n", found.name, found.size, (found.attrib & _A_SUBDIR) ? "DIR" : "FILE");
len = strlen(entry);
if (used + len >= sizeof(output)) break;
memcpy(output + used, entry, len);
used += len;
}
rc = _dos_findnext(&found);
}
ok_data((const unsigned char *)output, used);
}
static void command_exec(char *args) {
char command[700];
char shell_command[800];
const char *temp_path = "C:\\PIEXEC.TMP";
FILE *file;
int n, exit_code;
size_t count;
n = decode_hex(args, (unsigned char *)command, sizeof(command) - 1);
if (n < 0) { error_text("Invalid command encoding"); return; }
command[n] = '\0';
sprintf(shell_command, "COMMAND.COM /C %s > %s", command, temp_path);
exit_code = system(shell_command);
file = fopen(temp_path, "rb");
count = file ? fread(data, 1, CHUNK_SIZE, file) : 0;
if (file) fclose(file);
remove(temp_path);
sprintf(linebuf, "OK %d ", exit_code);
serial_write(linebuf);
print_hex(data, count);
serial_write("\r\n");
}
int main(void) {
char *command;
char *args;
serial_init();
cprintf("\r\n[DOSAGENT] Ready on COM1. Press Ctrl+C here to stop.\r\n");
serial_write("DOSAGENT READY\r\n");
for (;;) {
if (serial_readline(linebuf, sizeof(linebuf)) < 0) {
cprintf("\r\n[DOSAGENT] Stopped from VGA console.\r\n");
break;
}
command = linebuf;
args = strchr(command, ' ');
if (args) *args++ = '\0'; else args = command + strlen(command);
cprintf("[DOSAGENT] %s\r\n", command);
if (!strcmp(command, "PING")) ok_data((const unsigned char *)"PONG", 4);
else if (!strcmp(command, "READ")) command_read(args);
else if (!strcmp(command, "WRITE")) command_write(args);
else if (!strcmp(command, "LIST")) command_list(args);
else if (!strcmp(command, "EXEC")) command_exec(args);
else if (!strcmp(command, "QUIT")) { ok_data((const unsigned char *)"BYE", 3); break; }
else error_text("Unknown command");
}
return 0;
}
+71
View File
@@ -0,0 +1,71 @@
import net from "node:net";
const HOST = "127.0.0.1";
const TOOL_PORT = 5555;
const OBSERVER_PORT = 5557;
const TCP_AGENT_PORT = 5558;
let tcpAgent = null;
let controller = null;
const observers = new Set();
function safeWrite(socket, data) {
if (socket && !socket.destroyed && socket.writable) socket.write(data);
}
function trace(source, data) {
const text = data.toString("ascii");
const shortened = text.length > 400 ? `${text.slice(0, 400)}` : text;
for (const observer of observers) safeWrite(observer, `${source} ${shortened}\r\n`);
}
const tcpAgentServer = net.createServer((socket) => {
if (tcpAgent && !tcpAgent.destroyed) {
socket.end();
return;
}
tcpAgent = socket;
socket.setNoDelay(true);
socket.on("data", (data) => {
safeWrite(controller, data);
trace("DOS->Pi", data);
});
socket.on("close", () => {
if (tcpAgent === socket) tcpAgent = null;
});
socket.on("error", () => {});
});
const toolServer = net.createServer((socket) => {
if (controller && !controller.destroyed) {
socket.end("ERR 544350204147454E5420434F4E54524F4C4C45522042555359\r\n");
return;
}
if (!tcpAgent || tcpAgent.destroyed) {
socket.end("ERR 544350204147454E54204E4F5420434F4E4E4543544544\r\n");
return;
}
controller = socket;
socket.setNoDelay(true);
socket.on("data", (data) => {
safeWrite(tcpAgent, 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 TCP agent observer (read-only)\r\n");
socket.on("data", () => {});
socket.on("close", () => observers.delete(socket));
socket.on("error", () => observers.delete(socket));
});
tcpAgentServer.listen(TCP_AGENT_PORT, HOST);
toolServer.listen(TOOL_PORT, HOST);
observerServer.listen(OBSERVER_PORT, HOST);
-19
View File
@@ -1,19 +0,0 @@
@echo off
rem Pull a host-built TCP staging bundle into the authoritative C:\FEC tree.
rem QEMU user networking exposes the host as 10.0.2.2. HTGET and UNZIP are
rem supplied by the installed FreeDOS network tools.
if "%1"=="" goto usage
if exist C:\FEC\STAGE.ZIP del C:\FEC\STAGE.ZIP
htget -o C:\FEC\STAGE.ZIP %1
if errorlevel 1 goto fail
unzip -o C:\FEC\STAGE.ZIP -d C:\FEC
if errorlevel 1 goto fail
if exist C:\FEC\TCPSTG.OK del C:\FEC\TCPSTG.OK
echo OK>C:\FEC\TCPSTG.OK
goto done
:usage
echo usage: TCP-STAGE.BAT http://10.0.2.2:8000/STAGE.ZIP
goto done
:fail
echo FAIL>C:\FEC\TCPSTG.FAIL
:done
+59 -8
View File
@@ -12,7 +12,7 @@ from pathlib import Path
HOST = "127.0.0.1"
PORT = 5555
CHUNK_SIZE = 4096
DEFAULT_CHUNK_SIZE = 4096
def request(line: str) -> str:
@@ -32,11 +32,50 @@ def request(line: str) -> str:
return text
def stage(local: Path, remote: str) -> None:
def recv_line(sock: socket.socket) -> bytes:
line = bytearray()
while not line.endswith(b"\n"):
part = sock.recv(1)
if not part:
raise RuntimeError("TCP agent closed the connection")
line.extend(part)
return bytes(line).strip()
def fnv1a(data: bytes) -> int:
value = 2166136261
for byte in data:
value = ((value ^ byte) * 16777619) & 0xFFFFFFFF
return value
def binary_stage(local: Path, remote: str) -> None:
data = local.read_bytes()
path_hex = remote.encode("ascii").hex().upper()
with socket.create_connection((HOST, PORT), timeout=30) as sock:
sock.sendall(("PUT %s %d\n" % (path_hex, len(data))).encode("ascii"))
sock.sendall(data)
response = recv_line(sock)
if not response.startswith(b"OK"):
raise RuntimeError("PUT failed: " + response.decode("ascii", "replace"))
sock.sendall(("HASH %s\n" % path_hex).encode("ascii"))
header = recv_line(sock).decode("ascii", "strict")
fields = header.split()
if len(fields) != 3 or fields[0] != "STAT":
raise RuntimeError("HASH failed: " + header)
remote_length = int(fields[1])
remote_hash = int(fields[2], 16)
if remote_length != len(data) or remote_hash != fnv1a(data):
raise RuntimeError("verification mismatch for " + remote)
print("staged %s -> %s (%d bytes, sha256 %s, binary TCP)" % (
local, remote, len(data), hashlib.sha256(data).hexdigest()))
def stage(local: Path, remote: str, chunk_size: int) -> None:
data = local.read_bytes()
remote_hex = remote.encode("ascii").hex().upper()
for offset in range(0, max(1, len(data)), CHUNK_SIZE):
part = data[offset : offset + CHUNK_SIZE]
for offset in range(0, max(1, len(data)), chunk_size):
part = data[offset : offset + chunk_size]
mode = "T" if offset == 0 else "A"
request("WRITE %s %s %s" % (remote_hex, mode, part.hex().upper()))
remote_data = bytearray()
@@ -56,14 +95,26 @@ def stage(local: Path, remote: str) -> None:
def main() -> int:
if len(sys.argv) < 2:
print("usage: dos_stage.py LOCAL=DOS_PATH [... ]", file=sys.stderr)
args = sys.argv[1:]
chunk_size = DEFAULT_CHUNK_SIZE
binary = False
if args and args[0] == "--binary":
binary = True
args = args[1:]
if len(args) >= 2 and args[0] == "--chunk-size":
chunk_size = int(args[1])
args = args[2:]
if not args:
print("usage: dos_stage.py [--binary] [--chunk-size N] LOCAL=DOS_PATH [... ]", file=sys.stderr)
return 2
for spec in sys.argv[1:]:
for spec in args:
if "=" not in spec:
raise SystemExit("missing '=' in " + spec)
local, remote = spec.split("=", 1)
stage(Path(local), remote)
if binary:
binary_stage(Path(local), remote)
else:
stage(Path(local), remote, chunk_size)
return 0
-243
View File
@@ -1,243 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dos.h>
#include <conio.h>
#include <bios.h>
#define LINE_SIZE 9000
#define CHUNK_SIZE 4096
static char linebuf[LINE_SIZE];
static unsigned char data[CHUNK_SIZE + 1];
#define COM1_BASE 0x3F8
/* The QEMU backend is a TCP socket; use the fastest standard 16550 rate.
1.8432 MHz / (16 * 1) = 115200 baud. */
#define COM1_DIVISOR 1
static void serial_init(void) {
outp(COM1_BASE + 1, 0x00);
outp(COM1_BASE + 3, 0x80);
outp(COM1_BASE + 0, COM1_DIVISOR);
outp(COM1_BASE + 1, 0);
outp(COM1_BASE + 3, 0x03);
outp(COM1_BASE + 2, 0xC7);
outp(COM1_BASE + 4, 0x0B);
}
static int console_break_requested(void) {
unsigned short key = _bios_keybrd(_KEYBRD_READY);
if ((key & 0xFF) != 3) return 0;
_bios_keybrd(_KEYBRD_READ);
return 1;
}
static int serial_getc(void) {
while ((inp(COM1_BASE + 5) & 0x01) == 0) {
if (console_break_requested()) return -1;
}
return inp(COM1_BASE);
}
static void serial_putc(int c) {
while ((inp(COM1_BASE + 5) & 0x20) == 0) { }
outp(COM1_BASE, c);
}
static void serial_write(const char *text) {
while (*text) serial_putc((unsigned char)*text++);
}
static int serial_readline(char *buffer, int cap) {
int used = 0;
int c;
for (;;) {
c = serial_getc();
if (c < 0) return -1;
if (c == '\r' || c == '\n') {
if (used == 0) continue;
buffer[used] = '\0';
return used;
}
if (used < cap - 1) buffer[used++] = (char)c;
}
}
static int hexval(int c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
return -1;
}
static int decode_hex(const char *src, unsigned char *dst, int cap) {
int n = 0;
int hi, lo;
while (*src && src[1]) {
if (n >= cap) return -1;
hi = hexval((unsigned char)src[0]);
lo = hexval((unsigned char)src[1]);
if (hi < 0 || lo < 0) return -1;
dst[n++] = (unsigned char)((hi << 4) | lo);
src += 2;
}
return *src ? -1 : n;
}
static void print_hex(const unsigned char *src, unsigned count) {
static const char digits[] = "0123456789ABCDEF";
unsigned i;
for (i = 0; i < count; ++i) {
serial_putc(digits[src[i] >> 4]);
serial_putc(digits[src[i] & 15]);
}
}
static void ok_data(const unsigned char *src, unsigned count) {
serial_write("OK ");
print_hex(src, count);
serial_write("\r\n");
}
static void error_text(const char *message) {
serial_write("ERR ");
print_hex((const unsigned char *)message, strlen(message));
serial_write("\r\n");
}
static int decode_path(const char *hex, char *path, int cap) {
int n = decode_hex(hex, (unsigned char *)path, cap - 1);
if (n < 0) return 0;
path[n] = '\0';
return 1;
}
static void command_read(char *args) {
char path[260];
char *offset_text = strchr(args, ' ');
FILE *file;
long offset;
size_t count;
int eof;
if (!offset_text) { error_text("READ requires path and offset"); return; }
*offset_text++ = '\0';
if (!decode_path(args, path, sizeof(path))) { error_text("Invalid path encoding"); return; }
offset = atol(offset_text);
file = fopen(path, "rb");
if (!file) { error_text("Cannot open file"); return; }
if (fseek(file, offset, SEEK_SET) != 0) { fclose(file); error_text("Cannot seek file"); return; }
count = fread(data, 1, CHUNK_SIZE, file);
eof = feof(file) ? 1 : 0;
if (count < CHUNK_SIZE) eof = 1;
fclose(file);
sprintf(linebuf, "OK %d ", eof);
serial_write(linebuf);
print_hex(data, count);
serial_write("\r\n");
}
static void command_write(char *args) {
char path[260];
char *mode_text = strchr(args, ' ');
char *payload;
FILE *file;
int count;
if (!mode_text) { error_text("WRITE requires path, mode and data"); return; }
*mode_text++ = '\0';
payload = strchr(mode_text, ' ');
if (!payload) { error_text("WRITE requires data"); return; }
*payload++ = '\0';
if (!decode_path(args, path, sizeof(path))) { error_text("Invalid path encoding"); return; }
count = decode_hex(payload, data, CHUNK_SIZE);
if (count < 0) { error_text("Invalid data encoding"); return; }
file = fopen(path, mode_text[0] == 'A' ? "ab" : "wb");
if (!file) { error_text("Cannot write file"); return; }
if (count && fwrite(data, 1, count, file) != (size_t)count) {
fclose(file); error_text("Short write"); return;
}
fclose(file);
ok_data((const unsigned char *)"", 0);
}
static void command_list(char *args) {
char path[260];
char pattern[300];
char output[CHUNK_SIZE];
struct find_t found;
unsigned used = 0;
int rc;
if (!decode_path(args, path, sizeof(path))) { error_text("Invalid path encoding"); return; }
strcpy(pattern, path);
if (pattern[0] && pattern[strlen(pattern) - 1] != '\\' && pattern[strlen(pattern) - 1] != '/') strcat(pattern, "\\");
strcat(pattern, "*.*");
rc = _dos_findfirst(pattern, _A_NORMAL | _A_RDONLY | _A_HIDDEN | _A_SYSTEM | _A_SUBDIR | _A_ARCH, &found);
while (rc == 0) {
char entry[100];
int len;
if (strcmp(found.name, ".") && strcmp(found.name, "..")) {
sprintf(entry, "%s\t%lu\t%s\n", found.name, found.size, (found.attrib & _A_SUBDIR) ? "DIR" : "FILE");
len = strlen(entry);
if (used + len >= sizeof(output)) break;
memcpy(output + used, entry, len);
used += len;
}
rc = _dos_findnext(&found);
}
ok_data((const unsigned char *)output, used);
}
static void command_exec(char *args) {
char command[700];
char shell_command[800];
const char *temp_path = "C:\\PIEXEC.TMP";
FILE *file;
int n, exit_code;
size_t count;
n = decode_hex(args, (unsigned char *)command, sizeof(command) - 1);
if (n < 0) { error_text("Invalid command encoding"); return; }
command[n] = '\0';
sprintf(shell_command, "COMMAND.COM /C %s > %s", command, temp_path);
exit_code = system(shell_command);
file = fopen(temp_path, "rb");
count = file ? fread(data, 1, CHUNK_SIZE, file) : 0;
if (file) fclose(file);
remove(temp_path);
sprintf(linebuf, "OK %d ", exit_code);
serial_write(linebuf);
print_hex(data, count);
serial_write("\r\n");
}
int main(void) {
char *command;
char *args;
serial_init();
cprintf("\r\n[DOSAGENT] Ready on COM1. Press Ctrl+C here to stop.\r\n");
serial_write("DOSAGENT READY\r\n");
for (;;) {
if (serial_readline(linebuf, sizeof(linebuf)) < 0) {
cprintf("\r\n[DOSAGENT] Stopped from VGA console.\r\n");
break;
}
command = linebuf;
args = strchr(command, ' ');
if (args) *args++ = '\0'; else args = command + strlen(command);
cprintf("[DOSAGENT] %s\r\n", command);
if (!strcmp(command, "PING")) ok_data((const unsigned char *)"PONG", 4);
else if (!strcmp(command, "READ")) command_read(args);
else if (!strcmp(command, "WRITE")) command_write(args);
else if (!strcmp(command, "LIST")) command_list(args);
else if (!strcmp(command, "EXEC")) command_exec(args);
else if (!strcmp(command, "QUIT")) { ok_data((const unsigned char *)"BYE", 3); break; }
else error_text("Unknown command");
}
return 0;
}
-68
View File
@@ -1,68 +0,0 @@
#!/usr/bin/env python3
"""Build and serve a FreeDOS TCP staging bundle.
The guest pulls STAGE.ZIP with its existing HTGET client from QEMU user-net's
host address (10.0.2.2), then extracts it into C:\\FEC. This avoids changing
the live vvfat exchange disk and keeps serial for bootstrap/recovery only.
"""
import argparse
import http.server
import os
import shutil
import zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
DEFAULT_STAGE = ROOT / ".tcp-stage"
def bundle(stage: Path, files: list[str]) -> Path:
if stage.exists():
shutil.rmtree(stage)
stage.mkdir(parents=True)
archive = stage / "STAGE.ZIP"
with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as zf:
for item in files:
source = (ROOT / item).resolve()
try:
relative = source.relative_to(ROOT / "fec")
except ValueError as exc:
raise SystemExit("only files below fec/ may be staged: " + item) from exc
if not source.is_file():
raise SystemExit("not a file: " + item)
zf.write(source, relative.as_posix())
print("created %s (%d bytes)" % (archive, archive.stat().st_size))
return archive
def serve(stage: Path, port: int) -> None:
if not (stage / "STAGE.ZIP").is_file():
raise SystemExit("missing STAGE.ZIP; run bundle first")
handler = lambda *args, **kwargs: http.server.SimpleHTTPRequestHandler(
*args, directory=str(stage), **kwargs)
server = http.server.ThreadingHTTPServer(("0.0.0.0", port), handler)
print("serving %s at http://10.0.2.2:%d/STAGE.ZIP" % (stage, port))
try:
server.serve_forever()
finally:
server.server_close()
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--stage-dir", type=Path, default=DEFAULT_STAGE)
commands = parser.add_subparsers(dest="command", required=True)
make = commands.add_parser("bundle")
make.add_argument("files", nargs="+")
web = commands.add_parser("serve")
web.add_argument("--port", type=int, default=8000)
args = parser.parse_args()
if args.command == "bundle":
bundle(args.stage_dir, args.files)
else:
serve(args.stage_dir, args.port)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+6
View File
@@ -0,0 +1,6 @@
@echo off
set WATCOM=C:\DEVEL\WATCOMC
set PATH=C:\DEVEL\WATCOMC\BINW;C:\DEVEL\WATCOMC\BINP;C:\FREEDOS\BIN
set INCLUDE=C:\DEVEL\WATCOMC\H
set EDPATH=
wmake
+15
View File
@@ -0,0 +1,15 @@
@echo off
if not exist TCPAGENT.EXE goto fail
copy TCPAGENT.EXE C:\FREEDOS\BIN\TCPAGENT.EXE > nul
if errorlevel 1 goto fail
find "TCPAGENT.EXE" C:\FDAUTO.BAT > nul
if not errorlevel 1 goto done
echo SET MTCPCFG=C:\FREEDOS\MTCP.CFG>>C:\FDAUTO.BAT
echo IF EXIST C:\FREEDOS\BIN\TCPAGENT.EXE C:\FREEDOS\BIN\TCPAGENT.EXE>>C:\FDAUTO.BAT
:done
echo TCPAGENT installed.
goto end
:fail
echo TCPAGENT install failed.
verify other 2>nul
:end
+38
View File
@@ -0,0 +1,38 @@
# Place this directory at MTCP\APPS\TCPAGENT in an mTCP checkout.
tcp_c_dir = ..\..\TCPLIB
memory_model = -mc
compile_options = @WPP.RSP
tcpobjs = packet.obj arp.obj eth.obj ip.obj tcp.obj tcpsockm.obj udp.obj utils.obj timer.obj ipasm.obj trace.obj
all : tcpagent.exe
.cpp.obj :
wpp $[* $(compile_options)
tcpagent.obj: tcpagent.cpp
wpp $(compile_options) tcpagent.cpp
packet.obj:
wpp $(tcp_c_dir)\packet.cpp $(compile_options)
arp.obj:
wpp $(tcp_c_dir)\arp.cpp $(compile_options)
eth.obj:
wpp $(tcp_c_dir)\eth.cpp $(compile_options)
ip.obj:
wpp $(tcp_c_dir)\ip.cpp $(compile_options)
tcp.obj:
wpp $(tcp_c_dir)\tcp.cpp $(compile_options)
tcpsockm.obj:
wpp $(tcp_c_dir)\tcpsockm.cpp $(compile_options)
udp.obj:
wpp $(tcp_c_dir)\udp.cpp $(compile_options)
utils.obj:
wpp $(tcp_c_dir)\utils.cpp $(compile_options)
timer.obj:
wpp $(tcp_c_dir)\timer.cpp $(compile_options)
trace.obj:
wpp $(tcp_c_dir)\trace.cpp $(compile_options)
ipasm.obj:
wasm -0 -mc $(tcp_c_dir)\ipasm.asm
tcpagent.exe: tcpagent.obj $(tcpobjs)
wlink system dos option map option eliminate option stack=16384 name $@ file *.obj
clean : .symbolic
del *.obj
del *.map
del tcpagent.exe
+32
View File
@@ -0,0 +1,32 @@
# FreeDOS resident TCP agent
`TCPAGENT.EXE` is a foreground resident automation process. It uses the mTCP
packet-driver stack and maintains an outbound connection to QEMU's host at
`10.0.2.2:5558`. The host relay keeps the existing Pi tool endpoint on 5555.
## Build in FreeDOS
1. Obtain the GPLv3 mTCP source tree (tested with the jhpyle/mTCP 2022 fork).
2. Copy this directory to `MTCP\APPS\TCPAGENT` inside that tree.
3. Set `WATCOM` for Open Watcom and run `BUILD.BAT`.
4. Run `INSTALL.BAT`; it installs the executable and adds startup lines after
the existing packet-driver setup in `C:\FDAUTO.BAT`.
The build uses mTCP's compact memory model and Open Watcom C++16. The agent is
therefore distributed under GPLv3 when linked with mTCP.
## Protocol
Legacy text commands remain for Pi tool compatibility: `PING`, `READ`,
`WRITE`, `LIST`, and `EXEC`. Fast staging uses binary framing:
- `PUT <hex DOS path> <byte length>\n<raw bytes>` -> `OK\r\n`
- `GET <hex DOS path>\n` -> `DATA <length>\r\n<raw bytes>`
- `HASH <hex DOS path>\n` -> `STAT <length> <FNV1A32>\r\n`
Use `python tools/dos_stage.py --binary LOCAL=C:\\DOS\\PATH` for verified
staging. Verification reads only the length and FNV-1a hash, avoiding a slow
full-file return transfer.
Use `.qemu/reset.ps1` to restart QEMU. It waits for an established TCP agent
and a successful PONG rather than sleeping for a fixed boot duration.
+11
View File
@@ -0,0 +1,11 @@
-0
-mc
-DCFG_H="tcpagent.cfg"
-oh
-os
-s
-zp2
-zpw
-we
-i=..\..\TCPINC
-i=..\..\INCLUDE
+12
View File
@@ -0,0 +1,12 @@
#ifndef CONFIG_H
#define CONFIG_H
#define MTCP_PROGRAM_NAME "tcpagent"
#include "Global.Cfg"
#define COMPILE_ARP
#define IP_FRAGMENTS_ON
#define COMPILE_UDP
#define COMPILE_TCP
#define COMPILE_ICMP
#undef TCP_MAX_SOCKETS
#define TCP_MAX_SOCKETS (1)
#endif
+205
View File
@@ -0,0 +1,205 @@
#include <bios.h>
#include <conio.h>
#include <dos.h>
#include <io.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "types.h"
#include "trace.h"
#include "utils.h"
#include "packet.h"
#include "arp.h"
#include "udp.h"
#include "tcp.h"
#include "tcpsockm.h"
#define LINE_SIZE 9000
#define CHUNK_SIZE 4096
#define SERVER_PORT 5558
#define LOCAL_PORT 2058
#define RECV_SIZE 12288
static char linebuf[LINE_SIZE];
static unsigned char data[CHUNK_SIZE+1];
static TcpSocket *socketp;
static volatile uint8_t stop_requested;
static FILE *put_file;
static unsigned long put_remaining;
void __interrupt __far ctrl_break(void) { stop_requested=1; }
void __interrupt __far ctrl_c(void) { stop_requested=1; }
static void drive(void) {
PACKET_PROCESS_MULT(5);
Arp::driveArp();
Tcp::drivePackets();
}
static int send_all(const void *buffer, unsigned length) {
const uint8_t *p=(const uint8_t *)buffer;
unsigned sent=0;
int rc;
while(sent<length && !stop_requested) {
drive();
rc=socketp->send((uint8_t *)(p+sent),length-sent);
if(rc>0) sent+=(unsigned)rc;
else if(rc<0 || socketp->isRemoteClosed()) return -1;
}
return sent==length ? 0 : -1;
}
static void write_text(const char *s) { send_all(s,strlen(s)); }
static int hexval(int c) {
if(c>='0'&&c<='9') return c-'0';
if(c>='A'&&c<='F') return c-'A'+10;
if(c>='a'&&c<='f') return c-'a'+10;
return -1;
}
static int decode_hex(const char *src,unsigned char *dst,int cap) {
int n=0,hi,lo;
while(*src&&src[1]) {
if(n>=cap) return -1;
hi=hexval((unsigned char)src[0]); lo=hexval((unsigned char)src[1]);
if(hi<0||lo<0) return -1;
dst[n++]=(unsigned char)((hi<<4)|lo); src+=2;
}
return *src ? -1 : n;
}
static void write_hex(const unsigned char *src,unsigned count) {
static const char digits[]="0123456789ABCDEF";
char out[256]; unsigned i,n;
while(count) {
n=count>sizeof(out)/2 ? sizeof(out)/2 : count;
for(i=0;i<n;++i) { out[i*2]=digits[src[i]>>4]; out[i*2+1]=digits[src[i]&15]; }
send_all(out,n*2); src+=n; count-=n;
}
}
static void ok_data(const unsigned char *src,unsigned count) {
write_text("OK "); write_hex(src,count); write_text("\r\n");
}
static void error_text(const char *s) {
write_text("ERR "); write_hex((const unsigned char *)s,strlen(s)); write_text("\r\n");
}
static int decode_path(const char *hex,char *path,int cap) {
int n=decode_hex(hex,(unsigned char *)path,cap-1);
if(n<0) return 0; path[n]='\0'; return 1;
}
static void command_read(char *args) {
char path[260],*off=strchr(args,' '); FILE *f; long pos; size_t count; int eof;
if(!off){error_text("READ requires path and offset");return;} *off++='\0';
if(!decode_path(args,path,sizeof(path))){error_text("Invalid path encoding");return;}
pos=atol(off); f=fopen(path,"rb"); if(!f){error_text("Cannot open file");return;}
if(fseek(f,pos,SEEK_SET)){fclose(f);error_text("Cannot seek file");return;}
count=fread(data,1,CHUNK_SIZE,f); eof=count<CHUNK_SIZE; fclose(f);
sprintf(linebuf,"OK %d ",eof); write_text(linebuf); write_hex(data,count); write_text("\r\n");
}
static void command_write(char *args) {
char path[260],*mode=strchr(args,' '),*payload; FILE *f; int count;
if(!mode){error_text("WRITE requires path, mode and data");return;} *mode++='\0';
payload=strchr(mode,' '); if(!payload){error_text("WRITE requires data");return;} *payload++='\0';
if(!decode_path(args,path,sizeof(path))){error_text("Invalid path encoding");return;}
count=decode_hex(payload,data,CHUNK_SIZE); if(count<0){error_text("Invalid data encoding");return;}
f=fopen(path,mode[0]=='A'?"ab":"wb"); if(!f){error_text("Cannot write file");return;}
if(count&&fwrite(data,1,count,f)!=(size_t)count){fclose(f);error_text("Short write");return;}
fclose(f); ok_data((const unsigned char *)"",0);
}
static void command_put(char *args) {
char path[260],*length_text=strchr(args,' ');
if(!length_text){error_text("PUT requires path and length");return;}
*length_text++='\0';
if(!decode_path(args,path,sizeof(path))){error_text("Invalid path encoding");return;}
put_remaining=strtoul(length_text,0,10);
put_file=fopen(path,"wb");
if(!put_file){put_remaining=0;error_text("Cannot write file");return;}
if(!put_remaining){fclose(put_file);put_file=0;ok_data((const unsigned char *)"",0);}
}
static void command_get(char *args) {
char path[260]; FILE *f; long length; size_t count;
if(!decode_path(args,path,sizeof(path))){error_text("Invalid path encoding");return;}
f=fopen(path,"rb"); if(!f){error_text("Cannot open file");return;}
fseek(f,0,SEEK_END); length=ftell(f); fseek(f,0,SEEK_SET);
sprintf(linebuf,"DATA %ld\r\n",length); write_text(linebuf);
while((count=fread(data,1,CHUNK_SIZE,f))>0) if(send_all(data,count)<0)break;
fclose(f);
}
static void command_hash(char *args) {
char path[260]; FILE *f; size_t count; unsigned i; unsigned long length=0,hash=2166136261UL;
if(!decode_path(args,path,sizeof(path))){error_text("Invalid path encoding");return;}
f=fopen(path,"rb"); if(!f){error_text("Cannot open file");return;}
while((count=fread(data,1,CHUNK_SIZE,f))>0){length+=(unsigned long)count;for(i=0;i<count;++i){hash^=data[i];hash*=16777619UL;}}
fclose(f); sprintf(linebuf,"STAT %lu %08lX\r\n",length,hash); write_text(linebuf);
}
static void command_list(char *args) {
char path[260],pattern[300],output[CHUNK_SIZE]; struct find_t found;
unsigned used=0; int rc;
if(!decode_path(args,path,sizeof(path))){error_text("Invalid path encoding");return;}
strcpy(pattern,path); if(pattern[0]&&pattern[strlen(pattern)-1]!='\\') strcat(pattern,"\\"); strcat(pattern,"*.*");
rc=_dos_findfirst(pattern,_A_NORMAL|_A_RDONLY|_A_HIDDEN|_A_SYSTEM|_A_SUBDIR|_A_ARCH,&found);
while(rc==0) { char entry[100]; int len;
if(strcmp(found.name,".")&&strcmp(found.name,"..")) { sprintf(entry,"%s\t%lu\t%s\n",found.name,found.size,(found.attrib&_A_SUBDIR)?"DIR":"FILE"); len=strlen(entry); if(used+(unsigned)len>=sizeof(output)) break; memcpy(output+used,entry,len); used+=(unsigned)len; }
rc=_dos_findnext(&found);
}
ok_data((unsigned char *)output,used);
}
static void command_exec(char *args) {
char command[700],shell[800]; const char *tmp="C:\\PIEXEC.TMP"; FILE *f; int n,code; size_t count;
n=decode_hex(args,(unsigned char *)command,sizeof(command)-1); if(n<0){error_text("Invalid command encoding");return;} command[n]='\0';
sprintf(shell,"COMMAND.COM /C %s > %s",command,tmp); code=system(shell);
f=fopen(tmp,"rb"); count=f?fread(data,1,CHUNK_SIZE,f):0; if(f)fclose(f); remove(tmp);
sprintf(linebuf,"OK %d ",code); write_text(linebuf); write_hex(data,count); write_text("\r\n");
}
static void process_line(char *line) {
char *cmd=line,*args=strchr(line,' '); if(args)*args++='\0';else args=cmd+strlen(cmd);
if(!strcmp(cmd,"PING"))ok_data((const unsigned char *)"PONG",4);
else if(!strcmp(cmd,"READ"))command_read(args);
else if(!strcmp(cmd,"WRITE"))command_write(args);
else if(!strcmp(cmd,"PUT"))command_put(args);
else if(!strcmp(cmd,"GET"))command_get(args);
else if(!strcmp(cmd,"HASH"))command_hash(args);
else if(!strcmp(cmd,"LIST"))command_list(args);
else if(!strcmp(cmd,"EXEC"))command_exec(args);
else if(!strcmp(cmd,"QUIT")){ok_data((const unsigned char *)"BYE",3);stop_requested=1;}
else { char message[96]; sprintf(message,"Unknown command: %.70s",cmd); error_text(message); }
}
static int connect_host(void) {
IpAddr_t host={10,0,2,2}; int8_t rc;
socketp=TcpSocketMgr::getSocket(); if(!socketp)return -1;
socketp->setRecvBuffer(RECV_SIZE);
rc=socketp->connect(LOCAL_PORT,host,SERVER_PORT,10000);
while(rc==0&&!socketp->isConnectComplete()&&!socketp->isRemoteClosed()&&!stop_requested)drive();
if(socketp->isRemoteClosed()){TcpSocketMgr::freeSocket(socketp);socketp=0;return -1;}
return 0;
}
int main(void) {
int used,rc; uint16_t key;
if(Utils::parseEnv()!=0)return 2;
if(Utils::initStack(1,TCP_SOCKET_RING_SIZE,ctrl_break,ctrl_c))return 3;
cprintf("[TCPAGENT] Connecting to 10.0.2.2:%u\r\n",SERVER_PORT);
while(!stop_requested) {
if(connect_host()!=0){unsigned long spins=0;while(spins++<60000UL&&!stop_requested)drive();continue;}
cprintf("[TCPAGENT] Connected. Alt-X stops.\r\n"); write_text("TCPAGENT READY\r\n"); used=0;
while(!stop_requested&&!socketp->isRemoteClosed()) {
drive(); rc=socketp->recv((uint8_t *)data,CHUNK_SIZE);
if(rc<0)break;
for(int i=0;i<rc;++i) {
int c=data[i];
if(put_remaining) {
unsigned available=(unsigned)(rc-i);
unsigned take=put_remaining<available ? (unsigned)put_remaining : available;
if(fwrite(data+i,1,take,put_file)!=(size_t)take){fclose(put_file);put_file=0;put_remaining=0;error_text("Short write");}
else {
put_remaining-=take; i+=(int)take-1;
if(!put_remaining){fclose(put_file);put_file=0;ok_data((const unsigned char *)"",0);}
}
} else if(c=='\r'||c=='\n') {
if(used){linebuf[used]='\0';process_line(linebuf);used=0;}
} else if(used<LINE_SIZE-1) linebuf[used++]=(char)c;
}
if(_bios_keybrd(1)){key=_bios_keybrd(0);if((key&0xff)==3||(key>>8)==45)stop_requested=1;}
}
socketp->close(); TcpSocketMgr::freeSocket(socketp); socketp=0;
}
Utils::endStack(); return 0;
}