feat(xce-test-cli-ux): wrap every blocking RPC with the interrupt adapter

Routes all 26 blocking xcecli_* call sites across ~24 commands through
xcetest_run_interruptible(), wires xcetest_set_active_connection() at
execute_line()'s dispatch point (active_ctx->ctx, read fresh per
command), and fixes main()'s REPL loop to distinguish a real EOF from
an EINTR-interrupted readline() so Ctrl-C at an empty prompt discards
the line instead of exiting the session.

Live-discovered and fixed bug-1794: fill_job_alloc() never initializes
xce_job_alloc_t's nres_shared/allocs_shared fields -- harmless as stack
garbage before this task, genuine heap garbage once moved into a
heap-allocated ctx block (a necessary consequence of the adapter's
heap-only precondition). Fixed by switching all ~26 call-context
allocations from malloc to calloc uniformly. Also fixed an argv-aliasing
hazard: cmd_hello/cmd_jobckpt's -m values aliased optarg, which
execute_line() frees immediately after return including on the
abandoned path -- now strdup'd into the heap block.

Also closed a safety-critical gap this task's own design introduced
(bug-1795): before this task the REPL was fully synchronous, so
leave_ctx() (called by up/quit) could never run while an RPC was
outstanding. An abandoned worker can now persist past command
completion, so leave_ctx() tearing down the connection could
use-after-free it. Fixed via xcetest_wait_connection_idle(), which
blocks on the same per-connection semaphore the adapter already
tracks, then releases it, proving no worker is active before teardown.
Proven with a deterministic white-box RED->GREEN test, since the race
itself is too timing-dependent for a black-box test to reliably force.

1361/1361 across the full controller suite (+2), 0 regressions.

Task 3.2 of .kiro/specs/xce-test-cli-ux/tasks.md.
Requirements: 1.1, 1.2, 1.3, 1.4, 1.5

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019jaMf9QEwpugH2W9KAuc7f
This commit is contained in:
Rongsong Shen 2026-09-09 20:58:18 +00:00
parent f152c59e75
commit 45120af2cc
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
7 changed files with 1887 additions and 70 deletions

View file

@ -314,3 +314,41 @@ int xcetest_run_interruptible(xcetest_blocking_fn fn, void* ctx)
xcetest_sleep_poll_interval();
}
}
/* See interruptcall.h for the full contract. Reuses xcetest_conn_lock_for()
* -- the SAME lazy-lookup/create used by xcetest_run_interruptible() above
* -- rather than a parallel registry, so this genuinely observes the same
* per-connection semaphore any in-flight or abandoned worker on `conn_key`
* is holding. */
void xcetest_wait_connection_idle(void* conn_key)
{
sem_t* lock;
int rc;
lock = xcetest_conn_lock_for(conn_key);
if (!lock) {
/* Allocation failure in the lazy-create path: this key has never
* been registered, so by definition no worker could be holding a
* lock for it via this registry. Nothing to wait for. */
return;
}
/* Plain BLOCKING wait, deliberately not the interruptible
* sem_trywait()-poll loop used above -- see interruptcall.h's
* "CONTRACT" section on this function. A signal delivered while
* blocked here can legitimately interrupt sem_wait() with EINTR even
* though nothing else ever posts spuriously; retry rather than treat
* that as "acquired" or bail out early, since either would defeat
* this function's entire purpose (proving the wait genuinely
* completed before the caller proceeds to tear the connection down). */
do {
rc = sem_wait(lock);
} while ((rc != 0) && (errno == EINTR));
assert(rc == 0); /* NDEBUG caveat: see xcetest_worker_main()'s comment. */
/* Immediately release: the semantic is "prove idle right now," not
* "hold the connection locked." */
rc = sem_post(lock);
(void)rc;
assert(rc == 0);
}