Skip to content

feat: execute a command instead of an interactive shell - #44

Merged
raphaelcoeffic merged 3 commits into
mainfrom
feat/exec-command
Oct 26, 2026
Merged

raphaelcoeffic merged 3 commits into
mainfrom
feat/exec-command

Conversation

@raphaelcoeffic

Copy link
Copy Markdown
Owner

Stacked on #43: retarget to main once it is merged.

Changes

  • dive <container> -- <command>... runs the command in the dive environment instead of starting zsh. Plain dive <container> is unchanged.
    • The command runs directly (no zsh -c), so arguments arrive intact, and it is passed through the sudo re-exec.
    • Exit status: the command's own; 127 if it is not found and 126 if it cannot be run, as in shells; 128 + signal if it is killed.
    • Same environment as the interactive shell, but zshrc is not loaded (documented in the README).
  • Signal forwarding: dive's waiting processes forward SIGHUP/INT/QUIT/TERM to their child when another process sent them (kill, timeout, sudo relaying). Signals generated by the kernel (si_code > 0: Ctrl-C, terminal hangup) are not forwarded, since they already reach the command through the foreground process group.
  • Fix: the single-thread check before fork() read /proc/self/task by path, which no longer resolves inside the container's mount namespace. Since fix: guard fork() sites against multi-threaded regressions #40 every session logged could not read /proc/self/task, and the check was skipped. It now keeps a handle on the original /proc. Includes a unit test.
  • CI: smoke test of the release binary against an Alpine Docker container: a plain command, stdout, exit code, HTTPS.

Testing

Local x86_64 tests with the 26.05 base image, against Alpine containers under Docker and rootless podman:

  • exit codes 0 / 3 / 127 / 143
  • arguments with spaces and $; stdin and stdout piping
  • the command runs in the container's namespaces (container hostname, /etc/os-release)
  • PATH / NIX_SSL_CERT_FILE match the interactive shell; curl reaches github.com and rejects the eMudhra test host
  • real Ctrl-C through a pty, also with sudo use_pty: the command gets exactly one SIGINT, and dive exits with its status
  • timeout -s TERM|INT|HUP 4 dive <ctr> -- sleep 47: the command is stopped after 4s. Before, Docker left it running (TERM) or waited the full duration (INT).
  • the thread check reports 1 thread at both fork sites, with no warning

Not tested: an interactive terminal session by hand.

Known limitation

si_code shows who sent a signal, not who else received it. GNU timeout also signals its whole process group. Under rootless podman that group signal reaches the command directly, on top of the copies relayed by sudo and dive: timeout -s INT delivers SIGINT 3 times there. This is harmless for TERM/HUP, and only affects programs that treat a second Ctrl-C as "force quit". Under Docker only the relayed copy gets through, so it is delivered once. Fully deduplicating would require running the command in its own process group as the terminal's foreground job.

🤖 Generated with Claude Code

@raphaelcoeffic
raphaelcoeffic added this pull request to stack #45 October 26, 2026 08:37
Base automatically changed from chore/nixos-26.05 to main October 26, 2026 08:57
raphaelcoeffic and others added 3 commits October 26, 2026 09:57
`dive <container> -- <command>...` runs the command in the dive
environment instead of starting zsh, and exits with its exit status.

- the command is passed through the sudo re-exec
- exec failures exit with 127 (not found) or 126, as shells do
- a child killed by a signal exits with 128 + signal
- parent processes ignore SIGINT/SIGQUIT while waiting, like system(3),
  so Ctrl-C reaches the command without killing dive first

CI runs a smoke test of the release binary against a container.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
assert_single_threaded() read /proc/self/task by path, which no longer
resolves the calling process once inside the container's mount
namespace. Every session logged "could not read /proc/self/task" and
the check was skipped.

Keep a handle on the original /proc from the first check, and look up
self/task through it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ignoring SIGINT/SIGQUIT in the waiting parents only covered Ctrl-C on the
terminal. Signals sent by other processes (kill, timeout, sudo relaying
them) never reached the command: dive either waited for it to end, or
exited and left it running.

The parents now forward SIGHUP, SIGINT, SIGQUIT and SIGTERM to their
child, except those generated by the kernel (Ctrl-C, terminal hangup),
which already reach the command through its process group.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@raphaelcoeffic
raphaelcoeffic merged commit 6becc0b into main Oct 26, 2026
2 checks passed
@raphaelcoeffic
raphaelcoeffic deleted the feat/exec-command branch October 26, 2026 09:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant