diff --git a/tools/tcpagent/README.md b/tools/tcpagent/README.md index 2261744..0b2487f 100644 --- a/tools/tcpagent/README.md +++ b/tools/tcpagent/README.md @@ -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. diff --git a/tools/tcpagent/REBUILD.BAT b/tools/tcpagent/REBUILD.BAT new file mode 100644 index 0000000..989d172 --- /dev/null +++ b/tools/tcpagent/REBUILD.BAT @@ -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 diff --git a/tools/tcpagent/tcpagent.cpp b/tools/tcpagent/tcpagent.cpp index 2aff406..574316d 100644 --- a/tools/tcpagent/tcpagent.cpp +++ b/tools/tcpagent/tcpagent.cpp @@ -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 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 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 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