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

4.4 KiB

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:

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.

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.