Skip to content

Do not report READY when nothing is listening -- end the STC instead - #112

Merged
mgrossmann merged 2 commits into
mainfrom
issue-111-ready-without-listener
Aug 22, 2026
Merged

Do not report READY when nothing is listening -- end the STC instead#112
mgrossmann merged 2 commits into
mainfrom
issue-111-ready-without-listener

Conversation

@mgrossmann

@mgrossmann mgrossmann commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Closes #111.

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.

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_sock published means we are listening;
the socket thread's termecb posted means it gave up and returned. Both exits
are 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_ex failed, which initialize() does not check)
is the same answer arrived at sooner.

socket_thread() is untouched: termecb is posted by MVS at task end, so the
signal 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 /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. Measured on the failing server:
F FTPDT,SESSIONS answered FTPD015I ACTIVE SESSIONS: 0 / 10, which reads as
a 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 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.

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, so bind() fails
with EADDRNOTAVAIL (49) and socket_thread() gives up at once -- a
permanent, instant failure, unlike the EADDRINUSE path which retries.

Before (c63024d, the current FTPD.LINKLIB):

FTPD001I FTPD 1.0.1-DEV READY
FTPD051E BIND() FAILED ON 10.99.99.99 PORT 2122, ERRNO=49

READY, and the explanation only afterwards. The STC then stayed up, idle.

After (7d60cd8):

FTPD051E BIND() FAILED ON 10.99.99.99 PORT 2122, ERRNO=49
FTPD056E FTPD IS NOT LISTENING ON PORT 2122, THIS INSTANCE ENDS
FTPD099I FTPD SHUTDOWN COMPLETE
IEF404I FTPDT - ENDED - TIME=12.16.03
IEF142I FTPDT FTPDT - STEP WAS EXECUTED - COND CODE 0008

0.6s from S to ended, gone from D A,L, and the refusal in the step code.

Regression, same STC with SRVBIND=ANY restored:

FTPD054I LISTENING ON ANY PORT 2122
FTPD001I FTPD 1.0.1-DEV READY

Banner probe answered 220 FTPD 111 TEST, and /P ended it with
COND CODE 0000. The normal path still announces READY, still serves, and
costs nothing extra.

make test-host 78/78 (unchanged -- this path has no host-testable surface).
src/ftpd.c compiled by hand with -Wall -Werror, clean; the project cflags
carry 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_tries times at bind_sleep seconds, both from Parmlib, default
10 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.

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
@mgrossmann mgrossmann changed the title Do not report READY when nothing is listening Do not report READY when nothing is listening -- end the STC instead Aug 22, 2026
@mgrossmann
mgrossmann merged commit 83caa76 into main Aug 22, 2026
1 check passed
@mgrossmann
mgrossmann deleted the issue-111-ready-without-listener branch August 22, 2026 17:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FTPD001I READY is issued even when the listener never came up

1 participant