From 1adbf2a1f9ce06142d88d26a3a9a6373ef78febe Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 08:28:33 +0000 Subject: [PATCH] doctor: bound the docker probe, say what it is waiting for, and give the start command With Docker Desktop installed but stopped, 'docker info' can sit silent for minutes; init-project.sh runs doctor last, so the whole scaffold read as hung. The probe is now backgrounded and killed at MXTK_DOCKER_PROBE_SECS (8 s, portable to bash 3.2 without 'timeout'), prints a line before waiting, and --no-docker / MXTK_DOCTOR_SKIP_DOCKER=1 skip it. A daemon that is down gets the platform's start command and the re-run line; --install on macOS launches Docker Desktop. Fixture: tests/wave2/test-doctor-docker-probe.sh. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 3 + bin/doctor.sh | 78 ++++++++++++++++++++++--- tests/wave2/test-doctor-docker-probe.sh | 59 +++++++++++++++++++ 3 files changed, 133 insertions(+), 7 deletions(-) create mode 100755 tests/wave2/test-doctor-docker-probe.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index b8eee3e..b571f79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ moment updating it became a separate chore). One line per change: Kinds: `new` · `fix` · `learn` (a skill/learning) · `process` (rules, templates, CI). Credit the person or project that surfaced the change — the credit line is the thank-you. +## 2026-09-09 +- fix(bin/doctor.sh): the docker probe is bounded (`MXTK_DOCKER_PROBE_SECS`, default 8 s), announces itself before waiting, and can be skipped (`--no-docker` / `MXTK_DOCTOR_SKIP_DOCKER=1`). With Docker Desktop installed but stopped, `docker info` sat silent for minutes at the end of `init-project.sh` and the whole scaffold read as hung. A daemon that is down now gets the one start command for this platform (`open -a Docker`, the Windows `start` line, `systemctl start docker`) plus the re-run line; `--install` on macOS launches Docker Desktop itself. Fixture `tests/wave2/test-doctor-docker-probe.sh` (13 assertions: hanging, down, up, skipped) — a new builder's onboarding feedback, via Maurits Visser + ## 2026-09-08 - fix(bug-logs): re-probed the two workflow blockers on mxcli **v0.21.0** (released 2026-09-06, Mendix 11.13.0) — **both still open**, and BUG-76's diagnosis in this log was wrong. BUG-121 (`PARALLEL SPLIT`): the same script executed through v0.20.0 and v0.21.0 on two throwaway copies produces a **byte-identical** workflow unit (zero diff from `bson dump` after blanking the `$ID` GUIDs), so the runtime behaviour cannot have changed — and the path contents *are* present in the stored BSON on both versions, so the entry's own title ("writes the paths but not their contents") names the symptom, not the mechanism, which is still unidentified. BUG-76 (`DECISION`): not unconditional storage corruption but a **pincer between two spellings** — a bare outcome passes `mxcli check` and makes the model unloadable (`StorageLoadException`, "not a valid EnumerationValueIdentifier"), while a qualified one is rejected by mxcli's own `MDL-WF03` and loads clean via `exec --no-check` with a single `CE0117` for the empty expression; mxcli mandates the spelling the loader rejects and rejects the one it accepts, which implies two independent fixes rather than one. The correction was made on the source project on 2026-09-02 and never promoted here; it is already filed upstream as mendixlabs/mxcli#1031, so it goes into the draft as a comment to post rather than a rewritten body — a topbar-titled portal project · at merge: the three "disputed" `DECISION` notes on master (`mxcli-bugs.md` v0.20.0 clearance table, `learned-workflow-patterns.md` §1 pointer, §8 version table, §21 verdict table) now cite the resolved pincer diagnosis and the v0.21.0 re-probe instead of "shape-dependent, not isolated" — the toolkit merge queue diff --git a/bin/doctor.sh b/bin/doctor.sh index 4250363..39eab29 100755 --- a/bin/doctor.sh +++ b/bin/doctor.sh @@ -20,6 +20,11 @@ # # (same download the headless container build # # uses; cached at ~/.mxcli/mxbuild/). # bin/doctor.sh --install --yes # skip the confirmation (unattended/agent runs) +# bin/doctor.sh --no-docker [] # skip the docker probe (or MXTK_DOCTOR_SKIP_DOCKER=1). +# # The probe is bounded anyway: MXTK_DOCKER_PROBE_SECS +# # (default 8) — a stopped Docker Desktop can make +# # 'docker info' sit silent for minutes, which read +# # as "the setup hangs" (field report, 2026-09-09). # bin/doctor.sh --quick [] # ~2 s: platform, mxcli/mxbuild/java EXECUTE, spawn # # speed, path hygiene, model layout. Skips the # # once-per-machine sections (python, CRLF, script @@ -47,11 +52,14 @@ PROJECT_DIR="" INSTALL=0 ASSUME_YES=0 QUICK=0 +NO_DOCKER="${MXTK_DOCTOR_SKIP_DOCKER:-0}" +DOCKER_PROBE_SECS="${MXTK_DOCKER_PROBE_SECS:-8}" for _arg in "$@"; do case "$_arg" in --install) INSTALL=1 ;; --yes|-y) ASSUME_YES=1 ;; --quick) QUICK=1 ;; + --no-docker) NO_DOCKER=1 ;; *) PROJECT_DIR="$_arg" ;; esac done @@ -616,13 +624,69 @@ head_ "Self-verification stack (Docker — recommended)" # Pro to find out whether a page renders. Without Docker the mxbuild gate still verifies the # MODEL, but nothing verifies the RUNNING APP unless a human does. Recommended, never # required — hence WARN, not FAIL. -if command -v docker >/dev/null 2>&1; then - if docker info >/dev/null 2>&1; then - ok "docker daemon responding — mxcli docker check + test-stack-up.sh (app up, e2e, screenshots) available" - else - warn "docker is installed but the daemon is not responding." - note "Start Docker Desktop (or colima / Rancher Desktop). Until it runs: no app container," - note "no throwaway Postgres, no agent-driven e2e/screenshots." +# docker_daemon_up — `docker info`, bounded. When Docker Desktop is installed but not running, +# the CLI can sit on its socket/named pipe with no output for minutes (macOS and Git Bash both +# reported, 2026-09-09) — and since init-project.sh runs doctor last, the whole scaffold read as +# hung. No `timeout` on stock macOS, so: background the probe, poll once a second, kill it at +# the bound. Returns 0 up · 1 down (answered quickly) · 2 no answer within the bound. +docker_daemon_up() { + docker info >/dev/null 2>&1 & + _dd_pid=$! + _dd_waited=0 + while kill -0 "$_dd_pid" 2>/dev/null; do + if [ "$_dd_waited" -ge "$DOCKER_PROBE_SECS" ]; then + kill "$_dd_pid" 2>/dev/null; wait "$_dd_pid" 2>/dev/null + return 2 + fi + sleep 1; _dd_waited=$((_dd_waited + 1)) + done + wait "$_dd_pid" +} + +# docker_start_hint — the one command that starts the daemon on this platform, or the closest +# thing to it. Printed instead of "start Docker", which sent people to look for a button. +docker_start_hint() { + case "$PLATFORM" in + macos) + if [ -d /Applications/Docker.app ]; then note " open -a Docker # Docker Desktop; the daemon needs ~30-90 s after launch" + elif command -v colima >/dev/null 2>&1; then note " colima start" + else note " open -a Docker (Docker Desktop) or colima start — whichever is installed"; fi ;; + gitbash) + note ' start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe" # then wait ~30-90 s' + note " (or Rancher Desktop / Podman Desktop from the Start menu)" ;; + linux|wsl) + note " sudo systemctl start docker # or, on WSL, start Docker Desktop on the Windows side" ;; + *) note " start Docker Desktop / the docker service, then re-run this script" ;; + esac + note "Then re-run: bin/doctor.sh${PROJECT_DIR:+ $PROJECT_DIR} (this section is skipped by --quick)" +} + +if [ "$NO_DOCKER" = 1 ]; then + note "docker probe skipped (--no-docker / MXTK_DOCTOR_SKIP_DOCKER=1). Self-verification stack not checked." +elif command -v docker >/dev/null 2>&1; then + note "probing the docker daemon (bounded: ${DOCKER_PROBE_SECS} s — a stopped Docker Desktop can otherwise hang here)..." + DOCKER_STATE=0 + docker_daemon_up || DOCKER_STATE=$? + case "$DOCKER_STATE" in + 0) + ok "docker daemon responding — mxcli docker check + test-stack-up.sh (app up, e2e, screenshots) available" ;; + 2) + warn "docker is installed but 'docker info' gave no answer within ${DOCKER_PROBE_SECS} s — treated as not running." + note "That silent wait is what makes a setup look hung. Raise the bound with" + note "MXTK_DOCKER_PROBE_SECS=30 if the daemon is merely slow to answer here. To start it:" + docker_start_hint ;; + *) + warn "docker is installed but the daemon is not responding." + note "Until it runs: no app container, no throwaway Postgres, no agent-driven e2e/screenshots." + note "To start it:" + docker_start_hint ;; + esac + # --install is the "do it for me" mode: on macOS with Docker Desktop present, launch it. + # Never elsewhere — Windows launch paths vary and Linux needs sudo. + if [ "$DOCKER_STATE" != 0 ] && [ "$INSTALL" = 1 ] && [ "$PLATFORM" = macos ] && [ -d /Applications/Docker.app ]; then + if open -a Docker 2>/dev/null; then + note "--install: launched Docker Desktop (open -a Docker). Give it ~30-90 s, then re-run doctor." + fi fi else warn "docker is not installed — recommended for new builders." diff --git a/tests/wave2/test-doctor-docker-probe.sh b/tests/wave2/test-doctor-docker-probe.sh new file mode 100755 index 0000000..5843dfb --- /dev/null +++ b/tests/wave2/test-doctor-docker-probe.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# test-doctor-docker-probe.sh — doctor.sh's docker probe must never hang, and must say what +# it is doing. Field report 2026-09-09: with Docker Desktop installed but stopped, `docker info` +# sat silent for minutes at the end of init-project.sh and the whole scaffold read as hung. +# +# usage: bash tests/wave2/test-doctor-docker-probe.sh bin/doctor.sh +# +# Three fake `docker` binaries on PATH: one that hangs, one that answers "down" at once, one +# that answers "up". Plus --no-docker / MXTK_DOCTOR_SKIP_DOCKER=1 skipping the section. +set -u +DOCTOR="${1:?usage: $0 bin/doctor.sh}" +DOCTOR="$(cd "$(dirname "$DOCTOR")" && pwd)/$(basename "$DOCTOR")" +T="$(mktemp -d "${TMPDIR:-/tmp}/mxtk-doctor-docker.XXXXXX")" +trap 'rm -rf "$T"' EXIT +PASS=0; FAIL=0 +ok() { PASS=$((PASS + 1)); echo " ok $*"; } +fail() { FAIL=$((FAIL + 1)); echo " FAIL $*"; } +assert_contains() { if grep -q -- "$2" "$1"; then ok "$3"; else fail "$3 — expected: $2"; fi; } +assert_missing() { if grep -q -- "$2" "$1"; then fail "$3 — unexpected: $2"; else ok "$3"; fi; } + +mkfake() { # mkfake + mkdir -p "$1"; printf '#!/usr/bin/env bash\n%s\n' "$2" > "$1/docker"; chmod +x "$1/docker" +} +mkfake "$T/hang" 'sleep 120' +mkfake "$T/down" 'echo "error during connect: Docker Desktop is not running" >&2; exit 1' +mkfake "$T/up" 'echo "Server Version: 27.0.0"; exit 0' + +echo "doctor docker probe — $DOCTOR" + +# 1. hanging daemon: bounded, and says so +S0=$(date +%s) +PATH="$T/hang:$PATH" MXTK_DOCKER_PROBE_SECS=2 bash "$DOCTOR" > "$T/hang.out" 2>&1 +S1=$(date +%s) +if [ $((S1 - S0)) -lt 40 ]; then ok "hanging docker: doctor finished in $((S1 - S0)) s (bound 2 s)"; else fail "hanging docker: doctor took $((S1 - S0)) s — the bound is not biting"; fi +assert_contains "$T/hang.out" "probing the docker daemon (bounded: 2 s" "hanging docker: announces the probe and its bound before waiting" +assert_contains "$T/hang.out" "gave no answer within 2 s" "hanging docker: reports the timeout as 'not running'" +assert_contains "$T/hang.out" "MXTK_DOCKER_PROBE_SECS=30" "hanging docker: tells how to raise the bound" +assert_contains "$T/hang.out" "Then re-run: bin/doctor.sh" "hanging docker: says how to re-check after starting it" +assert_missing "$T/hang.out" "docker daemon responding" "hanging docker: not reported as up" + +# 2. daemon down, answering at once +PATH="$T/down:$PATH" bash "$DOCTOR" > "$T/down.out" 2>&1 +assert_contains "$T/down.out" "daemon is not responding" "down docker: reported as not responding" +assert_contains "$T/down.out" "To start it:" "down docker: carries a start command" +assert_missing "$T/down.out" "gave no answer within" "down docker: a fast 'down' is not called a timeout" + +# 3. daemon up +PATH="$T/up:$PATH" bash "$DOCTOR" > "$T/up.out" 2>&1 +assert_contains "$T/up.out" "docker daemon responding" "up docker: reported as responding" + +# 4. skipping the section entirely +PATH="$T/hang:$PATH" bash "$DOCTOR" --no-docker > "$T/skip.out" 2>&1 +assert_contains "$T/skip.out" "docker probe skipped" "--no-docker: section skipped (hanging fake never called)" +S0=$(date +%s); PATH="$T/hang:$PATH" MXTK_DOCTOR_SKIP_DOCKER=1 bash "$DOCTOR" > "$T/skip2.out" 2>&1; S1=$(date +%s) +assert_contains "$T/skip2.out" "docker probe skipped" "MXTK_DOCTOR_SKIP_DOCKER=1: section skipped" +if [ $((S1 - S0)) -lt 40 ]; then ok "MXTK_DOCTOR_SKIP_DOCKER=1: no wait ($((S1 - S0)) s)"; else fail "MXTK_DOCTOR_SKIP_DOCKER=1 still waited $((S1 - S0)) s"; fi + +echo; echo "$PASS passed, $FAIL failed" +[ "$FAIL" -eq 0 ]