forked from heavy-duty/box
fix: preserve box exec command newlines
This commit is contained in:
parent
f8ca59c22e
commit
7247768465
4 changed files with 62 additions and 4 deletions
|
|
@ -5,6 +5,10 @@ which records not just what changed but what each drill run proved.
|
|||
|
||||
## Unreleased
|
||||
|
||||
### Fixed
|
||||
|
||||
- `box exec` preserves newlines and command argv across its login-user boundary (#169)
|
||||
|
||||
### Changed
|
||||
|
||||
- Release and repository governance now use the shared ceremony pinned at `0.1.0` (heavy-duty/ceremony#14)
|
||||
|
|
|
|||
9
bin/box
9
bin/box
|
|
@ -1845,13 +1845,18 @@ box_user() {
|
|||
}
|
||||
|
||||
cmd_shell() { incus exec "$inst" -- sudo -u "$(box_user "$inst")" -i; }
|
||||
cmd_exec() { incus exec "$inst" -- sudo -u "$(box_user "$inst")" -i "${args[@]:1}"; }
|
||||
# sudo -i joins its command argv into one shell string. In that join, a
|
||||
# backslash-newline becomes a shell continuation and silently deletes the
|
||||
# newline from a multi-line `box exec` payload (#169). Keep the login
|
||||
# environment explicitly, but let the inner shell exec the original argv.
|
||||
cmd_exec() { incus exec "$inst" -- sudo -u "$(box_user "$inst")" -H bash -lc 'cd ~ && exec "$@"' _ "${args[@]:1}"; }
|
||||
|
||||
# A shell is a child of the exec connection: drop the terminal and everything
|
||||
# in it is SIGHUP'd — a long Claude run dies with it. tmux 'new-session -A'
|
||||
# attaches when the session exists and creates it when it doesn't, so starting
|
||||
# work and reattaching after a disconnect are the same command. 'shell' stays
|
||||
# bare on purpose — two verbs, two contracts.
|
||||
# bare on purpose — two verbs, two contracts. Unlike cmd_exec, tmux has no
|
||||
# caller-supplied command payload; its session name is validated below.
|
||||
cmd_tmux() {
|
||||
local session="${args[1]:-main}"
|
||||
case "$session" in
|
||||
|
|
|
|||
|
|
@ -71,8 +71,8 @@ wait_box() { # poll until exec answers (the VM agent can take a while), ~4 min
|
|||
# Read from inside a box WITHOUT ever hanging the drill.
|
||||
#
|
||||
# Two traps, both hit for real:
|
||||
# · 'box exec' becomes 'sudo -u <template user> -i' — a LOGIN zsh (oh-my-zsh and
|
||||
# all). Fine for a person, needless machinery for a probe.
|
||||
# · 'box exec' crosses a login-user shell boundary. Fine for a person,
|
||||
# needless machinery for a probe.
|
||||
# · $( ) waits for stdout to CLOSE, not for the command to exit. A grandchild
|
||||
# inheriting the exec session's stdout keeps the substitution open forever,
|
||||
# and 'timeout' does not save you: it kills the wrapper, not the holder of
|
||||
|
|
|
|||
49
test/cli.sh
49
test/cli.sh
|
|
@ -68,6 +68,55 @@ check "unknown flag on list exits 2" 2 "unknown option" "$BOX" list
|
|||
# A flag that needs a value and gets none.
|
||||
check "--name with no value exits 2" 2 "--name needs a value" "$BOX" new --name
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# box exec — preserve command argv across the login-environment boundary
|
||||
# (#169). `sudo -i <command...>` joins argv into one shell string; its escaped
|
||||
# newline becomes a continuation, so a multi-line body can fuse into a
|
||||
# different valid command and still return 0. Drive cmd_exec through a fake
|
||||
# incus boundary that validates the wrapper shape and then executes it. This
|
||||
# test therefore fails against the old -i implementation before trusting the
|
||||
# marker files.
|
||||
# ---------------------------------------------------------------------------
|
||||
EXECFN="$(mktemp)"
|
||||
grep '^cmd_exec()' "$BOX" > "$EXECFN"
|
||||
check "box exec: cmd_exec extracted (guards the grep)" 0 "exec \"\$@\"" cat "$EXECFN"
|
||||
check "box exec: extracted function is valid bash" 0 "" bash -n "$EXECFN"
|
||||
check "box exec: command argv never rides sudo -i" 1 "" grep -q -- ' -i ' "$EXECFN"
|
||||
|
||||
exec_fixture() { # exec_fixture <command> [arg...]
|
||||
bash -c '
|
||||
set -e
|
||||
. "$0"
|
||||
box_user() { printf "%s\n" fixture-user; }
|
||||
incus() {
|
||||
[ "$1" = exec ] && [ "$2" = fixture ] && [ "$3" = -- ]
|
||||
shift 3
|
||||
[ "$1" = sudo ] && [ "$2" = -u ] && [ "$3" = fixture-user ] &&
|
||||
[ "$4" = -H ]
|
||||
shift 4
|
||||
"$@"
|
||||
}
|
||||
inst=fixture
|
||||
args=(fixture "$@")
|
||||
cmd_exec
|
||||
' "$EXECFN" "$@"
|
||||
}
|
||||
|
||||
EXEC_STATE="$(mktemp -d)"
|
||||
exec_body="set -euo pipefail
|
||||
touch '$EXEC_STATE/step-one'
|
||||
touch '$EXEC_STATE/step-two'"
|
||||
check "box exec: silent-success multiline body executes each statement" 0 "" \
|
||||
exec_fixture bash -lc "$exec_body"
|
||||
check "box exec: multiline step one was not fused into set argv" 0 "" \
|
||||
test -f "$EXEC_STATE/step-one"
|
||||
check "box exec: multiline step two was not fused into set argv" 0 "" \
|
||||
test -f "$EXEC_STATE/step-two"
|
||||
check "box exec: plain argv remains separate" 0 "one argument" \
|
||||
exec_fixture printf '%s\n' "one argument"
|
||||
rm -rf "$EXEC_STATE"
|
||||
rm -f "$EXECFN"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# A shim `id` on PATH: lets us drive install.sh's DEST branch with a canned uid +
|
||||
# group output, exactly the way rig drives assert_runner_repo against fixtures.
|
||||
|
|
|
|||
Loading…
Reference in a new issue