Skip to content

Commit 2ddbf61

Browse files
authored
fix: address multiple issues with foreground controls for pipeline commands (#180)
1 parent ee0b8e3 commit 2ddbf61

12 files changed

+62
-74
lines changed

brush-core/src/commands.rs

+14-24
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,8 @@ pub(crate) enum CommandSpawnResult {
3333

3434
impl CommandSpawnResult {
3535
// TODO: jobs: remove `no_wait`; it doesn't make any sense
36-
// TODO: jobs: figure out how to remove 'shell'
3736
#[allow(clippy::too_many_lines)]
38-
pub async fn wait(
39-
self,
40-
shell: &mut Shell,
41-
no_wait: bool,
42-
) -> Result<CommandWaitResult, error::Error> {
37+
pub async fn wait(self, no_wait: bool) -> Result<CommandWaitResult, error::Error> {
4338
#[allow(clippy::ignored_unit_patterns)]
4439
match self {
4540
CommandSpawnResult::SpawnedProcess(mut child) => {
@@ -61,10 +56,6 @@ impl CommandSpawnResult {
6156
),
6257
};
6358

64-
if shell.options.interactive {
65-
sys::terminal::move_self_to_foreground()?;
66-
}
67-
6859
Ok(command_wait_result)
6960
}
7061
CommandSpawnResult::ImmediateExit(exit_code) => Ok(
@@ -354,16 +345,15 @@ pub(crate) fn execute_external_command(
354345
)?;
355346

356347
// Set up process group state.
357-
let required_pgid = if new_pg {
358-
let required_pgid = process_group_id.unwrap_or(0);
359-
348+
if new_pg {
349+
// We need to set up a new process group.
360350
#[cfg(unix)]
361-
cmd.process_group(required_pgid);
362-
363-
required_pgid
364-
} else {
365-
0
366-
};
351+
cmd.process_group(0);
352+
} else if let Some(pgid) = process_group_id {
353+
// We need to join an established process group.
354+
#[cfg(unix)]
355+
cmd.process_group(*pgid);
356+
}
367357

368358
// Register some code to run in the forked child process before it execs
369359
// the target command.
@@ -377,7 +367,7 @@ pub(crate) fn execute_external_command(
377367
// When tracing is enabled, report.
378368
tracing::debug!(
379369
target: trace_categories::COMMANDS,
380-
"Spawning: pgid={required_pgid} cmd='{} {}'",
370+
"Spawning: cmd='{} {}'",
381371
cmd.get_program().to_string_lossy().to_string(),
382372
cmd.get_args()
383373
.map(|a| a.to_string_lossy().to_string())
@@ -387,11 +377,11 @@ pub(crate) fn execute_external_command(
387377
match sys::process::spawn(cmd) {
388378
Ok(child) => {
389379
// Retrieve the pid.
390-
let pid = child.id();
380+
#[allow(clippy::cast_possible_wrap)]
381+
let pid = child.id().map(|id| id as i32);
391382
if let Some(pid) = &pid {
392-
#[allow(clippy::cast_possible_wrap)]
393-
if required_pgid == 0 {
394-
*process_group_id = Some(*pid as i32);
383+
if new_pg {
384+
*process_group_id = Some(*pid);
395385
}
396386
} else {
397387
tracing::warn!("could not retrieve pid for child process");

brush-core/src/interp.rs

+13-2
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ pub struct ExecutionParameters {
116116
pub process_group_policy: ProcessGroupPolicy,
117117
}
118118

119-
#[derive(Clone, Default)]
119+
#[derive(Clone, Debug, Default)]
120120
/// Policy for how to manage spawned external processes.
121121
pub enum ProcessGroupPolicy {
122122
/// Place the process in a new process group.
@@ -324,6 +324,12 @@ async fn spawn_pipeline_processes(
324324
process_group_id,
325325
params: params.clone(),
326326
};
327+
328+
// Make sure that all commands in the pipeline are in the same process group.
329+
if current_pipeline_index > 0 {
330+
pipeline_context.params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
331+
}
332+
327333
spawn_results.push_back(command.execute_in_pipeline(&mut pipeline_context).await?);
328334
process_group_id = pipeline_context.process_group_id;
329335
} else {
@@ -335,6 +341,7 @@ async fn spawn_pipeline_processes(
335341
process_group_id,
336342
params: params.clone(),
337343
};
344+
338345
spawn_results.push_back(command.execute_in_pipeline(&mut pipeline_context).await?);
339346
process_group_id = pipeline_context.process_group_id;
340347
}
@@ -352,7 +359,7 @@ async fn wait_for_pipeline_processes(
352359
let mut stopped_children = vec![];
353360

354361
while let Some(child) = process_spawn_results.pop_front() {
355-
match child.wait(shell, !stopped_children.is_empty()).await? {
362+
match child.wait(!stopped_children.is_empty()).await? {
356363
commands::CommandWaitResult::CommandCompleted(current_result) => {
357364
result = current_result;
358365
shell.last_exit_status = result.exit_code;
@@ -366,6 +373,10 @@ async fn wait_for_pipeline_processes(
366373
}
367374
}
368375

376+
if shell.options.interactive {
377+
sys::terminal::move_self_to_foreground()?;
378+
}
379+
369380
// If there were stopped jobs, then encapsulate the pipeline as a managed job and hand it
370381
// off to the job manager.
371382
if !stopped_children.is_empty() {

brush-core/src/jobs.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ pub struct Job {
242242
tasks: VecDeque<JobTask>,
243243

244244
/// If available, the process group ID of the job's processes.
245-
pgid: Option<u32>,
245+
pgid: Option<sys::process::ProcessId>,
246246

247247
/// The annotation of the job (e.g., current, previous).
248248
annotation: JobAnnotation,
@@ -417,7 +417,7 @@ impl Job {
417417
}
418418

419419
/// Tries to retrieve a "representative" pid for the job.
420-
pub fn get_representative_pid(&self) -> Option<u32> {
420+
pub fn get_representative_pid(&self) -> Option<sys::process::ProcessId> {
421421
for task in &self.tasks {
422422
match task {
423423
JobTask::External(p) => {
@@ -431,7 +431,7 @@ impl Job {
431431
None
432432
}
433433

434-
pub fn get_process_group_id(&self) -> Option<u32> {
434+
pub fn get_process_group_id(&self) -> Option<sys::process::ProcessId> {
435435
// TODO: Don't assume that the first PID is the PGID.
436436
self.pgid.or_else(|| self.get_representative_pid())
437437
}

brush-core/src/processes.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,21 @@ pub(crate) type WaitableChildProcess = std::pin::Pin<
1010
/// Tracks a child process being awaited.
1111
pub(crate) struct ChildProcess {
1212
/// If available, the process ID of the child.
13-
pid: Option<u32>,
13+
pid: Option<sys::process::ProcessId>,
1414
/// A waitable future that will yield the results of a child process's execution.
1515
exec_future: WaitableChildProcess,
1616
}
1717

1818
impl ChildProcess {
1919
/// Wraps a child process and its future.
20-
pub fn new(pid: Option<u32>, child: sys::process::Child) -> Self {
20+
pub fn new(pid: Option<sys::process::ProcessId>, child: sys::process::Child) -> Self {
2121
Self {
2222
pid,
2323
exec_future: Box::pin(child.wait_with_output()),
2424
}
2525
}
2626

27-
pub fn pid(&self) -> Option<u32> {
27+
pub fn pid(&self) -> Option<sys::process::ProcessId> {
2828
self.pid
2929
}
3030

brush-core/src/sys/stubs/process.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
pub(crate) type ProcessId = i32;
2+
13
pub(crate) struct Child {
24
inner: std::process::Child,
35
}

brush-core/src/sys/stubs/signal.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{error, traps};
1+
use crate::{error, sys, traps};
22

33
pub(crate) fn parse_numeric_signal(_signal: i32) -> Result<traps::TrapSignal, error::Error> {
44
Err(error::Error::InvalidSignal)
@@ -8,11 +8,11 @@ pub(crate) fn parse_os_signal_name(_signal: &str) -> Result<traps::TrapSignal, e
88
Err(error::Error::InvalidSignal)
99
}
1010

11-
pub(crate) fn continue_process(_pid: u32) -> Result<(), error::Error> {
11+
pub(crate) fn continue_process(_pid: sys::process::ProcessId) -> Result<(), error::Error> {
1212
error::unimp("continue process")
1313
}
1414

15-
pub(crate) fn kill_process(_pid: u32) -> Result<(), error::Error> {
15+
pub(crate) fn kill_process(_pid: sys::process::ProcessId) -> Result<(), error::Error> {
1616
error::unimp("kill process")
1717
}
1818

brush-core/src/sys/stubs/terminal.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::error;
1+
use crate::{error, sys};
22

33
#[derive(Clone)]
44
pub(crate) struct TerminalSettings {}
@@ -32,19 +32,19 @@ pub(crate) fn is_stdin_a_terminal() -> Result<bool, error::Error> {
3232
Ok(false)
3333
}
3434

35-
pub(crate) fn get_parent_process_id() -> Option<u32> {
35+
pub(crate) fn get_parent_process_id() -> Option<sys::process::ProcessId> {
3636
None
3737
}
3838

39-
pub(crate) fn get_process_group_id() -> Option<u32> {
39+
pub(crate) fn get_process_group_id() -> Option<sys::process::ProcessId> {
4040
None
4141
}
4242

43-
pub(crate) fn get_foreground_pid() -> Option<u32> {
43+
pub(crate) fn get_foreground_pid() -> Option<sys::process::ProcessId> {
4444
None
4545
}
4646

47-
pub(crate) fn move_to_foreground(_pid: u32) -> Result<(), error::Error> {
47+
pub(crate) fn move_to_foreground(_pid: sys::process::ProcessId) -> Result<(), error::Error> {
4848
Ok(())
4949
}
5050

brush-core/src/sys/tokio_process.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub(crate) type ProcessId = i32;
12
pub(crate) use tokio::process::Child;
23

34
pub(crate) fn spawn(command: std::process::Command) -> std::io::Result<Child> {

brush-core/src/sys/unix/signal.rs

+7-14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::str::FromStr;
22

3-
use crate::{error, traps};
3+
use crate::{error, sys, traps};
44

55
pub(crate) fn parse_numeric_signal(signal: i32) -> Result<traps::TrapSignal, error::Error> {
66
Ok(traps::TrapSignal::Signal(
@@ -14,23 +14,16 @@ pub(crate) fn parse_os_signal_name(signal: &str) -> Result<traps::TrapSignal, er
1414
))
1515
}
1616

17-
pub(crate) fn continue_process(pid: u32) -> Result<(), error::Error> {
17+
pub(crate) fn continue_process(pid: sys::process::ProcessId) -> Result<(), error::Error> {
1818
#[allow(clippy::cast_possible_wrap)]
19-
nix::sys::signal::kill(
20-
nix::unistd::Pid::from_raw(pid as i32),
21-
nix::sys::signal::SIGCONT,
22-
)
23-
.map_err(|_errno| error::Error::FailedToSendSignal)?;
19+
nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid), nix::sys::signal::SIGCONT)
20+
.map_err(|_errno| error::Error::FailedToSendSignal)?;
2421
Ok(())
2522
}
2623

27-
pub(crate) fn kill_process(pid: u32) -> Result<(), error::Error> {
28-
#[allow(clippy::cast_possible_wrap)]
29-
nix::sys::signal::kill(
30-
nix::unistd::Pid::from_raw(pid as i32),
31-
nix::sys::signal::SIGKILL,
32-
)
33-
.map_err(|_errno| error::Error::FailedToSendSignal)?;
24+
pub(crate) fn kill_process(pid: sys::process::ProcessId) -> Result<(), error::Error> {
25+
nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid), nix::sys::signal::SIGKILL)
26+
.map_err(|_errno| error::Error::FailedToSendSignal)?;
3427

3528
Ok(())
3629
}

brush-core/src/sys/unix/terminal.rs

+9-17
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::error;
1+
use crate::{error, sys};
22
use std::os::fd::{AsFd, AsRawFd};
33

44
#[derive(Clone)]
@@ -48,31 +48,23 @@ pub(crate) fn is_stdin_a_terminal() -> Result<bool, error::Error> {
4848
}
4949

5050
#[allow(clippy::unnecessary_wraps)]
51-
pub(crate) fn get_parent_process_id() -> Option<u32> {
52-
#[allow(clippy::cast_sign_loss)]
53-
{
54-
Some(nix::unistd::getppid().as_raw() as u32)
55-
}
51+
pub(crate) fn get_parent_process_id() -> Option<sys::process::ProcessId> {
52+
Some(nix::unistd::getppid().as_raw())
5653
}
5754

5855
#[allow(clippy::unnecessary_wraps)]
59-
pub(crate) fn get_process_group_id() -> Option<u32> {
60-
#[allow(clippy::cast_sign_loss)]
61-
{
62-
Some(nix::unistd::getpgrp().as_raw() as u32)
63-
}
56+
pub(crate) fn get_process_group_id() -> Option<sys::process::ProcessId> {
57+
Some(nix::unistd::getpgrp().as_raw())
6458
}
6559

66-
pub(crate) fn get_foreground_pid() -> Option<u32> {
67-
#[allow(clippy::cast_sign_loss)]
60+
pub(crate) fn get_foreground_pid() -> Option<sys::process::ProcessId> {
6861
nix::unistd::tcgetpgrp(std::io::stdin())
6962
.ok()
70-
.map(|pgid| pgid.as_raw() as u32)
63+
.map(|pgid| pgid.as_raw())
7164
}
7265

73-
pub(crate) fn move_to_foreground(pid: u32) -> Result<(), error::Error> {
74-
#[allow(clippy::cast_possible_wrap)]
75-
nix::unistd::tcsetpgrp(std::io::stdin(), nix::unistd::Pid::from_raw(pid as i32))?;
66+
pub(crate) fn move_to_foreground(pid: sys::process::ProcessId) -> Result<(), error::Error> {
67+
nix::unistd::tcsetpgrp(std::io::stdin(), nix::unistd::Pid::from_raw(pid))?;
7668
Ok(())
7769
}
7870

brush-core/src/terminal.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::{error, sys};
33
/// Encapsulates the state of a controlled terminal.
44
#[allow(clippy::module_name_repetitions)]
55
pub struct TerminalControl {
6-
prev_fg_pid: Option<u32>,
6+
prev_fg_pid: Option<sys::process::ProcessId>,
77
}
88

99
impl TerminalControl {

brush-shell/tests/interactive_tests.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ fn run_in_bg_then_fg() -> anyhow::Result<()> {
8686
Ok(())
8787
}
8888

89-
#[ignore] // TODO: Fix this test!
9089
#[test]
9190
fn run_pipeline_interactively() -> anyhow::Result<()> {
9291
let mut session = start_shell_session()?;
@@ -99,7 +98,7 @@ fn run_pipeline_interactively() -> anyhow::Result<()> {
9998
.context("Echoed text didn't show up")?;
10099
session.send("h")?;
101100
session
102-
.expect("SUMMARY OF LESS COMMANDS")
101+
.expect("SUMMARY")
103102
.context("less help didn't show up")?;
104103
session.send("q")?;
105104
session.send("q")?;

0 commit comments

Comments
 (0)