Skip to content

Commit 74ce78a

Browse files
feat(launcher-standard): cross-platform browser launch + env-overridable wait timing (#177)
## Summary Two related template improvements the standard had already partly declared but never specified or implemented in the reference templates. ### 1. Cross-platform `open_browser()` Previously Linux-only (`xdg-open` / `firefox` / `chromium`) despite the standard's stated cross-platform commitment (design principle 2: *"The same launcher script works on Linux, macOS, and Windows."*). On macOS the template fell through to "Open manually: \$URL"; on Windows-via-Git-Bash and on WSL it did too — even though every modern WSL has `wslview` and Git Bash has `start`. Now: | Step | Action | |------|--------| | 1 | Honour `\$BROWSER` env var (de-facto Unix convention; lets users override without re-minting) | | 2 | Platform dispatch via `uname -s`: macOS → `open`; Linux → WSL detection (`/proc/version contains "microsoft"`) → `wslview`, else `xdg-open` → `firefox` → `chromium`; Windows (MINGW/MSYS/CYGWIN) → `start` | | 3 | Fallback unchanged: print `"Open manually: \$URL"` | a2ml gains `[browser-launch]` declaring the resolution order so consumers (`launch-scaffolder`, audit tools) can reason about it without re-parsing the bash. ### 2. `wait_for_server()` honours the env-var override The a2ml had `wait-for-url-timeout-seconds = 15` but the bash template hardcoded `wait_for_server 15` — operators on slow hosts had no way to extend the timeout short of editing the script. Also no per-request timeout: a single hung TCP connection could block the whole polling loop. Fix to `[runtime]`: - `wait-for-url-timeout-env-var = "WAIT_FOR_URL_TIMEOUT_SECONDS"` — formalises the override name - `wait-for-url-poll-interval-seconds = 1` + env var (was implicit in the template's `sleep 1`) - `wait-for-url-per-request-timeout-seconds = 2` — caps each `curl` probe with `--max-time` Fix to template: \`\`\`bash local max_wait="\${1:-\${WAIT_FOR_URL_TIMEOUT_SECONDS:-15}}" # arg → env → default local poll_interval="\${WAIT_FOR_URL_POLL_INTERVAL:-1}" local per_request_timeout=2 curl -fsS --max-time "\$per_request_timeout" "\$URL" >/dev/null 2>&1 \`\`\` Error message now tells operators how to extend the timeout. ## Compatibility - Existing launchers that pass an explicit timeout (`wait_for_server 30`) still work — the new template prefers the arg over the env var. - Existing launchers without `\$BROWSER` set get the platform-aware ladder, which is strictly more capable than the old Linux-only chain (Linux-with-no-WSL behaves identically since `wslview` won't be found and `xdg-open` runs). ## Lock-step + coordination Both files in the same commit per §Sync requirement (and the gate introduced in #172, which will go green on first push). Independent of #170, #171, #172, #173, #175, #176. No file overlap with #176 (which is keepopen.sh). ## Test plan - [x] a2ml parses (python tomllib); new `[browser-launch]` table and 4 new `[runtime]` keys round-trip cleanly - [x] `open_browser()` and `wait_for_server()` bash blocks pass `bash -n` syntax check - [ ] Manual test on macOS / WSL hosts (deferred — needs those hosts; logic matches well-documented `uname -s` / `/proc/version` conventions used by `wslview` itself) - [ ] Live lock-step gate (#172) goes green on first push 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a03e5a3 commit 74ce78a

2 files changed

Lines changed: 86 additions & 19 deletions

File tree

docs/UX-standards/launcher-standard.adoc

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -75,47 +75,89 @@ stop_server() {
7575
[source,bash]
7676
----
7777
wait_for_server() {
78-
local max_wait=$1
78+
# All three values are env-overridable per [runtime] in launcher-standard.a2ml.
79+
local max_wait="${1:-${WAIT_FOR_URL_TIMEOUT_SECONDS:-15}}"
80+
local poll_interval="${WAIT_FOR_URL_POLL_INTERVAL:-1}"
81+
local per_request_timeout=2 # curl --max-time; caps each probe so a
82+
# hung server can't eat the whole budget
7983
local waited=0
80-
81-
while [ $waited -lt $max_wait ]; do
82-
if curl -fsS "$URL" >/dev/null 2>&1; then
84+
85+
while [ "$waited" -lt "$max_wait" ]; do
86+
if curl -fsS --max-time "$per_request_timeout" "$URL" >/dev/null 2>&1; then
8387
return 0 # Success
8488
fi
85-
sleep 1
86-
waited=$((waited + 1))
89+
sleep "$poll_interval"
90+
waited=$((waited + poll_interval))
8791
done
88-
92+
8993
return 1 # Timeout
9094
}
9195
92-
# Usage
93-
if ! wait_for_server 15; then
94-
err "Server did not start within 15 seconds"
96+
# Usage — pass nothing so the env-var/constant chain applies.
97+
if ! wait_for_server; then
98+
err "Server did not start within ${WAIT_FOR_URL_TIMEOUT_SECONDS:-15}s"
9599
err "Check log: $LOG_FILE"
100+
err "Override with WAIT_FOR_URL_TIMEOUT_SECONDS=<N> if the host needs longer"
96101
return 1
97102
fi
98103
----
99104

100105
=== Browser Launching
101106

107+
Resolution order, per `[browser-launch]` in `launcher-standard.a2ml`:
108+
109+
1. `$BROWSER` env var (de-facto Unix convention; user/operator override)
110+
2. Platform-specific ladder, dispatched via `uname -s`:
111+
- **macOS**: `open`
112+
- **Linux**: if WSL (detected via `/proc/version` containing
113+
"microsoft"), prefer `wslview` so the URL opens in the Windows-side
114+
default browser. Otherwise: `xdg-open` → `firefox` → `chromium`.
115+
- **Windows** (Git Bash / MSYS / Cygwin): `start` (cmd builtin)
116+
3. Fallback: print `"Open manually: $URL"` so the user is never silently
117+
left without a URL.
118+
102119
[source,bash]
103120
----
104121
open_browser() {
105122
if ! is_running; then
106123
err "Server is not running"
107124
return 1
108125
fi
109-
110-
if command -v xdg-open >/dev/null 2>&1; then
111-
xdg-open "$URL" &
112-
elif command -v firefox >/dev/null 2>&1; then
113-
firefox "$URL" &
114-
elif command -v chromium >/dev/null 2>&1; then
115-
chromium "$URL" &
116-
else
117-
log "Open manually: $URL"
126+
127+
# 1. $BROWSER override (canonical Unix convention)
128+
if [[ -n "${BROWSER:-}" ]]; then
129+
"$BROWSER" "$URL" &
130+
return $?
118131
fi
132+
133+
# 2. Platform-specific ladder
134+
case "$(uname -s)" in
135+
Darwin*)
136+
open "$URL" &
137+
;;
138+
Linux*)
139+
# WSL detection: /proc/version mentions "microsoft" under WSL1/2
140+
if grep -qi microsoft /proc/version 2>/dev/null && \
141+
command -v wslview >/dev/null 2>&1; then
142+
wslview "$URL" &
143+
elif command -v xdg-open >/dev/null 2>&1; then
144+
xdg-open "$URL" &
145+
elif command -v firefox >/dev/null 2>&1; then
146+
firefox "$URL" &
147+
elif command -v chromium >/dev/null 2>&1; then
148+
chromium "$URL" &
149+
else
150+
log "Open manually: $URL"
151+
fi
152+
;;
153+
MINGW*|MSYS*|CYGWIN*)
154+
# Git Bash / MSYS / Cygwin all carry `start` from cmd
155+
start "$URL" &
156+
;;
157+
*)
158+
log "Open manually: $URL"
159+
;;
160+
esac
119161
}
120162
----
121163

launcher/launcher-standard.a2ml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,37 @@ pid-file-pattern = "${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/{app-name}-server.pid"
9292
# Per-user, survives reboot, not world-writable. The {app-name} subdir
9393
# isolates each launcher's logs.
9494
log-file-pattern = "${XDG_STATE_HOME:-$HOME/.local/state}/{app-name}/server.log"
95+
# URL-readiness polling after start. The launcher polls the app's URL
96+
# until either it responds or total-timeout-seconds elapses.
97+
# All three timing values are env-var overridable so operators can tune
98+
# without re-minting the launcher.
9599
wait-for-url-timeout-seconds = 15
100+
wait-for-url-timeout-env-var = "WAIT_FOR_URL_TIMEOUT_SECONDS"
101+
wait-for-url-poll-interval-seconds = 1
102+
wait-for-url-poll-interval-env-var = "WAIT_FOR_URL_POLL_INTERVAL"
103+
# Per-request timeout: cap each individual curl probe so a hung server
104+
# does not eat the entire poll budget on a single request.
105+
wait-for-url-per-request-timeout-seconds = 2
96106
startup-command-search = [
97107
"{repo-dir}/scripts/run.sh",
98108
"{repo-dir}/dev.sh",
99109
]
100110

111+
[browser-launch]
112+
# How open_browser() resolves which command to invoke. Implementations
113+
# MUST honour env-var-override first (de-facto $BROWSER convention),
114+
# then fall back to the platform-specific ladder. Platform detected via
115+
# `uname -s`; on Linux, additionally probe /proc/version for "microsoft"
116+
# to detect WSL — under WSL, wslview is preferred so the URL opens in
117+
# the Windows-side default browser (Linux-side browsers either land in
118+
# WSLg or fail).
119+
env-var-override = "BROWSER"
120+
linux = ["wslview", "xdg-open", "firefox", "chromium"]
121+
linux-wsl-detect = "/proc/version contains 'microsoft' (case-insensitive)"
122+
macos = ["open"]
123+
windows = ["start"] # cmd builtin; works under Git Bash / MSYS / Cygwin
124+
fallback = "log 'Open manually: <URL>'"
125+
101126
[error-visibility]
102127
# When the launcher runs in a GUI context (no tty + DISPLAY/WAYLAND_DISPLAY set),
103128
# errors must be visible to the user via a GUI dialog, not only to stderr.

0 commit comments

Comments
 (0)