Do not report READY when nothing is listening -- end the STC instead - #112
Merged
Conversation
main() waited up to 5 seconds for the socket thread to publish listen_sock and then issued FTPD001I READY whether or not it ever appeared. The loop was a timeout, not a check: nothing looked at why it ended. When the bind fails, socket_thread() logs and returns 8 without signalling shutdown -- deliberate, so the operator can read the error and /P -- so the main event loop stayed up, answered MODIFY, and called itself READY while accepting nothing. Worse than a wrong message, it was a wrong message in the wrong order. The bind retry path outlasts 5 seconds, so READY was printed BEFORE the FTPD051E that explains the failure. The console read like a healthy start and the contradiction arrived afterwards, where a busy console scrolls it away. The wait now ends on either of the two things that can actually happen, and which one it was decides what is announced: listen_sock published means we are listening; the socket thread's termecb posted means it gave up and returned. Both exits are immediate. A NULL sock_task -- cthread_create_ex failed, which initialize() does not check -- is the same answer arrived at sooner. Not listening now says so, with the port, as FTPD056E. Without it an idle FTPD is indistinguishable from a working one across the console: the command handler stays up either way and answers STATS, SESSIONS and CONFIG exactly the same. The 20 second cap is a backstop against a socket thread that neither listens nor ends, not the normal path. It is 20 rather than 5 because it has to outlast what it is waiting on -- sweep, 2s settle, rebind, 10s wait, rebind. Closes #111 Claude-Session: https://claude.ai/code/session_01A6q4hLaokSJBeifJDkNJtH
An FTPD that cannot listen has nothing left to do. There is no console command that retries the bind, so /P and /S is the only way back either way -- and until the operator does that, the idle address space answers STATS, SESSIONS and CONFIG exactly like a working one. Reporting FTPD056E and staying up, as the first cut of this fix did, made the state legible but not shorter. The socket thread returning without signalling shutdown was never a decision about how a failed start should behave. It came from 72e8a7a, the S0A3 fix (#1), whose own second point taught terminate() to wait for sock_task->termecb before DETACH -- an un-DETACHed subtask at end-of-task was the actual abend. With that in place, ending here is safe: terminate() finds the socket thread already gone and moves straight on. HTTPD ends its address space in the same situation (`if (!httpd->listen) goto cleanup`) and carries the refusal out as the step code. main() returned 0 whatever happened, so a start that never listened still ended COND CODE 0000 -- the one part of a start an automation product can read without parsing the console. It now returns 8. Claude-Session: https://claude.ai/code/session_01A6q4hLaokSJBeifJDkNJtH
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #111.
main()waited up to 5 seconds for the socket thread to publishlisten_sockand then issued
FTPD001I READYwhether or not it ever appeared. The loop was atimeout, not a check -- nothing looked at why it ended.
Two commits: the first stops the wrong message, the second stops the wrong
state behind it.
Wait on what actually happens
The wait now ends on either of the two things that can, and which one it was
decides what follows:
listen_sockpublished means we are listening;the socket thread's
termecbposted means it gave up and returned. Both exitsare immediate. The 20 second cap is only a backstop against a socket thread
that neither listens nor ends -- 20 rather than 5 because it has to outlast
what it waits on (sweep, 2s settle, rebind, 10s wait, rebind, #109). A NULL
sock_task(cthread_create_exfailed, whichinitialize()does not check)is the same answer arrived at sooner.
socket_thread()is untouched:termecbis posted by MVS at task end, so thesignal was already there.
terminate()already reads the same bit.Then end, rather than sit there
An FTPD that cannot listen has nothing left to do. No console command retries
the bind, so
/Pand/Sis the only way back either way -- and until theoperator does that, the idle address space answers STATS, SESSIONS and CONFIG
exactly like a working one. Measured on the failing server:
F FTPDT,SESSIONSansweredFTPD015I ACTIVE SESSIONS: 0 / 10, which reads asa quiet server rather than a dead one. Reporting the state without ending it
made it legible but no shorter.
The socket thread returning without signalling shutdown was never a decision
about how a failed start should behave. It comes from
72e8a7a, the S0A3 fix(#1), whose own second point taught
terminate()to wait forsock_task->termecbbefore DETACH -- an un-DETACHed subtask at end-of-task wasthe actual abend. With that in place, ending here is safe:
terminate()findsthe socket thread already gone and moves straight on.
HTTPD ends its address space in the same situation (
if (!httpd->listen) goto cleanup) and carries the refusal out as the step code.main()returned 0whatever happened, so a start that never listened still ended COND CODE 0000 --
the one part of a start an automation product can read without parsing the
console. It now returns 8.
Verified on MVS (mvsdev, MVS/CE, 2026-08-22)
Reproduced first, then fixed. Throwaway STC on port 2122 with
SRVBIND=10.99.99.99, an address the host does not have, sobind()failswith
EADDRNOTAVAIL(49) andsocket_thread()gives up at once -- apermanent, instant failure, unlike the EADDRINUSE path which retries.
Before (
c63024d, the currentFTPD.LINKLIB):READY, and the explanation only afterwards. The STC then stayed up, idle.
After (
7d60cd8):0.6s from
Sto ended, gone fromD A,L, and the refusal in the step code.Regression, same STC with
SRVBIND=ANYrestored:Banner probe answered
220 FTPD 111 TEST, and/Pended it withCOND CODE 0000. The normal path still announces READY, still serves, andcosts nothing extra.
make test-host78/78 (unchanged -- this path has no host-testable surface).src/ftpd.ccompiled by hand with-Wall -Werror, clean; the projectcflagscarry neither, so CI does not enforce them.
Test STC proc and config data set were deleted afterwards; port 2122 refuses
connections and the live FTPD on 2121 answers normally.
Not in this PR
FTPD retries a failed bind exactly once, after 10 seconds, hardcoded. HTTPD
retries
bind_triestimes atbind_sleepseconds, both from Parmlib, default10 and 10. Now that a failed bind ends the STC, that difference decides how
much transient trouble a start survives -- worth its own issue rather than
being folded in here.