| 1 | #!/bin/sh |
| 2 | # PID 1 of an agent-session container. |
| 3 | # |
| 4 | # Starts the agent under a detached tmux session, tees a byte-exact transcript, |
| 5 | # and then blocks until that session ends — so the container's lifetime is the |
| 6 | # agent's lifetime, and `docker wait` is a valid completion signal. |
| 7 | # |
| 8 | # Every browser attach is a SEPARATE `docker exec … tmux attach`, so a dropped |
| 9 | # websocket kills only that client. That is the reason tmux is here at all. |
| 10 | # |
| 11 | # Usage: anvil-session <command> [args...] |
| 12 | set -eu |
| 13 | |
| 14 | SESSION="${ANVIL_TMUX_SESSION:-agent}" |
| 15 | TRANSCRIPT="${ANVIL_TRANSCRIPT:-/tmp/anvil-transcript}" |
| 16 | WORKDIR="${ANVIL_WORKDIR:-/workspace}" |
| 17 | EXIT_FILE="${ANVIL_EXIT_FILE:-/tmp/anvil-exit}" |
| 18 | CMD_FILE="${ANVIL_CMD_FILE:-/tmp/anvil-cmd.sh}" |
| 19 | |
| 20 | if [ "$#" -eq 0 ]; then |
| 21 | echo "anvil-session: no command given" >&2 |
| 22 | exit 64 |
| 23 | fi |
| 24 | |
| 25 | # Single-quote one argument for safe re-parsing by /bin/sh. |
| 26 | quote() { |
| 27 | printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")" |
| 28 | } |
| 29 | |
| 30 | : > "$TRANSCRIPT" |
| 31 | rm -f "$EXIT_FILE" |
| 32 | |
| 33 | # Generate a script rather than nesting quotes inside tmux's command string: |
| 34 | # tmux hands that string to `sh -c`, so an argument containing spaces or quotes |
| 35 | # (an autonomous session's prompt, most obviously) would otherwise re-split. |
| 36 | { |
| 37 | printf '#!/bin/sh\n' |
| 38 | printf 'cd %s || exit 1\n' "$(quote "$WORKDIR")" |
| 39 | for arg in "$@"; do |
| 40 | printf '%s ' "$(quote "$arg")" |
| 41 | done |
| 42 | printf '\n' |
| 43 | printf 'printf %%s $? > %s\n' "$(quote "$EXIT_FILE")" |
| 44 | } > "$CMD_FILE" |
| 45 | chmod 0755 "$CMD_FILE" |
| 46 | |
| 47 | tmux -f /etc/anvil/tmux.conf new-session -d -s "$SESSION" -c "$WORKDIR" \ |
| 48 | "sh $CMD_FILE" |
| 49 | |
| 50 | # Tee everything the pane emits into the transcript. `-o` means "only if not |
| 51 | # already piping", so a re-invocation is harmless. |
| 52 | tmux pipe-pane -o -t "$SESSION" "cat >> '$TRANSCRIPT'" |
| 53 | |
| 54 | # Block until the session is gone. Polling rather than `tmux wait-for` because |
| 55 | # the session can also be killed from outside (an operator stop, a sweep, an |
| 56 | # attached user typing `exit`), and has-session covers every one of those |
| 57 | # without needing a cooperating signaller. |
| 58 | while tmux has-session -t "$SESSION" 2>/dev/null; do |
| 59 | if [ -f "$EXIT_FILE" ]; then |
| 60 | # The command finished; the pane lingers because of remain-on-exit. |
| 61 | # Leave the final screen readable for a moment, then wind up. |
| 62 | sleep "${ANVIL_LINGER_SECS:-5}" |
| 63 | tmux kill-session -t "$SESSION" 2>/dev/null || true |
| 64 | break |
| 65 | fi |
| 66 | sleep 1 |
| 67 | done |
| 68 | |
| 69 | exit "$(cat "$EXIT_FILE" 2>/dev/null || echo 0)" |