diff --git a/tools/dos_stage.py b/tools/dos_stage.py new file mode 100644 index 0000000..b022d13 --- /dev/null +++ b/tools/dos_stage.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Stage changed source files into the running FreeDOS VM over DOSAGENT. + +Usage: python tools/dos_stage.py host/file=C:\\FEC\\DEST.FILE [...] +The DOS agent uses a 115200-baud virtual COM1 link and accepts at most 4096 +binary payload bytes per ASCII-hex WRITE request. +""" +import hashlib +import socket +import sys +from pathlib import Path + +HOST = "127.0.0.1" +PORT = 5555 +CHUNK_SIZE = 4096 + + +def request(line: str) -> str: + with socket.create_connection((HOST, PORT), timeout=30) as sock: + sock.sendall((line + "\n").encode("ascii")) + response = bytearray() + while not response.endswith(b"\n"): + part = sock.recv(65536) + if not part: + break + response.extend(part) + text = response.decode("ascii", "strict").strip() + if text.startswith("ERR "): + raise RuntimeError(bytes.fromhex(text[4:]).decode("utf-8", "replace")) + if not text.startswith("OK"): + raise RuntimeError("unexpected DOSAGENT response: " + text) + return text + + +def stage(local: Path, remote: str) -> 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] + mode = "T" if offset == 0 else "A" + request("WRITE %s %s %s" % (remote_hex, mode, part.hex().upper())) + remote_data = bytearray() + offset = 0 + while True: + response = request("READ %s %u" % (remote_hex, offset)) + _, eof, payload = response.split(" ", 2) + piece = bytes.fromhex(payload) + remote_data.extend(piece) + offset += len(piece) + if eof == "1": + break + if bytes(remote_data) != data: + raise RuntimeError("verification mismatch for " + remote) + print("staged %s -> %s (%d bytes, sha256 %s)" % ( + local, remote, len(data), hashlib.sha256(data).hexdigest())) + + +def main() -> int: + if len(sys.argv) < 2: + print("usage: dos_stage.py LOCAL=DOS_PATH [... ]", file=sys.stderr) + return 2 + for spec in sys.argv[1:]: + if "=" not in spec: + raise SystemExit("missing '=' in " + spec) + local, remote = spec.split("=", 1) + stage(Path(local), remote) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/dosagent.c b/tools/dosagent.c new file mode 100644 index 0000000..1c94073 --- /dev/null +++ b/tools/dosagent.c @@ -0,0 +1,243 @@ +#include +#include +#include +#include +#include +#include + +#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; +}