1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
//! Subprocess spawning abstraction.
//!
//! [`spawn_cmd`] is a drop-in replacement for `cmd.status()` that routes the
//! child process's output through a PTY to the parallel output mux when a
//! per-thread sink is active, and falls back to the plain `Command::status()`
//! path otherwise.
//!
//! Call sites in `compile.rs` and `test.rs` swap
//! `cmd.status().context("…")?` → `proc::spawn_cmd(&mut cmd).context("…")?`
//! and check `.success()` on the returned [`Status`], which has the same API.
use anyhow::{Context, Result};
use std::process::Command;
/// Opaque process-exit wrapper. Only `.success()` is used by callers.
pub struct Status(bool);
impl Status {
pub fn success(&self) -> bool {
self.0
}
}
/// Run `cmd`, routing its output to the per-thread parallel mux slot (via PTY)
/// when one is active, or running it normally otherwise.
pub fn spawn_cmd(cmd: &mut Command) -> Result<Status> {
if let Some(sink) = crate::parallel::try_get_sink() {
spawn_pty(cmd, &sink)
} else {
let s = cmd.status().context("command failed to start")?;
Ok(Status(s.success()))
}
}
// ── PTY path (Unix only) ──────────────────────────────────────────────────
#[cfg(unix)]
fn spawn_pty(
cmd: &mut Command,
sink: &std::sync::Arc<dyn crate::parallel::LineSink + Send + Sync>,
) -> Result<Status> {
use portable_pty::{native_pty_system, CommandBuilder, PtySize};
use std::io::Read;
let pty_system = native_pty_system();
// Report a narrower PTY width so child output doesn't wrap past the edge
// of the terminal: subtract the prefix column ("name │ ") and keep a
// floor of 40 so very-long prefixes don't produce unusably narrow output.
let cols = terminal_cols()
.saturating_sub(sink.prefix_visual_len() as u16)
.max(40);
let pair = pty_system
.openpty(PtySize {
rows: 50,
cols,
pixel_width: 0,
pixel_height: 0,
})
.context("failed to open PTY")?;
// Build a portable-pty CommandBuilder from the std Command.
let mut cb = CommandBuilder::new(cmd.get_program());
for arg in cmd.get_args() {
cb.arg(arg);
}
for (k, v) in cmd.get_envs() {
match v {
Some(val) => {
cb.env(k, val);
}
None => {
cb.env_remove(k);
}
}
}
// Always set CWD explicitly — portable-pty may not inherit the parent's
// working directory on all platforms when none is given.
let cwd = cmd
.get_current_dir()
.map(|d| d.to_path_buf())
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
cb.cwd(&cwd);
// Override COLUMNS so tools that read it (docker, native-image progress
// bars, etc.) also use the reduced content width rather than the parent's
// full-terminal value.
cb.env("COLUMNS", cols.to_string());
let mut child = pair
.slave
.spawn_command(cb)
.context("failed to spawn command on PTY")?;
// Release our slave handle so reading master gets EOF when the child exits.
drop(pair.slave);
// Read PTY master until EOF (slave closes when child exits).
let mut reader = pair
.master
.try_clone_reader()
.context("failed to clone PTY master reader")?;
let mut line_buf = String::new();
let mut byte_buf = [0u8; 4096];
loop {
match reader.read(&mut byte_buf) {
Ok(0) | Err(_) => break, // EOF or EIO (macOS)
Ok(n) => {
let bytes = &byte_buf[..n];
// Split into lines and forward to the mux slot (push_line logs them).
for ch in String::from_utf8_lossy(bytes).chars() {
if ch == '\n' {
let line = std::mem::take(&mut line_buf);
let line = line.trim_end_matches('\r').to_string();
if !line.is_empty() {
sink.push_line(line);
}
} else if ch != '\r' {
line_buf.push(ch);
}
}
}
}
}
if !line_buf.is_empty() {
sink.push_line(line_buf);
}
let exit = child.wait().context("failed to wait for child process")?;
Ok(Status(exit.success()))
}
// Non-Unix: no PTY support — fall back to normal spawn (sink was set but
// we can't honour it; this path only occurs in non-Unix builds).
#[cfg(not(unix))]
fn spawn_pty(
cmd: &mut Command,
_sink: &std::sync::Arc<dyn crate::parallel::LineSink + Send + Sync>,
) -> Result<Status> {
let s = cmd.status().context("command failed to start")?;
Ok(Status(s.success()))
}
fn terminal_cols() -> u16 {
// Prefer the real terminal width (TIOCGWINSZ); fall back to COLUMNS, then
// to a sane default when stdout is not a TTY (e.g. piped to a file).
crate::term::width()
.or_else(|| std::env::var("COLUMNS").ok().and_then(|v| v.parse().ok()))
.unwrap_or(120)
}