You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2026/10/23 21:32:00 UTC

(juneau) branch docs updated: Show one board when push.py hands off to test.py (the child draws the parent's rows and hands its own back instead of printing them), and ship tmux-pane.sh and watch-run.sh inside juneau-run with bootstrap.py --bin to locate them

This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch docs
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/docs by this push:
     new 3d90f8e50d Show one board when push.py hands off to test.py (the child draws the parent's rows and hands its own back instead of printing them), and ship tmux-pane.sh and watch-run.sh inside juneau-run with bootstrap.py --bin to locate them
3d90f8e50d is described below

commit 3d90f8e50d1ee9a551d7fee2e09e137929800368
Author: James Bognar <ja...@apache.org>
AuthorDate: Fri Oct 23 17:31:54 2026 -0400

    Show one board when push.py hands off to test.py (the child draws the parent's rows and hands its own back instead of printing them), and ship tmux-pane.sh and watch-run.sh inside juneau-run with bootstrap.py --bin to locate them
---
 pages/topics/22.01.AgentSidePane.md | 267 ++++++++++++++++--------------------
 1 file changed, 116 insertions(+), 151 deletions(-)

diff --git a/pages/topics/22.01.AgentSidePane.md b/pages/topics/22.01.AgentSidePane.md
index 09f52900a7..09518beaba 100644
--- a/pages/topics/22.01.AgentSidePane.md
+++ b/pages/topics/22.01.AgentSidePane.md
@@ -11,13 +11,12 @@ board view.
 ## The approach
 
 1. Run the agent inside tmux.
-2. Have the agent start the long command in a side pane with `tmux split-window -h -d`. The pane has a real
-   terminal, so juneau-run draws its live view there, and `-d` keeps focus where you are typing. This is the
-   **leader** pane.
-3. Stack a second pane, the **follower**, below it with `tmux split-window -v -d`. It runs `tail -F` on the full log,
-   so you also see every line of output, not only the board rows.
-4. The agent blocks on `tmux wait-for` until the leader signals that the command is done.
-5. It reads the result back with `tmux capture-pane` and the command's exit code, and carries on.
+2. Have the agent start the long command in a side pane. The pane has a real terminal, so juneau-run draws its live
+   view there, and focus stays where you are typing. This is the **leader** pane.
+3. A second pane, the **follower**, sits below it. It runs `tail -F` on the full log, so you also see every line of
+   output, not only the board rows.
+4. The agent blocks until the command is done.
+5. It reads the result back from the pane's last lines and the command's exit code, and carries on.
 
 You watch the run as it happens. The agent still gets what it needs: the final rows and a pass or fail.
 
@@ -33,146 +32,118 @@ With `JUNEAU_RUN_CONSOLE=board` (or `--console board`), a run in the leader pane
 
 By default the full log is written under `~/.cache/juneau-run/` (the newest 20 are kept), so the agent can read the
 details of a failure without scrolling the pane. A run also accepts the paths up front, in `JUNEAU_RUN_FULL_LOG` and
-`JUNEAU_RUN_BOARD_LOG`; the helper below uses that. See [Console view](/docs/topics/JuneauRun#console-view) for the
+`JUNEAU_RUN_BOARD_LOG`; `watch-run.sh` uses that. See [Console view](/docs/topics/JuneauRun#console-view) for the
 options.
 
-## A helper script
+## The scripts
 
-This script runs any command in a leader pane with a `tail -F` follower below it, waits for the leader, prints the
-pane's last lines and exits with the command's exit code. Save it as `pane-run.sh`.
+The toolkit zip, `org.apache.juneau:juneau-run:<version>:zip`, carries two bash scripts in `juneau_scripts/bin/`:
+
+- **`tmux-pane.sh`** runs a command in a side pane, blocks until it ends and exits with its code. It also cancels,
+  lists, closes and equalizes such panes. It knows nothing about juneau-run.
+- **`watch-run.sh`** is a wrapper for a juneau-run script (`push.py`, `release.py`, `deploy*.py`). It picks the log and
+  result paths, opens the follower pane, and prints a result summary when the script ends.
+
+They need `bash`, `tmux`, `ps`, `pgrep` and `perl`; `python3` (for the summary) and `lsof` (for the listening ports in
+the orphan report) are optional. They are tested on macOS.
+
+## Getting them
+
+A repository gets them with its copy of the toolkit; there is nothing separate to install.
+
+- **A repository that vendors the toolkit** (a tree pinned to a manifest-checked copy of `juneau-run`) gets
+  `juneau_scripts/bin/` on its next refresh of that copy.
+- **A repository with `scripts/bootstrap.py`** (the loader every push script vendors) asks it for the path with
+  `--bin`. It never runs `mvn`, and it looks at the installed zip, the in-tree sources and the `~/.m2` copy for the pom's
+  `<juneau.version>`.
+- **The Juneau repository** has the scripts in `juneau-run/src/main/python/juneau_scripts/bin/`, and runs them in place.
+
+Locate a script, check it, then use it:
+
+```bash
+wr=$(python3 scripts/bootstrap.py --bin watch-run.sh) && [ -x "$wr" ] && "$wr" -d . -t push -- python3 scripts/push.py "message"
+```
+
+Without a vendored `bootstrap.py`, the installed zip is itself runnable:
+
+```bash
+J=~/.m2/repository/org/apache/juneau/juneau-run/<v>/juneau-run-<v>.zip
+wr=$(python3 "$J" --bin watch-run.sh) && [ -x "$wr" ] && echo "$wr"
+```
+
+When the source is a zip, the scripts are extracted to `~/.cache/juneau-run/bin/<version>/`, outside `target/` so that
+`mvn clean` never deletes the cancel path of a run still going. Each file is renamed into place, so a running script
+keeps the file it opened.
+
+## Using them
+
+```bash
+"$wr" -d ~/git/some/repo -t "push" -- python3 scripts/push.py "message"
+"$wr" --no-tail -- python3 scripts/release.py
+```
+
+`watch-run.sh` prints `tail -f <full log>` and `board: <board log>` first; hand the `tail -f` line to the person
+watching, since it works from any terminal. When the script ends it prints the pane's last lines, a `result:` summary
+and `exit=N`, so read the last `exit=` line. It also closes finished panes left by earlier runs before opening a new
+one; a pane that is still running is left alone.
+
+Options (both scripts take the first group; `watch-run.sh` passes them to `tmux-pane.sh`):
+
+- `-d DIR` working directory, `-n LINES` how many trailing pane lines to print, `-t TITLE` the leader pane's title.
+- `-k fail|always|never` when to keep the panes open after the command ends (default `fail`, until Enter in the leader),
+  and `-K MIN` to close kept panes by themselves after MIN minutes.
+- `--reap` stops anything the command left running; `--keep-on-cancel` keeps a cancelled run's panes per `-k`.
+- `--no-tail` (`watch-run.sh`) leaves out the follower. `--pane 'CMD'` (`tmux-pane.sh`, repeatable, with `--size N%`
+  before a follower) runs several panes; the first is the leader.
+
+Subcommands of `tmux-pane.sh`:
+
+- `cancel [PANE|TITLE|--all]` stops a run from any shell. It sends INT, then TERM, then KILL to the command's process
+  group, `TMUX_PANE_GRACE` seconds apart (default 10), then stops what is left by PID and start time, never by name. The
+  command that does it is exported to the run as `JUNEAU_RUN_CANCEL`.
+- `list` shows each pane (id, title, state, age); `close --kept` or `close PANE` closes finished panes with their
+  followers; `equalize PANE` evens out the column widths.
+
+The leader's exit code is the script's. A usage error, or running outside tmux, exits 2; a leader pane closed before the
+command ended exits 129. Whenever a run ends, processes it started that are still running are listed as orphans, with
+their listening ports.
+
+### Equal widths
+
+The agent pane counts as one column, and each run's leader with its followers is another, so one run gives a 50/50
+split and two runs give thirds, and so on. The columns are equalized when a leader opens and again when a run's panes
+close. The agent pane is remembered in the `@tmux_agent` pane option. Other panes and other windows are not touched,
+and a failed resize only prints a warning.
+
+### The follower filter
+
+The follower shows the log through a filter; the log file itself is unchanged. U+2028, U+2029 and other format
+characters (`\p{Cf}`) become a space, because tmux and the outer terminal disagree on their width and a line's end then
+overflows into the pane to the left. Control characters other than tab, newline and ESC (so colour survives), and DEL, are
+dropped. Invalid UTF-8 becomes U+FFFD. `watch-run.sh --print-tail-filter` prints the filter.
+
+### Without tmux
+
+With `$TMUX` unset both scripts exit 2. Run the command directly and follow its log from another terminal:
 
 ```bash
-#!/bin/bash
-# Usage: pane-run.sh CMD [ARGS...]
-[ -n "${TMUX:-}" ] || { echo "not inside tmux" >&2; exit 2; }
-state=$(mktemp -d) chan="pane-run-$$-$RANDOM"
-log=${JUNEAU_RUN_LOG_DIR:-$HOME/.cache/juneau-run}/run-$(date +%Y%m%d-%H%M%S)-$$.log
-mkdir -p "$(dirname "$log")" && : > "$log"
-echo "tail -f $(printf '%q' "$log")"                # print it first, before the pane opens
-export -p > "$state/env"                            # the pane inherits tmux's environment, not ours
-q() { printf '%q ' "$@"; }
-script="cd $(q "$PWD") || exit 1
-. $(q "$state/env") 2>/dev/null
-JUNEAU_RUN_FULL_LOG=$(q "$log") $(q "$@")
-rc=\$?
-echo \$rc > $(q "$state/exit")
-printf '\\n[exit %s] press Enter to close\\n' \$rc
-tmux wait-for -S $chan
-read -r
-tmux kill-pane -t \"\$(cat $(q "$state/follower"))\"
-rm -rf $(q "$state")"
-pane=$(tmux split-window -h -d -P -F '#{pane_id}' "bash -c $(q "$script")") || exit 2
-tmux split-window -v -d -l 40% -P -F '#{pane_id}' -t "$pane" "tail -F $(q "$log")" > "$state/follower"
-tmux wait-for "$chan"
-rc=$(cat "$state/exit" 2>/dev/null || echo 1)
-tmux capture-pane -p -J -t "$pane" -S -60
-echo "exit=$rc"
-exit "$rc"
+JUNEAU_RUN_CONSOLE=board python3 push.py > run.log 2>&1 &
+tail -f run.log
 ```
 
-Run it as `pane-run.sh env JUNEAU_RUN_CONSOLE=board python3 push.py` or
-`pane-run.sh env JUNEAU_RUN_CONSOLE=board mvn verify`. How it works:
-
-- **Log path chosen up front.** The script picks the log file, creates it empty, prints `tail -f <path>` and only then
-  opens the panes. You can follow the run from any terminal at once, with no guessing at a path from the pane's
-  header. The command gets the path in `JUNEAU_RUN_FULL_LOG`. Set `JUNEAU_RUN_BOARD_LOG` the same way if you want the
-  board rows in a file too; setting only the full log does not create one.
-- **Environment and directory.** The pane's shell starts with the tmux server's environment, so the script writes the
-  caller's exported variables to a file (`export -p`) and sources it in the pane, after a `cd` to the caller's
-  directory. It works with the Bash that ships with macOS and with GNU Bash.
-- **Pane stays open.** After the command ends, the leader waits for Enter, so you can read the final view. Press Enter
-  in it to close both panes. The pane prints the exit line before it signals, so the captured output includes it.
-- **Signal.** The leader runs `tmux wait-for -S`, and the script's `tmux wait-for` returns once it is sent. There is no
-  polling.
-- **Exit code.** The command's status goes through a file, and the script exits with it.
-- **Follower.** `-l 40%` needs tmux 3.1 or later; on older versions use `-p 40`. The follower is not waited on and
-  does not affect the exit code.
-
-This sketch has no way to stop a run. A fuller helper adds the pieces below.
-
-## A fuller helper
-
-A helper you use every day grows a few more features. They are worth building in, because each one fixes a way the
-simple version goes wrong.
-
-### One wrapper for juneau-run scripts
-
-Split the work in two. A generic pane runner knows nothing about juneau-run: it takes one or more commands, opens one
-pane for each, and waits for the first. A thin wrapper (call it `watch-run.sh`) sits on top and owns the juneau-run
-side:
-
-- it picks the full log, board log and result paths, and exports them as `JUNEAU_RUN_FULL_LOG`,
-  `JUNEAU_RUN_BOARD_LOG` and `JUNEAU_RUN_RESULT`;
-- it prints `tail -f <full log>` and `board: <path>` straight away, before the pane opens, so the person can start
-  following at once;
-- it runs the command in the leader and `tail -F` on the full log in the follower, then, when the command ends,
-  prints the result summary (status, exit code, one row per step) after the pane's last lines.
-
-Put the stem of the script in the log name (`push-20261023-093001-4242.log`) and the wrapper's PID at the end, so two
-runs of the same script never share a file. The wrapper has to pass the paths itself: guessing which file in the log
-directory belongs to a run breaks as soon as two runs start together.
-
-The pane runner takes each pane as a string, `--pane 'CMD ...'`. The first is the leader and the caller waits on it;
-the rest are followers, stacked below it in order. `--size 30%` before a `--pane` sets that follower's height as a
-share of the column (the first follower defaults to 40%, later ones split what is left). The runner gives every pane
-two variables: one with the leader's pane id and one with the exact command that cancels the run, which the wrapper
-hands on to the run as `JUNEAU_RUN_CANCEL` so the header can show it.
-
-### Cancelling a run
-
-A pane that dies is not the same as a command that stops. If the agent gives up on a run, the command and everything
-it started must stop too, without touching anything else on the machine. Three rules do that:
-
-1. **Signal the process group, not one PID.** Start the command as its own process group, and send INT, then TERM,
-   then KILL to the whole group, waiting a grace period (say 10 seconds) between them. INT lets a build or a juneau-run
-   script clean up the way Ctrl-C does. In Bash, `set -m` makes a background job its own group, and it works with the
-   Bash 3.2 that ships with macOS (`setsid` doesn't exist there):
-
-   ```bash
-   set -m                            # job control: each background job gets its own process group
-   "$@" &  pgid=$!                   # the group id is the job's PID
-   wait "$pgid"
-   # to cancel, from any shell:
-   kill -INT -- "-$pgid"; sleep 10; kill -TERM -- "-$pgid"; sleep 10; kill -KILL -- "-$pgid"
-   ```
-
-2. **Keep a registry, by PID and start time.** While the command runs, record every descendant about every two
-   seconds: its PID, its start time and its command line, in a file named for the pane. `ps -o lstart= -p PID` prints
-   the start time on both macOS and Linux. Processes that leave the group (a daemon, a dev server, a test browser)
-   are still in the registry. A cancel, run from another shell or another agent, finds the run through that file.
-3. **Reap by PID and start time, never by pattern.** After the group is gone, signal what is left, but only a PID whose
-   start time still matches the one recorded, so a PID that the system has since reused is never hit. Never use
-   `pkill -f push.py` or the like: it kills other people's runs and the next run of the same script.
-
-   ```bash
-   reap() {   # reap PID "START TIME"
-     [ "$(ps -o lstart= -p "$1" | sed 's/^ *//; s/ *$//')" = "$2" ] && kill -TERM "$1"
-   }
-   ```
-
-Whenever a run ends, cancelled or not, report the recorded processes that are still running as **orphans**: PID,
-command line and any TCP port they listen on (`lsof -nP -a -p PID -iTCP -sTCP:LISTEN`). A forgotten dev server or
-browser then shows up in the output instead of silently holding a port. Reap them automatically only when asked
-(a `--reap` option); by default just report.
-
-A cancelled run closes its panes after the output is printed, whatever the keep setting is; a `--keep-on-cancel`
-option turns that off. See [Cancelling](/docs/topics/JuneauRun#cancelling) for what juneau-run does when it gets the
-INT.
-
-### Keeping and closing panes
-
-Closing the panes the moment a command fails throws away the evidence. Add a keep setting:
-
-- `-k fail` (the default) keeps the leader and its followers open when the command exits non-zero, until you press
-  Enter in the leader;
-- `-k always` keeps them after any exit, `-k never` closes them at once;
-- `-K 30` makes kept panes close themselves after 30 minutes, so they don't pile up.
-
-Two more commands keep the screen tidy. `list` shows each pane the helper opened (id, title, state, age) with its
-followers, and `close` closes the finished ones (`close --kept`, or one leader by id), followers included. Mark the
-panes with a tmux pane option (`tmux set-option -p -t %7 @pane_run 1`) so these commands never touch a pane the helper
-didn't open. If the leader's pane is closed or killed before the command ends, the helper exits with a distinct
-status (129 works) and closes the followers.
+## How it works
+
+1. `tmux-pane.sh` opens the leader pane to the right of the pane it was started from, without taking focus, and
+   gives every pane its environment (a pane otherwise starts with the tmux server's).
+2. The command runs as its own process group. While it runs, the script records its descendants about every two
+   seconds, by PID and start time, in a registry under `${TMPDIR:-/tmp}/tmux-pane/`; that registry is what lets `cancel`
+   and the orphan report work from another shell.
+3. The script waits for the leader, prints its last lines, and closes or keeps the panes per `-k`.
+4. `watch-run.sh` adds the juneau-run side: it chooses the log paths before anything opens, so nothing has to guess them,
+   exports `JUNEAU_RUN_FULL_LOG`, `JUNEAU_RUN_BOARD_LOG`, `JUNEAU_RUN_RESULT` and `JUNEAU_RUN_CANCEL`, and reads the
+   result file when the command ends.
+
+Each script's header comment is its full usage text.
 
 ## Tips
 
@@ -180,18 +151,12 @@ status (129 works) and closes the followers.
 - It works for any command juneau-run wraps: `test.py`, `push.py`, `release.py`, or a Maven, pytest or Playwright run
   started through `juneau_run.py`.
 - Hand the person the `tail -f` line. It works from any terminal and shows the same full log as the follower.
-- Read the last `exit=` line. A wrapper that prints the result summary after the pane's last lines prints the code
-  again after the summary.
 - Close the panes with Enter in the leader when you have seen enough; the agent already has its output.
-- Give each helper its own title (`-t`) so `list` and `cancel` can tell runs apart.
-- Edit a helper script while a run is using it by writing a temporary file and `mv`-ing it over the original. A running
-  Bash reads its script as it goes, so editing in place can break a run that is in progress. `mv` within one directory
-  is atomic on macOS and Linux.
-- Quote with `printf '%q'` every value you put into a string that `bash -c` runs. The leader's command passes through
-  two levels of `bash -c`, and an unquoted space or apostrophe in an argument breaks the second one.
-- Outside tmux there is no live pane. Have the agent send the output to a file and follow it from another terminal:
-  `JUNEAU_RUN_CONSOLE=board python3 push.py > run.log 2>&1 &`, then `tail -f run.log`. Off a terminal, each row
-  prints once when it ends, or you can follow the full-log path from the header.
+- Give each run its own title (`-t`) so `list` and `cancel` can tell runs apart.
+- Never use `pkill -f push.py` to stop a run: it kills other people's runs and the next run of the same script. Use
+  `cancel`.
+- Edit a script while a run is using it by writing a temporary file and `mv`-ing it over the original. A running
+  Bash reads its script as it goes, so editing in place can break a run that is in progress.
 
 A tool that needs structured progress and a machine-readable outcome, rather than the pane's text, can follow a
 markers file and read a result file; see [Driving a Run from Another Tool](/docs/topics/DrivingARun).