caproom 0.7.2 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,8 +51,11 @@ caproom --limit 4096 --docker --image python:3.12-slim -- python train.py
51
51
  | `--docker` | off | opt in to the Docker cgroup backend instead of the default host-native watchdog |
52
52
  | `--image <name>` | `node:22-slim` | docker image used by the `--docker` backend |
53
53
  | `--force-watchdog` | — | legacy no-op; the watchdog IS the default. Accepted so existing scripts and `init` snippets keep working |
54
+ | `--no-intercept-tty` | off | bypass stdio interposition for TUI/pty apps — `exec` directly and monitor via detached `watch --auto-park` (preserves `OSC 10/11`, `DSR CPR`, mouse `DEC 1003`) |
55
+ | `--pty` | off | allocate a real pty via `forkpty` (`scripts/pty_wrapper.py` python or `script`) and forward bytes verbatim — full terminal fidelity, watchdog enforces cap on pty tree |
56
+ | `--no-pty` | — | disable pty allocation (fallback to bypass/watchdog) |
54
57
 
55
- Env var overrides: `CAPROOM_LIMIT_MB`, `CAPROOM_IMAGE`, `CAPROOM_INTERVAL`, `CAPROOM_GRACE`.
58
+ Env var overrides: `CAPROOM_LIMIT_MB`, `CAPROOM_IMAGE`, `CAPROOM_INTERVAL`, `CAPROOM_GRACE`, `CAPROOM_BYPASS_TTY=1` (force bypass), `CAPROOM_PTY=1` (force pty, same as `--pty`).
56
59
 
57
60
  On cap breach, the watchdog backend sends `SIGTERM` first and waits `--grace` seconds before `SIGKILL`. If the process exits cleanly during the grace window, `caproom` propagates its real exit code; only a hard `SIGKILL` (process ignored `SIGTERM`, or grace ran out) reports `137` (same convention as Docker's own OOM-kill exit code, which the docker backend always uses on breach since Docker itself sends the kill).
58
61
 
@@ -94,6 +97,8 @@ caproom init claude --limit 6144 --grace 10 >> ~/.zshrc && source ~/.zshrc
94
97
 
95
98
  This appends a shell function that wraps `claude` through the watchdog backend (host-native — no Docker isolation, so the wrapped command keeps its normal filesystem/auth/PATH access) and an alias so plain `claude` picks it up. Per-shell override without editing the rc file: `CAPROOM_LIMIT_MB=8192 claude ...`. Works for any command, not just `claude` — `caproom init npm --limit 2048` wraps `npm` the same way.
96
99
 
100
+ **TUI / pty note:** `caproom -- <TUI>` (opencode, claude, vim, htop, …) would break the pty — `OSC 10/11` (`^[]10;rgb:...`), `DSR CPR` (`^[[5;1R`), and mouse `^[[<35;` reports leak as text. Since 0.7.3, `caproom` detects `[ -t 0 ] && [ -t 1 ]` for known TUIs and **bypasses stdio interposition**: the TUI `exec`s directly and a detached `caproom watch --auto-park` monitors its pid tree (same cap, no pty corruption). For full pty fidelity, use `--pty` / `CAPROOM_PTY=1` which allocates a real pty via `forkpty` (`python3` `scripts/pty_wrapper.py` or `script`) and forwards bytes verbatim while the watchdog still enforces the cap on the pty tree. Piped/batch `caproom -- opencode run "task"` stays fully capped via the watchdog. Override: `CAPROOM_BYPASS_TTY=1` / `--no-intercept-tty` forces bypass, `CAPROOM_PTY=1` / `--pty` forces pty for any command. `caproom init <TUI>` prints a warning and emits the tty-aware wrapper.
101
+
97
102
  ## setup / bind — shell integration for every terminal
98
103
 
99
104
  `caproom setup` binds headroom management to your shells at the **shell layer**, so it works in any terminal (Terminal.app, iTerm2, Ghostty, Windows Terminal) without touching terminal-specific config:
package/bin/caproom CHANGED
@@ -34,6 +34,14 @@ usage: caproom [--limit <mb>] [--image <docker-image>] [--interval <sec>] -- <co
34
34
  (default: node:22-slim)
35
35
  --force-watchdog no-op; the host-native watchdog IS the default — kept so
36
36
  existing scripts and 'init' snippets keep working
37
+ --no-intercept-tty bypass stdio interposition for TUI/pty apps (OSC 10/11,
38
+ DSR CPR, mouse DEC 1003) — exec directly and monitor via
39
+ detached 'caproom watch --auto-park' instead
40
+ --pty allocate a pty (forkpty) and forward bytes verbatim — full
41
+ terminal fidelity for TUI (requires python3 or script)
42
+ implies --no-intercept-tty + pty watchdog on the pty
43
+ tree; fallback to bypass if pty alloc fails
44
+ --no-pty disable pty allocation (use bypass or watchdog)
37
45
 
38
46
  park / wake — freeze an idle process so the kernel CAN reclaim/compress its
39
47
  memory without killing it. Honest semantics: SIGSTOP only makes the pages
@@ -61,7 +69,14 @@ output to your shell rc (~/.zshrc, ~/.bashrc):
61
69
 
62
70
  caproom init claude >> ~/.zshrc && source ~/.zshrc
63
71
 
64
- env vars (override flags): CAPROOM_LIMIT_MB, CAPROOM_IMAGE, CAPROOM_INTERVAL, CAPROOM_GRACE
72
+ env vars (override flags): CAPROOM_LIMIT_MB, CAPROOM_IMAGE, CAPROOM_INTERVAL, CAPROOM_GRACE, CAPROOM_BYPASS_TTY=1, CAPROOM_PTY=1
73
+
74
+ TUI note: 'caproom -- <TUI>' (opencode, vim, htop, ...) bypasses stdio when [ -t 0 ] && [ -t 1 ]
75
+ so terminal queries (OSC, mouse) stay on the pty. The cap is then advisory via a detached
76
+ 'caproom watch --auto-park' monitor, or via '--pty' which allocates a real pty (forkpty)
77
+ and forwards bytes verbatim (python3 or script). Piped/batch 'caproom -- opencode run "task"'
78
+ stays fully capped. Use --no-intercept-tty / CAPROOM_BYPASS_TTY=1 to force bypass, --pty /
79
+ CAPROOM_PTY=1 to force pty.
65
80
 
66
81
  examples:
67
82
  caproom --limit 2048 -- npm run build
@@ -92,14 +107,48 @@ cmd_init() {
92
107
  esac
93
108
  done
94
109
  local fn="${target}_capped"
95
- cat << EOF
110
+ local is_tui=0
111
+ case "$target" in
112
+ opencode|claude|codex|vim|nvim|htop|less|fzf|nano|emacs) is_tui=1 ;;
113
+ esac
114
+ if [[ $is_tui -eq 1 ]]; then
115
+ echo "caproom: warning: '$target' is a TUI — interactive tty runs will bypass stdio and use a detached 'caproom watch --auto-park' monitor to preserve pty (OSC 10/11, DSR CPR, mouse DEC 1003). Batch 'caproom -- $target run' stays fully capped. Set CAPROOM_BYPASS_TTY=1 to always bypass, or CAPROOM_BYPASS_TTY=0 + re-init to force legacy pipe mode." >&2
116
+ cat << EOF
96
117
  # caproom: auto-cap '$target' — added by 'caproom init $target'
97
118
  # override per-shell: CAPROOM_LIMIT_MB=8192 $target ...
119
+ # TUI note: when [ -t 0 ] && [ -t 1 ], '$target' runs directly and a detached 'caproom watch --auto-park' monitors it
120
+ # (preserves OSC 10/11, DSR CPR, mouse DEC 1003). Batch/headless (piped) still goes through the watchdog.
121
+ # Set CAPROOM_BYPASS_TTY=1 to always bypass, 0 to force legacy mode.
98
122
  $fn() {
123
+ if [ "\${CAPROOM_BYPASS_TTY:-0}" = "1" ]; then
124
+ command caproom --no-intercept-tty --limit "\${CAPROOM_LIMIT_MB:-$limit}" --grace "\${CAPROOM_GRACE:-$grace}" -- command $target "\$@"
125
+ return \$?
126
+ fi
127
+ if [ -t 0 ] && [ -t 1 ]; then
128
+ command $target "\$@" &
129
+ local _cap_pid=\$!
130
+ command caproom watch --threshold-mb "\${CAPROOM_LIMIT_MB:-$limit}" --interval "\${CAPROOM_INTERVAL:-0.2}" --auto-park --json "\$_cap_pid" >/dev/null 2>&1 &
131
+ wait "\$_cap_pid"
132
+ return \$?
133
+ fi
99
134
  command caproom --limit "\${CAPROOM_LIMIT_MB:-$limit}" --force-watchdog --grace "\${CAPROOM_GRACE:-$grace}" -- command $target "\$@"
100
135
  }
101
136
  alias $target=$fn
102
137
  EOF
138
+ else
139
+ cat << EOF
140
+ # caproom: auto-cap '$target' — added by 'caproom init $target'
141
+ # override per-shell: CAPROOM_LIMIT_MB=8192 $target ...
142
+ $fn() {
143
+ if [ "\${CAPROOM_BYPASS_TTY:-0}" = "1" ]; then
144
+ command caproom --no-intercept-tty --limit "\${CAPROOM_LIMIT_MB:-$limit}" --grace "\${CAPROOM_GRACE:-$grace}" -- command $target "\$@"
145
+ return \$?
146
+ fi
147
+ command caproom --limit "\${CAPROOM_LIMIT_MB:-$limit}" --force-watchdog --grace "\${CAPROOM_GRACE:-$grace}" -- command $target "\$@"
148
+ }
149
+ alias $target=$fn
150
+ EOF
151
+ fi
103
152
  }
104
153
 
105
154
  cmd_park() {
@@ -706,6 +755,10 @@ IMAGE="${CAPROOM_IMAGE:-node:22-slim}"
706
755
  INTERVAL="${CAPROOM_INTERVAL:-0.2}"
707
756
  GRACE="${CAPROOM_GRACE:-5}"
708
757
  USE_DOCKER=0
758
+ BYPASS_TTY=0
759
+ USE_PTY=0
760
+ [[ "${CAPROOM_BYPASS_TTY:-0}" == "1" ]] && BYPASS_TTY=1
761
+ [[ "${CAPROOM_PTY:-0}" == "1" ]] && USE_PTY=1
709
762
 
710
763
  while [[ $# -gt 0 ]]; do
711
764
  case "$1" in
@@ -717,6 +770,9 @@ while [[ $# -gt 0 ]]; do
717
770
  # legacy no-op: the watchdog IS the default now; accepted so old
718
771
  # scripts and init snippets keep working
719
772
  --force-watchdog) shift ;;
773
+ --no-intercept-tty) BYPASS_TTY=1; shift ;;
774
+ --pty) USE_PTY=1; shift ;;
775
+ --no-pty) USE_PTY=0; shift ;;
720
776
  --) shift; break ;;
721
777
  -h|--help) usage help ;;
722
778
  *) break ;;
@@ -740,13 +796,155 @@ collect_tree() {
740
796
  walk_tree "$1"
741
797
  }
742
798
 
799
+ is_known_tui() {
800
+ case "${1##*/}" in
801
+ opencode|claude|codex|vim|nvim|htop|less|fzf|nano|emacs) return 0 ;;
802
+ *) return 1 ;;
803
+ esac
804
+ }
805
+
806
+ SPAWN_PID=""
807
+ spawn_with_pty() {
808
+ # Try python3 pty_wrapper.py, then script fallback. Sets SPAWN_PID on success.
809
+ local wrapper
810
+ wrapper="$(dirname "${BASH_SOURCE[0]:-$0}")/../scripts/pty_wrapper.py"
811
+ if [[ ! -f "$wrapper" ]]; then
812
+ local cap_path
813
+ cap_path="$(command -v caproom 2>/dev/null || echo "")"
814
+ if [[ -n "$cap_path" ]]; then
815
+ wrapper="$(dirname "$cap_path")/../scripts/pty_wrapper.py"
816
+ [[ ! -f "$wrapper" ]] && wrapper="$(dirname "$(dirname "$cap_path")")/scripts/pty_wrapper.py"
817
+ fi
818
+ fi
819
+ [[ ! -f "$wrapper" ]] && wrapper="$HOME/Developer/caproom/scripts/pty_wrapper.py"
820
+ if [[ -f "$wrapper" ]] && command -v python3 >/dev/null 2>&1 && python3 -c "import pty" 2>/dev/null; then
821
+ # Avoid stdout capture issue: run in a way that doesn't hold the caller's command-substitution pipe.
822
+ # Use a temp file to pass pid back if needed, but we set global.
823
+ python3 "$wrapper" "$@" &
824
+ SPAWN_PID=$!
825
+ return 0
826
+ fi
827
+ if command -v script >/dev/null 2>&1; then
828
+ if [[ "$(uname)" == "Darwin" ]]; then
829
+ script -q /dev/null "$@" &
830
+ SPAWN_PID=$!
831
+ return 0
832
+ else
833
+ local cmd
834
+ cmd=$(printf '%q ' "$@")
835
+ script -q -c "$cmd" /dev/null &
836
+ SPAWN_PID=$!
837
+ return 0
838
+ fi
839
+ fi
840
+ return 1
841
+ }
842
+
743
843
  run_watchdog() {
844
+ # PTY mode: allocate a real pty (forkpty) and forward bytes verbatim.
845
+ # Full terminal fidelity — OSC 10/11, DSR CPR, mouse DEC 1003 all work
846
+ # because the TUI talks to a real pty, not a pipe. Watchdog still enforces
847
+ # the cap on the pty tree (same kill logic as normal).
848
+ if [[ "$USE_PTY" -eq 1 ]]; then
849
+ echo "caproom: pty mode — allocating pty via forkpty for '${1##*/}' (limit ${LIMIT_MB}m, verbatim forwarding)" >&2
850
+ printf '\033[?1049l\033[?1000l\033[?1002l\033[?1003l\033[?1006l\033[?2004l\033[?25h' >/dev/tty 2>/dev/null || true
851
+ local saved_stty=""
852
+ if [[ -t 0 ]]; then
853
+ saved_stty=$(stty -g </dev/tty 2>/dev/null || true)
854
+ fi
855
+ restore_pty() {
856
+ [[ -n "$saved_stty" ]] && stty "$saved_stty" </dev/tty 2>/dev/null \
857
+ || { [[ -t 0 ]] && stty sane </dev/tty 2>/dev/null || true; }
858
+ printf '\033[?1049l\033[?1000l\033[?1002l\033[?1003l\033[?1006l\033[?2004l\033[?25h' >/dev/tty 2>/dev/null || true
859
+ }
860
+ local pty_pid
861
+ if spawn_with_pty "$@"; then
862
+ pty_pid=$SPAWN_PID
863
+ echo "caproom: pty allocated, pid $pty_pid — monitoring tree RSS" >&2
864
+ local limit_kb=$(( LIMIT_MB * 1024 ))
865
+ local exit_code=0
866
+ while kill -0 "$pty_pid" 2>/dev/null; do
867
+ collect_tree "$pty_pid"
868
+ if [[ "${#TREE_PIDS[@]}" -gt 0 && "$TREE_RSS_KB" -gt "$limit_kb" ]]; then
869
+ local overshoot=$(( TREE_RSS_KB * 100 / limit_kb ))
870
+ echo "caproom: pid $pty_pid tree RSS ${TREE_RSS_KB}KB exceeded ${limit_kb}KB cap (+${overshoot}%) — killing pty tree (grace ${GRACE}s)" >&2
871
+ kill -TERM "${TREE_PIDS[@]}" 2>/dev/null || true
872
+ local -a breach_pids=("${TREE_PIDS[@]}")
873
+ local waited=0
874
+ while kill -0 "$pty_pid" 2>/dev/null && [[ "$waited" -lt "$GRACE" ]]; do
875
+ sleep 1
876
+ waited=$(( waited + 1 ))
877
+ done
878
+ local sp sweep=0
879
+ for sp in "${breach_pids[@]}"; do
880
+ if kill -0 "$sp" 2>/dev/null; then
881
+ kill -9 "$sp" 2>/dev/null || true
882
+ sweep=$(( sweep + 1 ))
883
+ fi
884
+ done
885
+ if [[ "$sweep" -gt 0 ]]; then
886
+ echo "caproom: SIGKILLed ${sweep} survivor(s) after grace — exit 137" >&2
887
+ wait "$pty_pid" 2>/dev/null || true
888
+ restore_pty
889
+ exit 137
890
+ fi
891
+ wait "$pty_pid" 2>/dev/null || exit_code=$?
892
+ echo "caproom: pid $pty_pid exited cleanly (code $exit_code) during grace period" >&2
893
+ restore_pty
894
+ exit "$exit_code"
895
+ fi
896
+ sleep "$INTERVAL"
897
+ done
898
+ wait "$pty_pid" 2>/dev/null || exit_code=$?
899
+ restore_pty 2>/dev/null || true
900
+ exit "$exit_code"
901
+ else
902
+ echo "caproom: pty alloc failed — falling back to bypass/watchdog" >&2
903
+ restore_pty 2>/dev/null || true
904
+ fi
905
+ fi
906
+ # TUI bypass: if stdio is a tty and target is a known TUI (or bypass flag),
907
+ # don't sit on stdio — exec directly and monitor via detached watch.
908
+ # This preserves pty semantics (OSC 10/11, DSR CPR, DEC 1003 mouse) that
909
+ # break when a wrapper backgrounds the TUI and steals foreground pgrp.
910
+ if [[ "$BYPASS_TTY" -eq 1 ]] || { [[ -t 0 && -t 1 ]] && is_known_tui "${1:-}"; }; then
911
+ if [[ "$BYPASS_TTY" -eq 1 ]]; then
912
+ echo "caproom: bypass-tty active — exec directly, no stdio interposition (limit ${LIMIT_MB}m advisory via detached watch)" >&2
913
+ else
914
+ echo "caproom: tty TUI detected ('${1##*/}') — bypassing stdio interposition, monitoring via detached watch (limit ${LIMIT_MB}m)" >&2
915
+ fi
916
+ # Clean stale tty modes before handing off, same snapshot hygiene as
917
+ # the normal watchdog path but without taking foreground away.
918
+ printf '\033[?1049l\033[?1000l\033[?1002l\033[?1003l\033[?1006l\033[?2004l\033[?25h' >/dev/tty 2>/dev/null || true
919
+ # Detached watchdog monitors the TUI's pid tree without owning stdio.
920
+ # Resolve our own path (works via shim and direct invocation).
921
+ local _cap_bin="${BASH_SOURCE[0]:-$0}"
922
+ [[ "$_cap_bin" != /* ]] && _cap_bin="$PWD/$_cap_bin"
923
+ if [[ ! -f "$_cap_bin" ]]; then
924
+ _cap_bin="$(command -v caproom 2>/dev/null || echo "$_cap_bin")"
925
+ fi
926
+ "$@" &
927
+ local _tui_pid=$!
928
+ # Fire-and-forget watch; it exits when _tui_pid exits. Suppressed so it
929
+ # never leaks into the TUI's stdout. Threshold is the cap itself.
930
+ bash "$_cap_bin" watch --threshold-mb "$LIMIT_MB" --interval "$INTERVAL" --auto-park --json "$_tui_pid" >/dev/null 2>&1 &
931
+ local _watch_pid=$!
932
+ wait "$_tui_pid"
933
+ local _rc=$?
934
+ # Best-effort cleanup of the detached watcher (it should already exit
935
+ # via all-exited, but kill is cheap if TUI exited fast).
936
+ kill "$_watch_pid" 2>/dev/null || true
937
+ wait "$_watch_pid" 2>/dev/null || true
938
+ exit "$_rc"
939
+ fi
744
940
  echo "caproom: watchdog backend (host-native), limit=${LIMIT_MB}m poll=${INTERVAL}s (process-tree RSS)" >&2
745
941
  local limit_kb=$(( LIMIT_MB * 1024 ))
746
942
  # Terminal hygiene: a TUI child (opencode, claude, ...) puts the tty in
747
943
  # raw + mouse-tracking mode. If WE kill it, it never restores, and the
748
944
  # user's shell then prints mouse reports like [[<35;25;15M as garbage.
749
- # Snapshot termios before spawn so we can put the tty back after a kill.
945
+ # Clear any stale mode from a previous kill BEFORE snapshot, then snapshot
946
+ # the clean state so restore goes to sane, not back to garbage.
947
+ printf '\033[?1049l\033[?1000l\033[?1002l\033[?1003l\033[?1006l\033[?2004l\033[?25h' >/dev/tty 2>/dev/null || true
750
948
  local saved_stty=""
751
949
  if [[ -t 0 ]]; then
752
950
  saved_stty=$(stty -g </dev/tty 2>/dev/null || true)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caproom",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "Memory-cap any command (AI coding agents, builds, background jobs) on macOS, Linux, and Windows — real enforcement via Docker cgroups, Windows Job Objects, or a polling watchdog, plus park/wake to reclaim idle process memory without killing.",
5
5
  "bin": {
6
6
  "caproom": "bin/caproom.js",
@@ -11,7 +11,8 @@
11
11
  "bin/caproom.js",
12
12
  "bin/caproom.ps1",
13
13
  "bin/caproom-mcp.js",
14
- "scripts/postinstall.js"
14
+ "scripts/postinstall.js",
15
+ "scripts/pty_wrapper.py"
15
16
  ],
16
17
  "keywords": [
17
18
  "memory",
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env python3
2
+ """pty_wrapper — allocate a pty and run command with verbatim forwarding, propagating exit code.
3
+
4
+ Usage: pty_wrapper.py <command> [args...]
5
+ """
6
+ import os, sys, pty, select, signal, struct, fcntl, termios
7
+
8
+ def copy_winsize():
9
+ try:
10
+ s = struct.pack("HHHH", 0, 0, 0, 0)
11
+ a = struct.unpack('hhhh', fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, s))
12
+ fcntl.ioctl(master_fd, termios.TIOCSWINSZ, struct.pack("HHHH", a[0], a[1], a[2], a[3]))
13
+ except Exception:
14
+ pass
15
+
16
+ if len(sys.argv) < 2:
17
+ print("usage: pty_wrapper.py <command> [args...]", file=sys.stderr)
18
+ sys.exit(1)
19
+
20
+ pid, master_fd = pty.fork()
21
+ if pid == 0:
22
+ # child
23
+ try:
24
+ os.execvp(sys.argv[1], sys.argv[1:])
25
+ except Exception as e:
26
+ print(f"pty_wrapper: exec failed: {e}", file=sys.stderr)
27
+ os._exit(127)
28
+
29
+ # parent: forward
30
+ # handle window resize
31
+ def handle_winch(sig, frame):
32
+ copy_winsize()
33
+ try:
34
+ signal.signal(signal.SIGWINCH, handle_winch)
35
+ except Exception:
36
+ pass
37
+ copy_winsize()
38
+
39
+ # make stdin non-blocking if it's a tty
40
+ stdin_fd = sys.stdin.fileno()
41
+ stdout_fd = sys.stdout.fileno()
42
+ is_tty = os.isatty(stdin_fd)
43
+
44
+ # set master to non-blocking? select handles
45
+ import errno
46
+
47
+ exit_code = 127
48
+ try:
49
+ while True:
50
+ r, _, _ = select.select([master_fd] + ([stdin_fd] if is_tty else []), [], [], 0.1)
51
+ if master_fd in r:
52
+ try:
53
+ data = os.read(master_fd, 1024)
54
+ except OSError as e:
55
+ if e.errno == errno.EIO:
56
+ data = b''
57
+ else:
58
+ raise
59
+ if not data:
60
+ # EOF — child may have exited
61
+ pass
62
+ else:
63
+ os.write(stdout_fd, data)
64
+ if is_tty and stdin_fd in r:
65
+ try:
66
+ data = os.read(stdin_fd, 1024)
67
+ except OSError:
68
+ data = b''
69
+ if data:
70
+ os.write(master_fd, data)
71
+ else:
72
+ # stdin EOF
73
+ pass
74
+ # check if child exited
75
+ try:
76
+ pid2, status = os.waitpid(pid, os.WNOHANG)
77
+ if pid2 != 0:
78
+ if os.WIFEXITED(status):
79
+ exit_code = os.WEXITSTATUS(status)
80
+ elif os.WIFSIGNALED(status):
81
+ exit_code = 128 + os.WTERMSIG(status)
82
+ else:
83
+ exit_code = 1
84
+ # drain remaining master data
85
+ try:
86
+ while True:
87
+ r2, _, _ = select.select([master_fd], [], [], 0.1)
88
+ if not r2:
89
+ break
90
+ d = os.read(master_fd, 1024)
91
+ if not d:
92
+ break
93
+ os.write(stdout_fd, d)
94
+ except Exception:
95
+ pass
96
+ break
97
+ except ChildProcessError:
98
+ break
99
+ finally:
100
+ try:
101
+ os.close(master_fd)
102
+ except Exception:
103
+ pass
104
+ sys.exit(exit_code)