feat: color the agent console and log every command
DOS 콘솔에서 직접 보기 위한 네 가지 수정. 1. 컬러. Open Watcom의 conio.h에는 textattr()이 없고(cprintf/cputs/getch만 제공) 이 FreeDOS 콘솔은 ANSI 이스케이프도 해석하지 않는다. 그래서 cprintf가 줄을 배치하게 두고 -- 스크롤을 알아서 처리한다 -- 방금 쓴 셀의 VGA 속성 바이트만 다시 칠한다. 커서가 그 줄 다음 행의 0열에 있다는 점을 이용해 스크롤을 직접 추적하지 않고 대상 셀을 찾는다. 줄바꿈된 긴 줄도 처리한다. 2. GET, HASH, LIST, READ, WRITE, QUIT, 미지원 명령을 로깅한다. 이전에는 EXEC과 PUT만 보였다. PING은 wait-ready가 초당 두 번 폴링하므로 제외한다. LIST는 잘림 여부를, PUT은 short write를 구분해 남긴다. 3. put_path가 채워지기만 하고 쓰이지 않아 PUT 완료 줄에 경로가 없었다. 완료 경로가 둘(길이 0, 본문 수신 완료)이라 put_finished()로 합쳤다. 4. 틱->초 변환이 1.1% 빨랐다. BIOS 틱은 18.2065Hz이므로 delta/18이 아니라 delta*549/100 (하루치 틱에도 32비트를 넘지 않는다) 을 쓴다. REBUILD.BAT을 추가한다. BUILD.BAT은 현재 디렉터리에서 wmake만 실행하는데 호스트가 exec으로 부를 때의 시작 디렉터리가 거기가 아니다. QEMU FreeDOS에서 Open Watcom C++16으로 빌드하고(no warnings, -we 활성) 콘솔 스크린샷으로 색상, 줄바꿈 색칠, 명령별 로그를 확인했다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012PQm6oAvWX4Lp3iSN5AHGT
This commit is contained in:
@@ -40,7 +40,43 @@ uv run ferro-vm put host-file 'C:\DOS\FILE'
|
||||
uv run ferro-vm get 'C:\DOS\FILE' host-file
|
||||
```
|
||||
|
||||
The foreground agent shows timestamped connection, transfer, and command
|
||||
start/finish lines. It also keeps the same metadata 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.
|
||||
## 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).
|
||||
|
||||
Note that mTCP is not driven while `system()` runs a child, so a DOS command
|
||||
lasting tens of seconds can drop the TCP connection. The agent logs `link lost`
|
||||
and reconnects on its own, but the host loses that command's result.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
@echo off
|
||||
rem Rebuild TCPAGENT.EXE from C:\MTSRC and install it, in one EXEC.
|
||||
rem BUILD.BAT only runs wmake in the current directory, which is not where a
|
||||
rem host-driven `ferro-vm exec` starts. Copy this to C:\ and run it by path.
|
||||
rem The running agent keeps the old image in memory, so reset the VM afterwards:
|
||||
rem uv run ferro-vm reset
|
||||
C:
|
||||
cd C:\MTSRC\MTCP\APPS\TCPAGENT
|
||||
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=
|
||||
if exist TCPAGENT.OBJ del TCPAGENT.OBJ
|
||||
if exist TCPAGENT.EXE del TCPAGENT.EXE
|
||||
wmake
|
||||
if not exist TCPAGENT.EXE goto fail
|
||||
copy /Y TCPAGENT.EXE C:\FREEDOS\BIN\TCPAGENT.EXE
|
||||
echo BUILD-OK
|
||||
goto end
|
||||
:fail
|
||||
echo BUILD-FAILED
|
||||
:end
|
||||
+78
-35
@@ -38,17 +38,42 @@ static unsigned long ticks(void) {
|
||||
_bios_timeofday(_TIME_GETCLOCK,&value);
|
||||
return (unsigned long)value;
|
||||
}
|
||||
/* The BIOS tick is 18.2065 Hz, so one tick is 5.49254 hundredths of a second.
|
||||
549/100 keeps the error under 0.05% and cannot overflow 32 bits for a delta
|
||||
up to a full day (1573040 ticks * 549 fits). Plain delta/18 ran 1.1% fast. */
|
||||
static void elapsed_text(unsigned long started,char *out) {
|
||||
unsigned long now=ticks(),delta=now>=started?now-started:now+(1573040UL-started);
|
||||
sprintf(out,"%lu.%02lus",delta/18UL,(delta%18UL)*100UL/18UL);
|
||||
unsigned long hundredths=delta*549UL/100UL;
|
||||
sprintf(out,"%lu.%02lus",hundredths/100UL,hundredths%100UL);
|
||||
}
|
||||
|
||||
/* Open Watcom's DOS conio has no textattr()/textcolor() -- it offers only
|
||||
cprintf/cputs/getch and friends -- and ANSI escapes are not interpreted on
|
||||
the installed FreeDOS console. So let cprintf lay the line out (it scrolls
|
||||
correctly) and then repaint the attribute bytes of the cells it just wrote.
|
||||
The cursor sits at column 0 of the row after the line, which is what lets us
|
||||
find those cells without tracking scrolling ourselves. */
|
||||
#define TIMESTAMP_WIDTH 9
|
||||
static void colorize(unsigned total,int attr) {
|
||||
unsigned char __far *vram; unsigned cols,used,row,col,start,k;
|
||||
if(*(unsigned char __far *)MK_FP(0x0040,0x0049)==7) return; /* MDA: no color */
|
||||
cols=*(unsigned __far *)MK_FP(0x0040,0x004A);
|
||||
if(cols<40||cols>132) cols=80;
|
||||
used=(total+cols-1)/cols; if(!used) used=1;
|
||||
row=*(unsigned char __far *)MK_FP(0x0040,0x0051);
|
||||
if(row<used) return; /* line scrolled off the top; nothing to paint */
|
||||
start=row-used;
|
||||
vram=(unsigned char __far *)MK_FP(0xB800,0);
|
||||
for(k=0;k<total;++k) {
|
||||
row=start+k/cols; col=k%cols;
|
||||
vram[((unsigned)row*cols+col)*2+1]=(unsigned char)(k<TIMESTAMP_WIDTH?0x08:attr);
|
||||
}
|
||||
}
|
||||
static void log_line(int attr,const char *fmt,...) {
|
||||
struct dostime_t now; va_list ap; char text[760];
|
||||
_dos_gettime(&now); va_start(ap,fmt); vsprintf(text,fmt,ap); va_end(ap);
|
||||
/* Open Watcom's DOS conio has no textattr(), and ANSI escapes are not
|
||||
interpreted on the installed FreeDOS console. Keep output clean. */
|
||||
(void)attr;
|
||||
cprintf("%02u:%02u:%02u %s\r\n",now.hour,now.minute,now.second,text);
|
||||
colorize(TIMESTAMP_WIDTH+(unsigned)strlen(text),attr);
|
||||
{ FILE *f=fopen("C:\\TCPAGENT.LOG","a");
|
||||
if(f){fprintf(f,"%02u:%02u:%02u %s\n",now.hour,now.minute,now.second,text);fclose(f);} }
|
||||
}
|
||||
@@ -59,6 +84,12 @@ static void init_log(void) {
|
||||
f=fopen("C:\\TCPAGENT.LOG","a");
|
||||
if(f){fputs("--- TCPAGENT start ---\n",f);fclose(f);}
|
||||
}
|
||||
/* PUT completes either here in command_put (zero length) or in the receive loop
|
||||
once the body arrives, so keep the one line both paths emit in one place. */
|
||||
static void put_finished(void) {
|
||||
char elapsed[24]; elapsed_text(put_started,elapsed);
|
||||
log_line(0x0A,"< PUT %s OK %s",put_path,elapsed);
|
||||
}
|
||||
|
||||
void __interrupt __far ctrl_break(void) { stop_requested=1; }
|
||||
void __interrupt __far ctrl_c(void) { stop_requested=1; }
|
||||
@@ -120,60 +151,71 @@ static int decode_path(const char *hex,char *path,int cap) {
|
||||
}
|
||||
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;}
|
||||
if(!off){log_line(0x0C,"< READ ERR missing offset");error_text("READ requires path and offset");return;} *off++='\0';
|
||||
if(!decode_path(args,path,sizeof(path))){log_line(0x0C,"< READ ERR bad path encoding");error_text("Invalid path encoding");return;}
|
||||
pos=atol(off); log_line(0x0B,"> READ %s @%ld",path,pos);
|
||||
f=fopen(path,"rb"); if(!f){log_line(0x0C,"< READ ERR cannot open");error_text("Cannot open file");return;}
|
||||
if(fseek(f,pos,SEEK_SET)){fclose(f);log_line(0x0C,"< READ ERR cannot seek");error_text("Cannot seek file");return;}
|
||||
count=fread(data,1,CHUNK_SIZE,f); eof=count<CHUNK_SIZE; fclose(f);
|
||||
log_line(0x0A,"< READ %uB eof=%d",(unsigned)count,eof);
|
||||
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);
|
||||
if(!mode){log_line(0x0C,"< WRITE ERR missing mode");error_text("WRITE requires path, mode and data");return;} *mode++='\0';
|
||||
payload=strchr(mode,' '); if(!payload){log_line(0x0C,"< WRITE ERR missing data");error_text("WRITE requires data");return;} *payload++='\0';
|
||||
if(!decode_path(args,path,sizeof(path))){log_line(0x0C,"< WRITE ERR bad path encoding");error_text("Invalid path encoding");return;}
|
||||
count=decode_hex(payload,data,CHUNK_SIZE); if(count<0){log_line(0x0C,"< WRITE ERR bad data encoding");error_text("Invalid data encoding");return;}
|
||||
log_line(0x0B,"> WRITE %s %c %dB",path,mode[0]=='A'?'A':'T',count);
|
||||
f=fopen(path,mode[0]=='A'?"ab":"wb"); if(!f){log_line(0x0C,"< WRITE ERR cannot open");error_text("Cannot write file");return;}
|
||||
if(count&&fwrite(data,1,count,f)!=(size_t)count){fclose(f);log_line(0x0C,"< WRITE ERR short write");error_text("Short write");return;}
|
||||
fclose(f); log_line(0x0A,"< WRITE OK"); 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;}
|
||||
if(!length_text){log_line(0x0C,"< PUT ERR missing length");error_text("PUT requires path and length");return;}
|
||||
*length_text++='\0';
|
||||
if(!decode_path(args,path,sizeof(path))){error_text("Invalid path encoding");return;}
|
||||
if(!decode_path(args,path,sizeof(path))){log_line(0x0C,"< PUT ERR bad path encoding");error_text("Invalid path encoding");return;}
|
||||
put_remaining=strtoul(length_text,0,10); strcpy(put_path,path); put_started=ticks();
|
||||
log_line(0x0B,"> PUT %s %luB",path,put_remaining);
|
||||
log_line(0x0B,"> PUT %s %luB",put_path,put_remaining);
|
||||
put_file=fopen(path,"wb");
|
||||
if(!put_file){put_remaining=0;log_line(0x0C,"< PUT ERR cannot open");error_text("Cannot write file");return;}
|
||||
if(!put_remaining){char elapsed[24];fclose(put_file);put_file=0;elapsed_text(put_started,elapsed);log_line(0x0A,"< PUT OK %s",elapsed);ok_data((const unsigned char *)"",0);}
|
||||
if(!put_file){put_remaining=0;log_line(0x0C,"< PUT %s ERR cannot open",put_path);error_text("Cannot write file");return;}
|
||||
if(!put_remaining){fclose(put_file);put_file=0;put_finished();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;}
|
||||
char path[260],elapsed[24]; FILE *f; long length; size_t count; unsigned long started=ticks();
|
||||
if(!decode_path(args,path,sizeof(path))){log_line(0x0C,"< GET ERR bad path encoding");error_text("Invalid path encoding");return;}
|
||||
log_line(0x0B,"> GET %s",path);
|
||||
f=fopen(path,"rb"); if(!f){log_line(0x0C,"< GET %s ERR cannot open",path);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);
|
||||
fclose(f); elapsed_text(started,elapsed);
|
||||
log_line(0x0A,"< GET OK %ldB %s",length,elapsed);
|
||||
}
|
||||
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;}
|
||||
char path[260],elapsed[24]; FILE *f; size_t count; unsigned i;
|
||||
unsigned long length=0,hash=2166136261UL,started=ticks();
|
||||
if(!decode_path(args,path,sizeof(path))){log_line(0x0C,"< HASH ERR bad path encoding");error_text("Invalid path encoding");return;}
|
||||
log_line(0x0B,"> HASH %s",path);
|
||||
f=fopen(path,"rb"); if(!f){log_line(0x0C,"< HASH %s ERR cannot open",path);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);
|
||||
fclose(f); elapsed_text(started,elapsed);
|
||||
log_line(0x0A,"< HASH %luB %08lX %s",length,hash,elapsed);
|
||||
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;}
|
||||
unsigned used=0,entries=0; int rc,truncated=0;
|
||||
if(!decode_path(args,path,sizeof(path))){log_line(0x0C,"< LIST ERR bad path encoding");error_text("Invalid path encoding");return;}
|
||||
log_line(0x0B,"> LIST %s",path);
|
||||
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; }
|
||||
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)) {truncated=1;break;} memcpy(output+used,entry,len); used+=(unsigned)len; ++entries; }
|
||||
rc=_dos_findnext(&found);
|
||||
}
|
||||
log_line(truncated?0x0E:0x0A,"< LIST %u entries%s",entries,truncated?" (truncated)":"");
|
||||
ok_data((unsigned char *)output,used);
|
||||
}
|
||||
static void command_exec(char *args) {
|
||||
@@ -202,6 +244,7 @@ static void command_exec(char *args) {
|
||||
}
|
||||
static void process_line(char *line) {
|
||||
char *cmd=line,*args=strchr(line,' '); if(args)*args++='\0';else args=cmd+strlen(cmd);
|
||||
/* PING is deliberately not logged: wait-ready polls it twice a second. */
|
||||
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);
|
||||
@@ -210,8 +253,8 @@ static void process_line(char *line) {
|
||||
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); }
|
||||
else if(!strcmp(cmd,"QUIT")){log_line(0x07,"* QUIT received");ok_data((const unsigned char *)"BYE",3);stop_requested=1;}
|
||||
else { char message[96]; sprintf(message,"Unknown command: %.70s",cmd); log_line(0x0C,"< ERR %s",message); error_text(message); }
|
||||
}
|
||||
static int connect_host(void) {
|
||||
IpAddr_t host={10,0,2,2}; int8_t rc;
|
||||
@@ -241,10 +284,10 @@ int main(void) {
|
||||
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");}
|
||||
if(fwrite(data+i,1,take,put_file)!=(size_t)take){fclose(put_file);put_file=0;put_remaining=0;log_line(0x0C,"< PUT %s ERR short write (disk full?)",put_path);error_text("Short write");}
|
||||
else {
|
||||
put_remaining-=take; i+=(int)take-1;
|
||||
if(!put_remaining){char elapsed[24];fclose(put_file);put_file=0;elapsed_text(put_started,elapsed);log_line(0x0A,"< PUT OK %s",elapsed);ok_data((const unsigned char *)"",0);}
|
||||
if(!put_remaining){fclose(put_file);put_file=0;put_finished();ok_data((const unsigned char *)"",0);}
|
||||
}
|
||||
} else if(c=='\r'||c=='\n') {
|
||||
if(used){linebuf[used]='\0';process_line(linebuf);used=0;}
|
||||
|
||||
Reference in New Issue
Block a user