Input can be discarded on failed socket writes; reconnect and keepalive paths can silently stop
Clean patch: https://github.com/adkins17/potato/commit/68705bd43fd40ebc1936e50613f060fd1002d82d.patch
Patch branch: https://github.com/adkins17/potato/tree/fix/socket-input-reliability-v2
Compared against upstream: 46d18387bd6b156b85176fa588a60991cbba6235
Summary
Potato can erase a submitted command even when the command is not accepted by the socket layer. The input handler clears the widget before attempting delivery, the intermediate send helpers do not return delivery status, and ioWrite catches and discards every puts error.
This makes a connection appear usable while commands disappear without reaching the world or producing an actionable client error. The race is easier to encounter while switching between connections or when a socket becomes invalid between the UI event and the write.
The same source paths contain related reliability problems:
- EOF detected before
read disconnects without scheduling automatic reconnect.
boot_reconnect calls after cancel conn($c,reconnectId) without dereferencing the timer ID.
- The retry callback invokes
reconnect directly, so a synchronous failed attempt can end the retry loop.
- Keepalive timer IDs are not tracked; duplicate invocations can create multiple loops, while an uncaught per-connection error can stop the loop.
- NOP keepalive is gated on
hasProtocol telnet, although Telnet NOP itself has no option negotiation requirement.
- Null-byte keepalive uses the normal text path, appending the world's configured line ending instead of sending exactly one NUL byte.
- When NOP and null-byte keepalives are both enabled, Potato sends both. Some servers retain the NUL and discard the next command line even though Potato's socket write succeeded.
Deterministic reproduction
The attached patch includes tests/socket-input-reliability.test.tcl, which extracts and executes the actual Potato procedure bodies.
Without the patch:
- Make
ioWrite raise a channel error during sendRaw.
- Submit text through
send_mushage.
- Observe that the input widget has already been cleared and no failure reaches the caller.
- Trigger
boot_reconnect twice and observe that the original timer is not reliably cancelled.
- Enable null-byte keepalive with CRLF and observe
00 0D 0A instead of a single 00.
- Enable both NOP and null-byte keepalive, wait for
FF F1 00, then submit numbered commands. On an affected server, the first command after each NUL is absent from the server command log while the following command succeeds.
Expected behavior
- Input should only be cleared after the local socket write is accepted.
- Rejected input should remain editable and produce a visible warning.
- The focused input widget should determine its owning connection rather than relying only on global active state.
- Unexpected EOF/read/write failures should use one disconnect-and-reconnect path.
- A failed automatic reconnect should re-arm the retry loop while automatic reconnect remains enabled.
- Only one keepalive timer should exist.
- Only one keepalive method should be sent per interval: Telnet NOP (
FF F1) as the primary method, or null (00) as a fallback when NOP is disabled. A user-submitted blank line with CRLF should remain 0D 0A.
Patch behavior
The patch:
- propagates
ioWrite errors and makes sendRaw, send_to_real, send_to, and send_to_noparse return success/failure;
- preserves input when delivery is rejected and shows
Message not sent; input retained.;
- resolves the destination from the input widget that generated the event;
- centralizes unexpected connection-loss handling;
- fixes reconnect timer cancellation and adds a generation-guarded, self-rearming retry callback;
- validates sockets during keepalive and isolates per-connection errors;
- tracks a single keepalive timer;
- sends NOP and NUL using their exact raw wire forms;
- gives NOP precedence and uses NUL only as an exclusive fallback, preventing the fallback byte from consuming the next command on affected servers.
Validation
Validated on Windows with Tcl/Tk 8.6:
- focused reliability test: all 16 assertions passed;
- NOP precedence plus explicit blank-line bytes:
FF F1 0D 0A;
- null-only fallback byte:
00;
- failed-send input retention: passed;
- focused-input connection routing: passed;
- reconnect timer replacement and retry rearming: passed;
- all 341 procedures in the two changed production files parsed and compiled successfully;
git diff --check: clean.
The advanced packet capture also traced two missing commands through Input-Key,
Widget-Resolve, Input-Dispatch, Application-Text, Wire-Write, and Channel-Queue.
Both writes were accepted and drained on the same ordered TCP socket used by
commands that succeeded, but the missing commands never appeared in PennMUSH's
server command log. The loss followed a raw NUL keepalive. After making NOP and
NUL mutually exclusive, five consecutive numbered live tests passed.
Run the included test with:
tclkit-8.6.18-win64-intel-tcl.exe tests\socket-input-reliability.test.tcl
Input can be discarded on failed socket writes; reconnect and keepalive paths can silently stop
Clean patch: https://github.com/adkins17/potato/commit/68705bd43fd40ebc1936e50613f060fd1002d82d.patch
Patch branch: https://github.com/adkins17/potato/tree/fix/socket-input-reliability-v2
Compared against upstream:
46d18387bd6b156b85176fa588a60991cbba6235Summary
Potato can erase a submitted command even when the command is not accepted by the socket layer. The input handler clears the widget before attempting delivery, the intermediate send helpers do not return delivery status, and
ioWritecatches and discards everyputserror.This makes a connection appear usable while commands disappear without reaching the world or producing an actionable client error. The race is easier to encounter while switching between connections or when a socket becomes invalid between the UI event and the write.
The same source paths contain related reliability problems:
readdisconnects without scheduling automatic reconnect.boot_reconnectcallsafter cancel conn($c,reconnectId)without dereferencing the timer ID.reconnectdirectly, so a synchronous failed attempt can end the retry loop.hasProtocol telnet, although Telnet NOP itself has no option negotiation requirement.Deterministic reproduction
The attached patch includes
tests/socket-input-reliability.test.tcl, which extracts and executes the actual Potato procedure bodies.Without the patch:
ioWriteraise a channel error duringsendRaw.send_mushage.boot_reconnecttwice and observe that the original timer is not reliably cancelled.00 0D 0Ainstead of a single00.FF F1 00, then submit numbered commands. On an affected server, the first command after each NUL is absent from the server command log while the following command succeeds.Expected behavior
FF F1) as the primary method, or null (00) as a fallback when NOP is disabled. A user-submitted blank line with CRLF should remain0D 0A.Patch behavior
The patch:
ioWriteerrors and makessendRaw,send_to_real,send_to, andsend_to_noparsereturn success/failure;Message not sent; input retained.;Validation
Validated on Windows with Tcl/Tk 8.6:
FF F1 0D 0A;00;git diff --check: clean.The advanced packet capture also traced two missing commands through Input-Key,
Widget-Resolve, Input-Dispatch, Application-Text, Wire-Write, and Channel-Queue.
Both writes were accepted and drained on the same ordered TCP socket used by
commands that succeeded, but the missing commands never appeared in PennMUSH's
server command log. The loss followed a raw NUL keepalive. After making NOP and
NUL mutually exclusive, five consecutive numbered live tests passed.
Run the included test with: