Files
doslang-mirror/tools/tcpagent/README.md
T
coolguyandClaude Opus 5 ec48a60e3e feat: supervise EXEC with guest liveness instead of a stopwatch
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
2026-08-16 17:42:43 +09:00

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.