Skip to main content

max / shop

Shop --exec flag + PTY EOF fix on Linux Added --exec CMD to run `sh -c CMD` instead of $SHELL. Powers the benchmark harness (shop --exec "cat file"; time it) but also useful generally. Real bug found by the harness: on Linux, reading a PTY master after the slave closes returns EIO, not read=0. The Err(_) branch just logged and looped, leaving shop hanging indefinitely after any --exec child exited. Fix: treat any non-transient read error as EOF and set exit=true. WouldBlock and Interrupted still just break the drain loop.
Co-Authored-By
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-24 02:08 UTC
Signed with PGP, not checked
Commit: 5af7c0ebcf60cabd24f091b7ece38ff4699f803b
Parent: b5dc760
1 file changed, +32 insertions, -7 deletions
@@ -67,12 +67,27 @@
67 67
68 68 let font_data: Vec<u8> = FONT_BYTES.to_vec();
69 69
70 - let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".into());
70 + // `--exec CMD` runs `sh -c CMD` instead of the interactive shell — useful
71 + // for benchmarks and one-shot invocations. Shop exits when the child
72 + // exits (PTY EOF triggers the read=0 branch).
73 + let args: Vec<String> = std::env::args().collect();
74 + let exec_cmd = args
75 + .iter()
76 + .position(|a| a == "--exec")
77 + .and_then(|i| args.get(i + 1).cloned());
78 + let (spawn_cmd, spawn_args): (String, Vec<String>) = match &exec_cmd {
79 + Some(cmd) => ("/bin/sh".into(), vec!["-c".into(), cmd.clone()]),
80 + None => (
81 + std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".into()),
82 + Vec::new(),
83 + ),
84 + };
85 + let spawn_args_refs: Vec<&str> = spawn_args.iter().map(String::as_str).collect();
71 86 let cols_initial = grid_cols(INITIAL.0);
72 87 let rows_initial = grid_rows(INITIAL.1);
73 88 let pty = Pty::spawn(
74 - &shell,
75 - &[],
89 + &spawn_cmd,
90 + &spawn_args_refs,
76 91 PtySize {
77 92 cols: cols_initial,
78 93 rows: rows_initial,
@@ -81,7 +96,7 @@
81 96 },
82 97 "xterm-256color",
83 98 )?;
84 - info!(shell = %shell, "spawned shell");
99 + info!(spawn = %spawn_cmd, "spawned child");
85 100
86 101 let conn = Connection::connect_to_env()?;
87 102 let spec = WindowSpec {
@@ -230,9 +245,19 @@
230 245 app.cursor_phase = !app.cursor_phase;
231 246 app.pending_redraw = true;
232 247 }
233 - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
234 - Err(e) => {
235 - warn!("pty read: {e}");
248 + Err(e)
249 + if matches!(
250 + e.kind(),
251 + std::io::ErrorKind::WouldBlock | std::io::ErrorKind::Interrupted
252 + ) =>
253 + {
254 + break;
255 + }
256 + Err(_) => {
257 + // Linux returns EIO on the PTY master once the slave
258 + // has been closed by the child exiting. Treat any
259 + // non-transient read error as EOF and exit.
260 + app.chrome.exit = true;
236 261 break;
237 262 }
238 263 }