Skip to content

Commit 39f32be

Browse files
fix: process manager tests tty (#7156)
### Description I realized that the process manager tests were broken if they were run from a TTY on non-windows. This PR does a few things: - Switches `use_pty` calculation to happen once during process manager construction. This saves a `isatty` syscall from each process getting spawned. - Fixes how we constructed `portable_pty::CommandBuilder` as it differs from the stdlib where if `cwd` isn't specified it uses the user home dir instead of the current process cwd [source](https://docs.rs/portable-pty/latest/src/portable_pty/cmdbuilder.rs.html#458). This came up because process manager tests don't use absolute paths to specify script paths where the child tests use absolute paths so cwd doesn't matter. We could update the process manager tests to cover both codepaths, but the spawn method shouldn't impact the process manager behavior and should be captured by the child tests. ### Testing Instructions Pull onto your machine and verify that `cargo tr-test` passes now. Closes TURBO-2185
1 parent 7145362 commit 39f32be

File tree

3 files changed

+39
-23
lines changed

3 files changed

+39
-23
lines changed

crates/turborepo-lib/src/process/command.rs

+5
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,11 @@ impl From<Command> for portable_pty::CommandBuilder {
152152
cmd.args(args);
153153
if let Some(cwd) = cwd {
154154
cmd.cwd(cwd.as_std_path());
155+
} else if let Ok(cwd) = std::env::current_dir() {
156+
// portably_pty defaults to a users home directory instead of cwd if one isn't
157+
// configured on the command builder.
158+
// We explicitly set the cwd if one exists to avoid this behavior
159+
cmd.cwd(&cwd);
155160
}
156161
for (key, value) in env {
157162
cmd.env(key, value);

crates/turborepo-lib/src/process/mod.rs

+33-22
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ pub use self::child::{Child, ChildExit};
3030
/// using `spawn`. When the manager is Closed, all currently-running children
3131
/// will be closed, and no new children can be spawned.
3232
#[derive(Debug, Clone)]
33-
pub struct ProcessManager(Arc<Mutex<ProcessManagerInner>>);
33+
pub struct ProcessManager {
34+
state: Arc<Mutex<ProcessManagerInner>>,
35+
use_pty: bool,
36+
}
3437

3538
#[derive(Debug)]
3639
struct ProcessManagerInner {
@@ -39,11 +42,22 @@ struct ProcessManagerInner {
3942
}
4043

4144
impl ProcessManager {
42-
pub fn new() -> Self {
43-
Self(Arc::new(Mutex::new(ProcessManagerInner {
44-
is_closing: false,
45-
children: Vec::new(),
46-
})))
45+
pub fn new(use_pty: bool) -> Self {
46+
Self {
47+
state: Arc::new(Mutex::new(ProcessManagerInner {
48+
is_closing: false,
49+
children: Vec::new(),
50+
})),
51+
use_pty,
52+
}
53+
}
54+
55+
/// Construct a process manager and infer if pty should be used
56+
pub fn infer() -> Self {
57+
// Only use PTY if we're not on windows and we're currently hooked up to a
58+
// in a TTY
59+
let use_pty = !cfg!(windows) && atty::is(atty::Stream::Stdout);
60+
Self::new(use_pty)
4761
}
4862
}
4963

@@ -61,17 +75,14 @@ impl ProcessManager {
6175
command: Command,
6276
stop_timeout: Duration,
6377
) -> Option<io::Result<child::Child>> {
64-
let mut lock = self.0.lock().unwrap();
78+
let mut lock = self.state.lock().unwrap();
6579
if lock.is_closing {
6680
return None;
6781
}
68-
// Only use PTY if we're not on windows and we're currently hooked up to a
69-
// in a TTY
70-
let use_pty = !cfg!(windows) && atty::is(atty::Stream::Stdout);
7182
let child = child::Child::spawn(
7283
command,
7384
child::ShutdownStyle::Graceful(stop_timeout),
74-
use_pty,
85+
self.use_pty,
7586
);
7687
if let Ok(child) = &child {
7788
lock.children.push(child.clone());
@@ -108,7 +119,7 @@ impl ProcessManager {
108119
let mut set = JoinSet::new();
109120

110121
{
111-
let mut lock = self.0.lock().expect("not poisoned");
122+
let mut lock = self.state.lock().expect("not poisoned");
112123
lock.is_closing = true;
113124
for child in lock.children.iter() {
114125
let child = child.clone();
@@ -123,7 +134,7 @@ impl ProcessManager {
123134
}
124135

125136
{
126-
let mut lock = self.0.lock().expect("not poisoned");
137+
let mut lock = self.state.lock().expect("not poisoned");
127138

128139
// just allocate a new vec rather than clearing the old one
129140
lock.children = vec![];
@@ -155,7 +166,7 @@ mod test {
155166

156167
#[tokio::test]
157168
async fn test_basic() {
158-
let manager = ProcessManager::new();
169+
let manager = ProcessManager::new(false);
159170
let mut child = manager
160171
.spawn(get_script_command("hello_world.js"), Duration::from_secs(2))
161172
.unwrap()
@@ -168,7 +179,7 @@ mod test {
168179

169180
#[tokio::test]
170181
async fn test_multiple() {
171-
let manager = ProcessManager::new();
182+
let manager = ProcessManager::new(false);
172183

173184
let children = (0..2)
174185
.map(|_| {
@@ -191,7 +202,7 @@ mod test {
191202

192203
#[tokio::test]
193204
async fn test_closed() {
194-
let manager = ProcessManager::new();
205+
let manager = ProcessManager::new(false);
195206
let mut child = manager
196207
.spawn(get_command(), Duration::from_secs(2))
197208
.unwrap()
@@ -218,7 +229,7 @@ mod test {
218229

219230
#[tokio::test]
220231
async fn test_exit_code() {
221-
let manager = ProcessManager::new();
232+
let manager = ProcessManager::new(false);
222233
let mut child = manager
223234
.spawn(get_script_command("hello_world.js"), Duration::from_secs(2))
224235
.unwrap()
@@ -235,7 +246,7 @@ mod test {
235246
#[tokio::test]
236247
#[traced_test]
237248
async fn test_message_after_stop() {
238-
let manager = ProcessManager::new();
249+
let manager = ProcessManager::new(false);
239250
let mut child = manager
240251
.spawn(get_script_command("hello_world.js"), Duration::from_secs(2))
241252
.unwrap()
@@ -258,14 +269,14 @@ mod test {
258269

259270
#[tokio::test]
260271
async fn test_reuse_manager() {
261-
let manager = ProcessManager::new();
272+
let manager = ProcessManager::new(false);
262273
manager.spawn(get_command(), Duration::from_secs(2));
263274

264275
sleep(Duration::from_millis(100)).await;
265276

266277
manager.stop().await;
267278

268-
assert!(manager.0.lock().unwrap().children.is_empty());
279+
assert!(manager.state.lock().unwrap().children.is_empty());
269280

270281
// TODO: actually do some check that this is idempotent
271282
// idempotent
@@ -280,7 +291,7 @@ mod test {
280291
script: &str,
281292
expected: Option<ChildExit>,
282293
) {
283-
let manager = ProcessManager::new();
294+
let manager = ProcessManager::new(false);
284295
let tasks = FuturesUnordered::new();
285296

286297
for _ in 0..10 {
@@ -315,7 +326,7 @@ mod test {
315326

316327
#[tokio::test]
317328
async fn test_wait_multiple_tasks() {
318-
let manager = ProcessManager::new();
329+
let manager = ProcessManager::new(false);
319330

320331
let mut out = Vec::new();
321332
let mut child = manager

crates/turborepo-lib/src/run/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ pub struct Run {
6969

7070
impl Run {
7171
pub fn new(base: CommandBase, api_auth: Option<APIAuth>) -> Result<Self, Error> {
72-
let processes = ProcessManager::new();
72+
let processes = ProcessManager::infer();
7373
let mut opts: Opts = base.args().try_into()?;
7474
let config = base.config()?;
7575
let is_linked = turborepo_api_client::is_linked(&api_auth);

0 commit comments

Comments
 (0)