Skip to content

Commit 7db44a8

Browse files
authored
fix(core): do not set tasks which cannot be interactive to interactive (nrwl#31240)
<!-- Please make sure you have read the submission guidelines before posting an PR --> <!-- https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr --> <!-- Please make sure that your commit message follows our format --> <!-- Example: `fix(nx): must begin with lowercase` --> <!-- If this is a particularly complex change or feature addition, you can request a dedicated Nx release for this pull request branch. Mention someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they will confirm if the PR warrants its own release for testing purposes, and generate it for you if appropriate. --> ## Current Behavior <!-- This is the behavior we have today --> Some tasks cannot be interactive because of the way they are implemented. At the moment, those tasks can be set to interactive mode. ## Expected Behavior <!-- This is the behavior we should expect with the changes in this PR --> Tasks which cannot be interactive are not set as interactive ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #
1 parent 8ff47e0 commit 7db44a8

File tree

9 files changed

+79
-64
lines changed

9 files changed

+79
-64
lines changed

packages/nx/src/native/pseudo_terminal/child_process.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ use napi::{
99
ErrorStrategy::Fatal, ThreadsafeFunction, ThreadsafeFunctionCallMode::NonBlocking,
1010
},
1111
};
12+
use parking_lot::Mutex;
1213
use std::io::Write;
13-
use std::sync::{Arc, Mutex, RwLock};
14+
use std::sync::{Arc, RwLock};
1415
use tracing::warn;
1516
use vt100_ctt::Parser;
1617

packages/nx/src/native/pseudo_terminal/command/unix.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,9 @@ pub fn write_to_pty(stdin: &mut Stdin, writer: WriterArc) -> anyhow::Result<()>
4646
mio::Token(0) => {
4747
// Read data from stdin
4848
loop {
49-
let mut writer = writer.lock().expect("Failed to lock writer");
50-
5149
match stdin.read(&mut buffer) {
5250
Ok(n) => {
51+
let mut writer = writer.lock();
5352
writer.write_all(&buffer[..n])?;
5453
writer.flush()?;
5554
}

packages/nx/src/native/pseudo_terminal/command/windows.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ pub fn handle_path_space(path: String) -> String {
2626
}
2727

2828
pub fn write_to_pty(stdin: &mut Stdin, writer: WriterArc) -> anyhow::Result<()> {
29-
let mut writer = writer.lock().expect("Failed to lock writer");
29+
let mut writer = writer.lock();
3030
std::io::copy(stdin, writer.as_mut())
3131
.map_err(|e| anyhow::anyhow!(e))
3232
.map(|_| ())

packages/nx/src/native/pseudo_terminal/process_killer/unix.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use nix::{
22
sys::signal::{Signal as NixSignal, kill},
33
unistd::Pid,
44
};
5+
use tracing::debug;
56

67
pub struct ProcessKiller {
78
pid: i32,
@@ -13,12 +14,12 @@ impl ProcessKiller {
1314
}
1415

1516
pub fn kill(&self, signal: Option<&str>) -> anyhow::Result<()> {
17+
let signal = signal.unwrap_or("SIGINT");
18+
debug!("Killing process {} with {}", &self.pid, signal);
1619
let pid = Pid::from_raw(self.pid);
1720
match kill(
1821
pid,
19-
NixSignal::from(
20-
Signal::try_from(signal.unwrap_or("SIGINT")).map_err(|e| anyhow::anyhow!(e))?,
21-
),
22+
NixSignal::from(Signal::try_from(signal).map_err(|e| anyhow::anyhow!(e))?),
2223
) {
2324
Ok(_) => Ok(()),
2425
Err(e) => Err(anyhow::anyhow!("Failed to kill process: {}", e)),

packages/nx/src/native/pseudo_terminal/pseudo_terminal.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ use crossterm::{
66
tty::IsTty,
77
};
88
use napi::bindgen_prelude::*;
9+
use parking_lot::Mutex;
910
use portable_pty::{CommandBuilder, NativePtySystem, PtyPair, PtySize, PtySystem};
1011
use std::io::stdout;
11-
use std::sync::{Mutex, RwLock};
12+
use std::sync::RwLock;
1213
use std::{
1314
collections::HashMap,
1415
io::{Read, Write},

packages/nx/src/native/tui/app.rs

Lines changed: 34 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,12 @@ use ratatui::style::Style;
99
use ratatui::text::{Line, Span};
1010
use ratatui::widgets::{Block, Borders, Paragraph};
1111
use std::collections::HashMap;
12-
use std::io::{self, Write};
13-
use std::sync::{Arc, Mutex, RwLock};
12+
use std::io;
13+
use std::sync::{Arc, Mutex};
1414
use tokio::sync::mpsc;
1515
use tokio::sync::mpsc::UnboundedSender;
1616
use tracing::{debug, trace};
1717
use tui_logger::{LevelFilter, TuiLoggerSmartWidget, TuiWidgetEvent, TuiWidgetState};
18-
use vt100_ctt::Parser;
1918

2019
use crate::native::tui::tui::Tui;
2120
use crate::native::{
@@ -174,22 +173,30 @@ impl App {
174173

175174
pub fn update_task_status(&mut self, task_id: String, status: TaskStatus) {
176175
self.dispatch_action(Action::UpdateTaskStatus(task_id.clone(), status));
177-
if status == TaskStatus::InProgress && self.tasks.len() == 1 {
176+
if status == TaskStatus::InProgress && self.should_set_interactive_by_default(&task_id) {
178177
self.terminal_pane_data[0].set_interactive(true);
179178
}
180179
}
181180

181+
fn should_set_interactive_by_default(&self, task_id: &str) -> bool {
182+
self.tasks.len() == 1
183+
&& self
184+
.pty_instances
185+
.get(task_id)
186+
.is_some_and(|pty| pty.can_be_interactive())
187+
}
188+
182189
pub fn print_task_terminal_output(&mut self, task_id: String, output: String) {
183190
// Tasks run within a pseudo-terminal always have a pty instance and do not need a new one
184191
// Tasks not run within a pseudo-terminal need a new pty instance to print output
185192
if !self.pty_instances.contains_key(&task_id) {
186-
let (parser, parser_and_writer) = Self::create_empty_parser_and_noop_writer();
193+
let pty = PtyInstance::non_interactive();
187194

188195
// Add ANSI escape sequence to hide cursor at the end of output, it would be confusing to have it visible when a task is a cache hit
189196
let output_with_hidden_cursor = format!("{}\x1b[?25l", output);
190-
Self::write_output_to_parser(parser, output_with_hidden_cursor);
197+
Self::write_output_to_parser(&pty, output_with_hidden_cursor);
191198

192-
self.create_and_register_pty_instance(&task_id, parser_and_writer);
199+
self.register_pty_instance(&task_id, pty);
193200
// Ensure the pty instances get resized appropriately
194201
let _ = self.debounce_pty_resize();
195202
return;
@@ -255,23 +262,31 @@ impl App {
255262
}
256263

257264
// A pseudo-terminal running task will provide the parser and writer directly
258-
pub fn register_running_task(
265+
pub fn register_running_interactive_task(
259266
&mut self,
260267
task_id: String,
261268
parser_and_writer: External<(ParserArc, WriterArc)>,
262269
) {
263-
self.create_and_register_pty_instance(&task_id, parser_and_writer);
264-
self.update_task_status(task_id.clone(), TaskStatus::InProgress);
270+
debug!("Registering interactive task: {task_id}");
271+
let pty =
272+
PtyInstance::interactive(parser_and_writer.0.clone(), parser_and_writer.1.clone());
273+
self.register_running_task(task_id, pty)
265274
}
266275

267-
pub fn register_running_task_with_empty_parser(&mut self, task_id: String) {
268-
let (_, parser_and_writer) = Self::create_empty_parser_and_noop_writer();
269-
self.create_and_register_pty_instance(&task_id, parser_and_writer);
276+
pub fn register_running_non_interactive_task(&mut self, task_id: String) {
277+
debug!("Registering non-interactive task: {task_id}");
278+
let pty = PtyInstance::non_interactive();
279+
self.register_pty_instance(&task_id, pty);
270280
self.update_task_status(task_id.clone(), TaskStatus::InProgress);
271281
// Ensure the pty instances get resized appropriately
272282
let _ = self.debounce_pty_resize();
273283
}
274284

285+
fn register_running_task(&mut self, task_id: String, pty: PtyInstance) {
286+
self.register_pty_instance(&task_id, pty);
287+
self.update_task_status(task_id.clone(), TaskStatus::InProgress);
288+
}
289+
275290
pub fn append_task_output(&mut self, task_id: String, output: String) {
276291
let pty = self
277292
.pty_instances
@@ -973,13 +988,14 @@ impl App {
973988
let terminal_pane_data = &mut self.terminal_pane_data[pane_idx];
974989
terminal_pane_data.is_continuous = task.continuous;
975990
let in_progress = task.status == TaskStatus::InProgress;
976-
terminal_pane_data.can_be_interactive = in_progress;
977991
if !in_progress {
978992
terminal_pane_data.set_interactive(false);
979993
}
980994

981995
let mut has_pty = false;
982996
if let Some(pty) = self.pty_instances.get(&relevant_pane_task) {
997+
terminal_pane_data.can_be_interactive =
998+
in_progress && pty.can_be_interactive();
983999
terminal_pane_data.pty = Some(pty.clone());
9841000
has_pty = true;
9851001
}
@@ -1487,41 +1503,17 @@ impl App {
14871503
self.layout_manager.get_task_list_visibility() == TaskListVisibility::Hidden
14881504
}
14891505

1490-
fn create_and_register_pty_instance(
1491-
&mut self,
1492-
task_id: &str,
1493-
parser_and_writer: External<(ParserArc, WriterArc)>,
1494-
) {
1506+
fn register_pty_instance(&mut self, task_id: &str, pty: PtyInstance) {
14951507
// Access the contents of the External
1496-
let pty = Arc::new(
1497-
PtyInstance::new(
1498-
task_id.to_string(),
1499-
parser_and_writer.0.clone(),
1500-
parser_and_writer.1.clone(),
1501-
)
1502-
.map_err(|e| napi::Error::from_reason(format!("Failed to create PTY: {}", e)))
1503-
.unwrap(),
1504-
);
1508+
let pty = Arc::new(pty);
15051509

15061510
self.pty_instances.insert(task_id.to_string(), pty);
15071511
}
15081512

1509-
fn create_empty_parser_and_noop_writer() -> (ParserArc, External<(ParserArc, WriterArc)>) {
1510-
// Use sane defaults for rows, cols and scrollback buffer size. The dimensions will be adjusted dynamically later.
1511-
let parser = Arc::new(RwLock::new(Parser::new(24, 80, 10000)));
1512-
let writer: Arc<Mutex<Box<dyn Write + Send>>> =
1513-
Arc::new(Mutex::new(Box::new(std::io::sink())));
1514-
(parser.clone(), External::new((parser, writer)))
1515-
}
1516-
15171513
// Writes the given output to the given parser, used for the case where a task is a cache hit, or when it is run outside of the rust pseudo-terminal
1518-
fn write_output_to_parser(parser: ParserArc, output: String) {
1514+
fn write_output_to_parser(parser: &PtyInstance, output: String) {
15191515
let normalized_output = normalize_newlines(output.as_bytes());
1520-
parser
1521-
.write()
1522-
.unwrap()
1523-
.write_all(&normalized_output)
1524-
.unwrap();
1516+
parser.process_output(&normalized_output);
15251517
}
15261518

15271519
fn display_and_focus_current_task_in_terminal_pane(&mut self, force_spacebar_mode: bool) {

packages/nx/src/native/tui/components/terminal_pane.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use ratatui::{
1111
},
1212
};
1313
use std::{io, sync::Arc};
14+
use tracing::debug;
1415
use tui_term::widget::PseudoTerminal;
1516

1617
use crate::native::tui::components::tasks_list::TaskStatus;
@@ -100,6 +101,7 @@ impl TerminalPaneData {
100101
_ if self.is_interactive => match key.code {
101102
KeyCode::Char(c) if key.modifiers.contains(KeyModifiers::CONTROL) => {
102103
let ascii_code = (c as u8) - 0x60;
104+
debug!("Sending ASCII code: {}", &[ascii_code].escape_ascii());
103105
pty_mut.write_input(&[ascii_code])?;
104106
}
105107
KeyCode::Char(c) => {

packages/nx/src/native/tui/lifecycle.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,14 +256,14 @@ impl AppLifeCycle {
256256
) {
257257
self.app
258258
.lock()
259-
.register_running_task(task_id, parser_and_writer)
259+
.register_running_interactive_task(task_id, parser_and_writer)
260260
}
261261

262262
#[napi]
263263
pub fn register_running_task_with_empty_parser(&mut self, task_id: String) {
264264
self.app
265265
.lock()
266-
.register_running_task_with_empty_parser(task_id)
266+
.register_running_non_interactive_task(task_id)
267267
}
268268

269269
#[napi]

packages/nx/src/native/tui/pty.rs

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use super::utils::normalize_newlines;
22
use crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind};
3+
use parking_lot::Mutex;
34
use std::{
45
io::{self, Write},
5-
sync::{Arc, Mutex, RwLock},
6+
sync::{Arc, RwLock},
67
};
78
use tracing::debug;
89
use vt100_ctt::Parser;
@@ -29,28 +30,42 @@ impl std::ops::Deref for PtyScreenRef<'_> {
2930

3031
#[derive(Clone)]
3132
pub struct PtyInstance {
32-
pub task_id: String,
33-
pub parser: Arc<RwLock<Parser>>,
34-
pub writer: Arc<Mutex<Box<dyn Write + Send>>>,
33+
parser: Arc<RwLock<Parser>>,
34+
writer: Option<Arc<Mutex<Box<dyn Write + Send>>>>,
3535
rows: u16,
3636
cols: u16,
3737
}
3838

3939
impl PtyInstance {
40-
pub fn new(
41-
task_id: String,
40+
pub fn interactive(
4241
parser: Arc<RwLock<Parser>>,
4342
writer: Arc<Mutex<Box<dyn Write + Send>>>,
44-
) -> io::Result<Self> {
43+
) -> Self {
4544
// Read the dimensions from the parser
4645
let (rows, cols) = parser.read().unwrap().screen().size();
47-
Ok(Self {
48-
task_id,
46+
Self {
4947
parser,
50-
writer,
48+
writer: Some(writer),
5149
rows,
5250
cols,
53-
})
51+
}
52+
}
53+
54+
pub fn non_interactive() -> Self {
55+
// Use sane defaults for rows, cols and scrollback buffer size. The dimensions will be adjusted dynamically later.
56+
let rows = 24;
57+
let cols = 80;
58+
let parser = Arc::new(RwLock::new(Parser::new(rows, cols, 10000)));
59+
Self {
60+
parser,
61+
writer: None,
62+
rows,
63+
cols,
64+
}
65+
}
66+
67+
pub fn can_be_interactive(&self) -> bool {
68+
self.writer.is_some()
5469
}
5570

5671
pub fn resize(&mut self, rows: u16, cols: u16) -> io::Result<()> {
@@ -86,9 +101,13 @@ impl PtyInstance {
86101
}
87102

88103
pub fn write_input(&mut self, input: &[u8]) -> io::Result<()> {
89-
if let Ok(mut writer_guard) = self.writer.lock() {
104+
if let Some(writer) = &self.writer {
105+
let mut writer_guard = writer.lock();
106+
debug!("Writing input: {:?}", input);
90107
writer_guard.write_all(input)?;
91108
writer_guard.flush()?;
109+
} else {
110+
debug!("Swallowing input: {:?}", input);
92111
}
93112

94113
Ok(())

0 commit comments

Comments
 (0)