Skip to content

Commit ceba427

Browse files
authored
Merge pull request #1796 from liuchong/fix/daemon-start-survives-ui-config
fix(cli): a refused UI configuration is not a failed daemon start
2 parents b3d898e + ad479b6 commit ceba427

2 files changed

Lines changed: 104 additions & 9 deletions

File tree

‎src/main.c‎

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2177,6 +2177,26 @@ static void main_daemon_ctl_open_browser(int port) {
21772177
}
21782178
}
21792179

2180+
/* Ask the daemon to enable the UI on `port`; true when it accepted.
2181+
*
2182+
* The seam below forces the refusal that the caller must survive. Reproducing it
2183+
* for real needs a machine loaded enough to miss a bounded handshake, which is
2184+
* not a state a test can ask for; it is COMPILED OUT of ordinary builds (see
2185+
* TEST_SEAMS in Makefile.cbm). */
2186+
static bool main_daemon_ctl_apply_ui_config(cbm_daemon_runtime_client_t *client,
2187+
uint8_t update_mask, int port) {
2188+
#ifdef CBM_ENABLE_TEST_SEAMS
2189+
char forced[8];
2190+
if (cbm_safe_getenv("CBM_TEST_DAEMON_UI_CONFIG_REFUSED", forced, sizeof(forced), NULL) &&
2191+
forced[0] == '1') {
2192+
return false;
2193+
}
2194+
#endif
2195+
return cbm_daemon_application_client_set_ui_config(client, update_mask, true, port,
2196+
MAIN_CONNECT_TIMEOUT_MS) ==
2197+
CBM_DAEMON_RUNTIME_APPLICATION_OK;
2198+
}
2199+
21802200
static int main_daemon_ctl_finish_ui_open(cbm_daemon_runtime_client_t **client_io, int port,
21812201
bool open_browser) {
21822202
if (!open_browser) {
@@ -2395,6 +2415,7 @@ static int main_run_daemon_ctl(int argc, char **argv, const cbm_daemon_ipc_endpo
23952415
/* The committed control connection satisfied the daemon's no-client
23962416
* startup window; configure the UI before departing. */
23972417
int ui_port = 0;
2418+
bool ui_configured = false;
23982419
if ((CBM_EMBEDDED_FILE_COUNT > 0)) {
23992420
cbm_ui_config_t ui_config;
24002421
cbm_ui_config_load(&ui_config);
@@ -2403,14 +2424,30 @@ static int main_run_daemon_ctl(int argc, char **argv, const cbm_daemon_ipc_endpo
24032424
bool context_set =
24042425
main_set_client_context(start_result.client, ".", CBM_MCP_TOOL_PROFILE_ALL, NULL, NULL,
24052426
MAIN_CONNECT_TIMEOUT_MS);
2406-
if (!context_set || cbm_daemon_application_client_set_ui_config(
2407-
start_result.client, update_mask, true, ui_port,
2408-
MAIN_CONNECT_TIMEOUT_MS) != CBM_DAEMON_RUNTIME_APPLICATION_OK) {
2427+
ui_configured = context_set &&
2428+
main_daemon_ctl_apply_ui_config(start_result.client, update_mask, ui_port);
2429+
if (!ui_configured) {
2430+
/* Reaching here means the daemon is up: the control connection above
2431+
* already satisfied its startup window. Only the UI handshake — two
2432+
* requests bounded by MAIN_CONNECT_TIMEOUT_MS — came back short.
2433+
*
2434+
* Whether that is fatal depends on what was asked for. `--port`/`--open`
2435+
* make the UI the point of the command, so failing to configure it is a
2436+
* failed command. A bare `daemon start` asks for a daemon, and it got
2437+
* one; reporting failure there sent operators hunting for a daemon that
2438+
* was in fact running and healthy. It also made a loaded machine look
2439+
* like a broken one, because a second of scheduling delay after an
2440+
* abrupt shutdown is enough to miss this handshake. */
2441+
if (requested_port > 0 || open_browser) {
2442+
(void)fprintf(stderr,
2443+
"error: the daemon did not accept the UI configuration; browser was "
2444+
"not opened\n");
2445+
(void)cbm_daemon_runtime_client_close(start_result.client, MAIN_CLOSE_TIMEOUT_MS);
2446+
return EXIT_FAILURE;
2447+
}
24092448
(void)fprintf(stderr,
2410-
"error: the daemon did not accept the UI configuration; browser was "
2411-
"not opened\n");
2412-
(void)cbm_daemon_runtime_client_close(start_result.client, MAIN_CLOSE_TIMEOUT_MS);
2413-
return EXIT_FAILURE;
2449+
"warning: the daemon started but did not accept the UI configuration; "
2450+
"the UI is unavailable until the next `daemon start`\n");
24142451
}
24152452
} else if (requested_port > 0 || open_browser) {
24162453
(void)fprintf(stderr, "warning: this binary was built without UI support; "
@@ -2427,8 +2464,10 @@ static int main_run_daemon_ctl(int argc, char **argv, const cbm_daemon_ipc_endpo
24272464
}
24282465
printf("It survives idle periods and session ends; `codebase-memory-mcp daemon stop` "
24292466
"retires it.\n");
2467+
/* Skipped when the handshake was refused: that path announces the port as
2468+
* warming, which would be a promise nothing is keeping. */
24302469
int ui_result =
2431-
(CBM_EMBEDDED_FILE_COUNT > 0)
2470+
(CBM_EMBEDDED_FILE_COUNT > 0 && ui_configured)
24322471
? main_daemon_ctl_finish_ui_open(&start_result.client, ui_port, open_browser)
24332472
: EXIT_SUCCESS;
24342473
if (start_result.client) {

‎tests/windows/test_daemon_lifecycle.py‎

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@
2424
import tempfile
2525

2626

27-
def run_cli(binary, cache, args, timeout=60):
27+
def run_cli(binary, cache, args, timeout=60, extra_env=None):
2828
env = dict(os.environ)
2929
env["CBM_CACHE_DIR"] = cache
30+
if extra_env:
31+
env.update(extra_env)
3032
return subprocess.run([binary] + args, capture_output=True, timeout=timeout, env=env)
3133

3234

@@ -81,6 +83,8 @@ def main():
8183
% start_text[:400])
8284
return 1
8385
print("PASS: daemon start reported permanent pid %d" % daemon_pid)
86+
# Only a UI build configures a UI, and only it can refuse to.
87+
ui_build = "ui:" in start_text
8488

8589
warm = run_cli(binary, cache, ["cli", "list_projects", "{}"])
8690
warm_text = output_text(warm)
@@ -107,6 +111,58 @@ def main():
107111
% output_text(stop_again)[:300])
108112
return 1
109113
print("PASS: stop retired the idle daemon; second stop was idempotent")
114+
daemon_pid = 0
115+
116+
if not ui_build:
117+
print("SKIP: non-UI binary cannot exercise the UI-configuration refusal")
118+
else:
119+
refused_env = {"CBM_TEST_DAEMON_UI_CONFIG_REFUSED": "1"}
120+
# A refused UI handshake must not be reported as a failed start. The
121+
# daemon is already up by then, so a bare `daemon start` got exactly
122+
# what it asked for; only `--port`/`--open` make the UI the point of
123+
# the command. Before the fix a busy machine — one second of delay
124+
# after an abrupt shutdown was enough — turned a healthy start into a
125+
# nonzero exit, which is how this reached CI as a phantom failure.
126+
refused = run_cli(binary, cache, ["daemon", "start"], extra_env=refused_env)
127+
refused_text = output_text(refused)
128+
pid_match = re.search(r"pid (\d+)", refused_text)
129+
daemon_pid = int(pid_match.group(1)) if pid_match else 0
130+
if refused.returncode != 0 or "permanent" not in refused_text:
131+
print("RED: a refused UI configuration must not fail `daemon start`; "
132+
"the daemon is already running by then:\n%s" % refused_text[:400])
133+
return 1
134+
if "warning" not in refused_text:
135+
print("RED: a refused UI configuration must still be reported, as a "
136+
"warning:\n%s" % refused_text[:400])
137+
return 1
138+
alive = run_cli(binary, cache, ["daemon", "status"])
139+
if alive.returncode != 0 or "permanent" not in output_text(alive):
140+
print("RED: the daemon that survived a refused UI configuration should "
141+
"be reachable:\n%s" % output_text(alive)[:400])
142+
return 1
143+
print("PASS: a refused UI configuration warns and leaves the daemon running")
144+
145+
stopped = run_cli(binary, cache, ["daemon", "stop"])
146+
if stopped.returncode != 0:
147+
print("RED: could not retire the daemon started without a UI:\n%s"
148+
% output_text(stopped)[:300])
149+
return 1
150+
daemon_pid = 0
151+
152+
# The other half of the contract: when the UI *was* asked for, failing
153+
# to configure it is still a failed command.
154+
demanded = run_cli(binary, cache, ["daemon", "start", "--port=45871"],
155+
extra_env=refused_env)
156+
demanded_text = output_text(demanded)
157+
pid_match = re.search(r"pid (\d+)", demanded_text)
158+
daemon_pid = int(pid_match.group(1)) if pid_match else 0
159+
if demanded.returncode == 0:
160+
print("RED: `daemon start --port=N` must fail when the UI it asked for "
161+
"could not be configured:\n%s" % demanded_text[:400])
162+
return 1
163+
print("PASS: an explicitly requested UI still fails the command when refused")
164+
run_cli(binary, cache, ["daemon", "stop"])
165+
daemon_pid = 0
110166

111167
print("\nGREEN: daemon lifecycle (status/start/recycle/stop) behaves.")
112168
return 0

0 commit comments

Comments
 (0)