From bc5c669b94beede8be82b1b2af41234a1595066b Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Thu, 20 Aug 2026 11:47:12 +0000 Subject: [PATCH] feat(build-time-versioning): worker --version and startup log Worker uses plain getopt_long(), not argp, so it has no free --version handling like controller/scheduler. Adds a long-only OPT_VERSION=257 option and a handling case that computes xce_compose_version() into a static buffer and returns before any startup work (check_user()/ xcecd_init/xcecd_start). Both startup INFO log branches (--peers and no-peers) extended to log the identical buffer. Live-verified: --version and both startup-log branches are byte-identical (captured via a real daemon run under sudo). Also updates bug-1519 (pre-existing, unrelated ASAN "Joining already joined thread" abort during worker shutdown): independently reproduced under real root during this task's review, refining its prior "never reproduced under real root" note -- not a userns/unshare artifact, still unfixed, does not affect this task's code path. Task 3.3/13 of build-time-versioning. This closes out the "Core: C daemon version display" phase (tasks 3.1-3.3). Co-Authored-By: Claude Sonnet 5 --- .kiro/specs/build-time-versioning/tasks.md | 8 ++--- .wolf/buglog.json | 6 ++-- src/worker/src/main.c | 42 +++++++++++++++++----- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/.kiro/specs/build-time-versioning/tasks.md b/.kiro/specs/build-time-versioning/tasks.md index 9670ba1c..77e828d0 100644 --- a/.kiro/specs/build-time-versioning/tasks.md +++ b/.kiro/specs/build-time-versioning/tasks.md @@ -40,10 +40,10 @@ - _Boundary: controller/main.c, scheduler/main.c, controller/testsuite/test.c_ - _Depends: 3.1_ -- [ ] 3.3 (P) Worker: add --version and startup log line - - Add a long-only `--version` option to worker's `getopt_long()` option table and a handling case that prints the same macro output and exits before any other startup work - - Add the version string to worker's existing first startup INFO log line (its own `"HA cluster config..."` line) - - Observable: `xcecd --version` prints commit id + build time and exits without starting the daemon; the startup log's first lines include the identical two values +- [x] 3.3 (P) Worker: add --version and startup log line + - Added `OPT_VERSION=257` (long-only, beyond ASCII range, no collision with `"n:l:vh"`/`OPT_PEERS=256`); handling case computes `xce_compose_version()` into a `static char version_buf[128]` and returns before any startup work (`check_user()`/logdir/`xcecd_init`/`xcecd_start`) + - Both startup INFO log branches (`--peers` and no-`--peers`) extended to log the same buffer + - Observable confirmed live (independently re-verified by review): `xcecd --version` prints and exits; both startup log branches (captured via a real `sudo` daemon run) are byte-identical to `--version` output - _Requirements: 1.1, 1.2, 2.1, 2.2, 3.1, 3.2, 4.3_ - _Boundary: worker/main.c_ - _Depends: 3.1_ diff --git a/.wolf/buglog.json b/.wolf/buglog.json index db61ce69..47098437 100644 --- a/.wolf/buglog.json +++ b/.wolf/buglog.json @@ -15027,7 +15027,7 @@ "error_message": "==ERROR: AddressSanitizer: Joining already joined thread, aborting.", "file": "src/worker/src/main.c (xcecd daemon startup)", "root_cause": "UNRESOLVED / not root-caused. xcecd's root check (main.c:288 geteuid()!=0) was bypassed for sandbox testing via `unshare -r` (user-namespace fake root, euid=0 but not real root). Startup then immediately hit an ASAN double-pthread_join abort during/after the HA-cluster-config init log line (main.c:435), before any further logging. Never reproduced under real root -- unknown whether this is a genuine double-join bug in xcecd's daemonize/thread-spawn path (e.g. duplicate cleanup on an early-init error branch) or an artifact of unshare's user-namespace semantics (e.g. a syscall xcecd's threading depends on behaving differently under CLONE_NEWUSER, such as thread creation ordering or signal delivery). Blocked further component-failover-adoption Task 3.3 Item-3 E2E validation from getting a live worker registered, which in turn meant no real job ever produced a scheduler decision (picker/allocator never had a matching candidate to dispatch to or explicitly PEND against), so the scheduler's decision-publish/rejoin code path (task 3.2) could not be exercised via a fully live job in this sandbox.", - "fix": "Not fixed -- out of scope for Task 3.3 (which validates scheduler HA reconnect, not worker daemon startup). Worked around by validating scheduler join/failover/reconnect behavior via direct socket-state inspection (ss/proc fd) instead of a full live job pipeline; the decision-publish retry+rejoin logic itself already has white-box unit coverage from task 3.2 (test_scheduler_decision_publish_retry.c) and task 3.3 item 2 (test_scheduler_backpressure.c). If a future session needs a live worker in this sandbox, retry under real root (not unshare -r) first to isolate whether this is userns-specific.", + "fix": "Not fixed -- out of scope for Task 3.3 (which validates scheduler HA reconnect, not worker daemon startup). Worked around by validating scheduler join/failover/reconnect behavior via direct socket-state inspection (ss/proc fd) instead of a full live job pipeline; the decision-publish retry+rejoin logic itself already has white-box unit coverage from task 3.2 (test_scheduler_decision_publish_retry.c) and task 3.3 item 2 (test_scheduler_backpressure.c). If a future session needs a live worker in this sandbox, retry under real root (not unshare -r) first to isolate whether this is userns-specific. UPDATE (2026-08-20, build-time-versioning task 3.3 review): independently reproduced under REAL sudo root (not unshare -r) -- same signature, same timing (fires during later thread-join startup work, strictly AFTER the HA-cluster-config INFO log line, both with and without --peers). This refines/answers the prior 'never reproduced under real root' open question: it is NOT a userns/unshare-specific artifact -- the abort reproduces identically under genuine root. Still not root-caused/fixed; still does not block this session's version-display changes (main.c's new --version/log-line code runs entirely before the point where this abort fires).", "tags": [ "asan", "double-join", @@ -15039,8 +15039,8 @@ "component-failover-adoption" ], "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-08-11T12:49:17.815Z" + "occurrences": 2, + "last_seen": "2026-08-20T11:16:00Z" }, { "id": "bug-1520", diff --git a/src/worker/src/main.c b/src/worker/src/main.c index 3e7db17d..291eefc4 100644 --- a/src/worker/src/main.c +++ b/src/worker/src/main.c @@ -304,7 +304,8 @@ static int check_user(void) } /* Long-option identifiers (beyond ASCII range to avoid collisions) */ -#define OPT_PEERS 256 +#define OPT_PEERS 256 +#define OPT_VERSION 257 int main(int argc, char* argv[]) { @@ -313,6 +314,15 @@ int main(int argc, char* argv[]) int verbose = 0; __attribute__((unused)) char worker_log[1024] = { 0 }; const char* peers_str = NULL; + /* build-time-versioning task 3.3: worker uses plain getopt_long(), not + * argp, so there is no free argp_program_version auto-handling here + * (unlike controller/main.c, scheduler/src/main.c). version_buf is + * computed once, before argument parsing even starts, via the same + * xce_compose_version() runtime helper those two use -- both the + * --version case below and the startup INFO log lines further down + * read this SAME buffer, never recomputing it independently, so the + * two can never disagree (Requirement 2.2). */ + static char version_buf[128]; xce_cluster_config_t cluster_cfg = {0}; int use_ha = 0; /* 1 when --peers given */ @@ -356,11 +366,19 @@ int main(int argc, char* argv[]) char* logdir = NULL; static struct option long_opts[] = { - { "peers", required_argument, NULL, OPT_PEERS }, - { "help", no_argument, NULL, 'h' }, - { NULL, 0, NULL, 0 } + { "peers", required_argument, NULL, OPT_PEERS }, + { "version", no_argument, NULL, OPT_VERSION }, + { "help", no_argument, NULL, 'h' }, + { NULL, 0, NULL, 0 } }; + /* Computed before argument parsing begins (mirrors controller/main.c's + * and scheduler/src/main.c's "compute before argp_parse()" ordering) so + * the OPT_VERSION case immediately below can print it, and so it is + * ready by the time the startup INFO log lines run later in main(). */ + xce_compose_version(version_buf, sizeof(version_buf), "xce v0.0.1-", + STR(VERSION_COMMIT_ID), STR(VERSION_BUILD_TIME)); + while ((opt = getopt_long(argc, argv, "n:l:vh", long_opts, NULL)) != -1) { switch (opt) { @@ -381,6 +399,12 @@ int main(int argc, char* argv[]) case OPT_PEERS: peers_str = optarg; break; + case OPT_VERSION: + /* Prints and exits before any other startup work (peer + * parsing, check_user(), daemon init, etc.) -- same contract + * as controller/scheduler's --version. */ + printf("%s\n", version_buf); + return 0; case 'h': default: printf("Usage: %s [-n name] [-l log_dir] [-v] " @@ -433,14 +457,16 @@ int main(int argc, char* argv[]) cluster_cfg.candidates_count, N_MAX_MASTERS); } XCECD_LOG(XCE_LOG_INFO, - "HA cluster config: %d candidate(s), identity=%s", - cluster_cfg.candidates_count, cluster_cfg.identity); + "HA cluster config: %d candidate(s), identity=%s, " + "version %s", + cluster_cfg.candidates_count, cluster_cfg.identity, + version_buf); use_ha = 1; } else { XCECD_LOG(XCE_LOG_INFO, "No --peers flag; using single-controller path " - "(host=%s port=%d)", - config.remote_host, config.remote_port); + "(host=%s port=%d), version %s", + config.remote_host, config.remote_port, version_buf); } /* P2.2 fix (component-failover-adoption, task 2.2, Req 1.4): assign * cluster_cfg onto config BEFORE xcecd_init()/xcecd_start() run, so it