TCPAGENT는 system()이 도는 동안 통째로 얼어 있어 응답도 진행 보고도 못 한다. 그런데 호스트는 소켓에 30초 고정 타임아웃을 걸고, 만료되면 연결 자체를 버렸다. 그래서 35초짜리 컴파일이 "느린 명령"이 아니라 "죽은 에이전트"로 취급됐다. QEMU는 게스트가 얼어 있어도 계속 돈다. info blockstats의 idle_time_ns로 "작업 중"과 "멈춤"을 구분한다. 실측으로 확인했다: 에이전트가 완전히 벙어리인 동안에도 rd_operations가 7초당 47000씩 증가하고 idle은 0.00s를 유지한다. - EXEC은 짧은 간격으로 깨어나 감시만 하고 소켓은 절대 안 버린다. - --idle-timeout(기본 60s)과 --hard-timeout(기본 900s). 후자는 디스크를 안 쓰는 CPU 바운드 멈춤용 백스톱이다. - 중단은 QEMU 모니터로 Ctrl+C를 주입하고 COMMAND.COM의 "Terminate batch file (Y/N/A)?" 프롬프트에 답한다. - Ctrl+C는 DOS break check에서만 먹는다. FreeDOS 기본값 BREAK=OFF에서 출력을 파일로 돌린 CPU 바운드 자식은 거기 도달 안 할 수 있다. 그래서 중단은 보장이 아니라 요청으로 다루고, 명령이 안 멈춰도 RESULT를 끝까지 수거해 스트림을 깨뜨리지 않는다. - ferro-vm abort 추가. 실행 중에도 응답해야 하므로 파이프 서버를 요청당 스레드로 바꿨다. - 5558 바인딩을 SO_EXCLUSIVEADDRUSE로. Windows의 SO_REUSEADDR는 다른 프로세스가 같은 포트를 잡아 조용히 반쯤 동작하게 만든다. 검증 (QEMU FreeDOS 실측): - 32.4초 명령 정상 완료 (이전에는 30초에 실패) - 실행 중 abort가 0.1초에 응답, exit=95로 종료, 부분 출력 1805B 수거, 연결 유지 - pause처럼 디스크를 안 쓰는 명령을 idle 15s로 검출해 중단 시리얼 시절에 있다가 TCP 전환에서 사라진 TODO 3건을 복구한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012PQm6oAvWX4Lp3iSN5AHGT
102 lines
4.4 KiB
Markdown
102 lines
4.4 KiB
Markdown
# 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 the QEMU host at
|
|
`10.0.2.2:5558`.
|
|
|
|
The Windows-only Python `ferro-vm` daemon owns that listener. It logs metadata
|
|
and decoded command output to `.qemu/ferro-vm.log`; it does not expose an
|
|
observer/controller TCP port or emit binary payloads to the log. Local host
|
|
control uses a Windows named pipe.
|
|
|
|
## 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
|
|
|
|
`PING`, `READ`, `WRITE`, and `LIST` use text commands. `EXEC` captures both
|
|
stdout and stderr at the DOS handle level and returns an untruncated raw body:
|
|
|
|
- `EXEC <hex command>\n` -> `RESULT <exit> <length> <flags>\r\n<raw bytes>`
|
|
|
|
Fast transfer commands are:
|
|
|
|
- `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`
|
|
|
|
The host invokes them through:
|
|
|
|
```powershell
|
|
uv run ferro-vm put host-file 'C:\DOS\FILE'
|
|
uv run ferro-vm get 'C:\DOS\FILE' host-file
|
|
```
|
|
|
|
## Agent-side logging
|
|
|
|
The foreground agent prints one timestamped line per event on the VGA console
|
|
and keeps the same text in `C:\TCPAGENT.LOG`, rotating files larger than 256 KiB
|
|
to `C:\TCPAGENT.OLD`. Payloads and command output are never written to that
|
|
metadata log.
|
|
|
|
Every command is logged with a request line and a result line carrying byte
|
|
counts and elapsed time — `EXEC`, `PUT`, `GET`, `HASH`, `LIST`, `READ`, and
|
|
`WRITE`. `PING` is deliberately excluded because `wait-ready` polls it twice a
|
|
second. Connection events (`connecting`, `connected`, `connect failed; retry N`,
|
|
`link lost`) are logged too; those are invisible to the host by definition,
|
|
since they happen when the socket is down.
|
|
|
|
Lines are colored by writing VGA attribute bytes after `cprintf` lays out the
|
|
line: gray timestamps, cyan requests, yellow `EXEC` command text, green success,
|
|
red failure. Open Watcom's DOS `conio.h` has no `textattr()`, and ANSI escapes
|
|
are not interpreted on this FreeDOS console, so neither of the usual routes
|
|
works. Elapsed times come from the BIOS tick counter at 18.2065 Hz (~55 ms
|
|
resolution).
|
|
|
|
## Long commands
|
|
|
|
mTCP is only driven when the agent calls it, and `system()` freezes the agent
|
|
for the entire child command. So during a long `EXEC` the DOS side is mute: it
|
|
cannot answer, cannot acknowledge, cannot report progress. Silence therefore
|
|
proves nothing about whether the command is healthy.
|
|
|
|
The host must not read that silence as failure. `ferro-vm exec` waits on
|
|
QEMU's own view of the guest instead: `info blockstats` keeps counting while
|
|
the agent is frozen, and `idle_time_ns` distinguishes a slow command from a
|
|
stuck one. See `--idle-timeout` and `--hard-timeout` in `ferro-vm exec --help`.
|
|
|
|
When the host does decide to stop a command it injects Ctrl+C through the QEMU
|
|
monitor, then answers COMMAND.COM's `Terminate batch file (Y/N/A)?` prompt.
|
|
That is a request, not a guarantee: Ctrl+C only lands at a DOS break check, and
|
|
with `BREAK=OFF` (the FreeDOS default in `C:\FDCONFIG.SYS`) a compute-bound
|
|
child whose output we redirected to a file may never reach one. The host keeps
|
|
collecting the result either way rather than abandoning a stream that still
|
|
owes it a `RESULT`.
|
|
|
|
Adding `BREAK=ON` to `C:\FDCONFIG.SYS` would make DOS check on every system
|
|
call and so make Ctrl+C reliable, at a small cost to every DOS call.
|
|
|
|
## Rebuilding inside the VM
|
|
|
|
`REBUILD.BAT` compiles and installs the agent in a single `exec`. `BUILD.BAT`
|
|
only runs `wmake` in the current directory, which is not where a host-driven
|
|
`ferro-vm exec` starts.
|
|
|
|
```powershell
|
|
uv run ferro-vm put tools/tcpagent/tcpagent.cpp 'C:\MTSRC\MTCP\APPS\TCPAGENT\TCPAGENT.CPP'
|
|
uv run ferro-vm put tools/tcpagent/REBUILD.BAT 'C:\REBUILD.BAT'
|
|
uv run ferro-vm exec 'C:\REBUILD.BAT'
|
|
uv run ferro-vm reset
|
|
```
|
|
|
|
The reset is required: the running agent holds the old image in memory, and
|
|
`C:\FDAUTO.BAT` starts it at boot.
|