From e8adac09d8796eaed23afb71f5127d8cd3b1cdfb Mon Sep 17 00:00:00 2001 From: warren2k <846021+warren2k@users.noreply.github.com> Date: Sat, 15 Jun 2024 10:31:31 -0400 Subject: [PATCH] feat: does not rely on a pty Co-authored-by: ruben beck Co-authored-by: Shahar "Dawn" Or --- Cargo.lock | 46 +++++++++---------- Cargo.toml | 9 +--- src/app/state.rs | 92 ++++++++++++++++++++++++------------- src/app/state/repl_state.rs | 20 ++++---- src/repl/driver.rs | 61 ++++++++++++------------ 5 files changed, 123 insertions(+), 105 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3becfba..621f958 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -218,6 +218,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "clap" version = "4.3.21" @@ -354,9 +360,9 @@ dependencies = [ "glob", "indoc", "itertools", + "nix", "predicates", "pretty_assertions", - "pty-process", "strip-ansi-escapes", "tokio", ] @@ -656,9 +662,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "line-wrap" @@ -719,6 +725,18 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.3.3", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -862,16 +880,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "pty-process" -version = "0.4.0" -source = "git+https://github.com/mobusoperandi/pty-process.git?rev=7889630#78896303a47c370e6cd5527267d8db696ffc62f1" -dependencies = [ - "libc", - "rustix 0.38.8", - "tokio", -] - [[package]] name = "quick-xml" version = "0.29.0" @@ -962,7 +970,6 @@ checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" dependencies = [ "bitflags 2.3.3", "errno", - "itoa", "libc", "linux-raw-sys 0.4.3", "windows-sys", @@ -1050,16 +1057,6 @@ dependencies = [ "deunicode", ] -[[package]] -name = "socket2" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "strip-ansi-escapes" version = "0.1.1" @@ -1210,7 +1207,6 @@ dependencies = [ "num_cpus", "pin-project-lite", "signal-hook-registry", - "socket2", "tokio-macros", "windows-sys", ] diff --git a/Cargo.toml b/Cargo.toml index 762f06c..30edb51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,14 +18,9 @@ futures = "0.3.28" glob = "0.3.1" indoc = "2.0.3" itertools = "0.11.0" +nix = "0.28.0" strip-ansi-escapes = "0.1.1" -tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread", "io-util"] } - -[dependencies.pty-process] -version = "0.4.0" -features = ["async"] -git = "https://github.com/mobusoperandi/pty-process.git" -rev = "7889630" +tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread", "io-util", "process", "fs"] } [dev-dependencies] assert_cmd = "2.0.12" diff --git a/src/app/state.rs b/src/app/state.rs index 7f9c2d5..66b1163 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -87,7 +87,7 @@ impl State { fn repl_event_spawn( &mut self, - spawn: Result, + spawn: Result, ) -> anyhow::Result> { let id = spawn?; @@ -128,48 +128,46 @@ impl State { let session_live = session_live.state.live_mut()?; let output = match &mut session_live.expecting { - ReplSessionExpecting::Nothing => anyhow::bail!("not expecting, got {:?}", ch), - ReplSessionExpecting::Prompt(acc) => { - acc.push(ch); - let string = String::from_utf8(strip_ansi_escapes::strip(acc)?)?; - - if string.ends_with("nix-repl> ") { - session_live.expecting = ReplSessionExpecting::Nothing; - self.next_query(&id)? - } else { - vec![] + ReplSessionExpecting::ClearlineBeforeInitialPrompt { cl_progress } => { + use ClearLineProgressStatus::*; + match cl_progress.clone().character(ch)? { + InProgress(progress) => { + *cl_progress = progress; + vec![] + } + ReachedEnd => self.next_query(&id)?, } } - ReplSessionExpecting::Echo { - acc, - last_query: expected, + ReplSessionExpecting::ClearLineBeforeResult { + cl_progress, expected_result, } => { - acc.push(ch); - if !acc.ends_with('\n') { - vec![] - } else if Self::sanitize(acc)? == expected.as_str() { - session_live.expecting = ReplSessionExpecting::ResultAndNextPrompt { - acc: String::new(), - expected_result: expected_result.clone(), - }; - vec![] - } else { - anyhow::bail!("actual: {acc:?}, expected: {expected:?}"); - } + use ClearLineProgressStatus::*; + match cl_progress.clone().character(ch)? { + InProgress(progress) => { + *cl_progress = progress; + } + ReachedEnd => { + session_live.expecting = + ReplSessionExpecting::ResultAndClearlineBeforeNextPrompt { + acc: String::new(), + expected_result: expected_result.clone(), + }; + } + }; + vec![] } - ReplSessionExpecting::ResultAndNextPrompt { + ReplSessionExpecting::ResultAndClearlineBeforeNextPrompt { acc, expected_result, } => 'arm: { acc.push(ch); - let sanitized = Self::sanitize(acc)?; - - let Some(result) = sanitized.strip_suffix("\nnix-repl> ") else { + let Some(result) = acc.strip_suffix(CLEAR_LINE) else { break 'arm vec![]; }; + let result = Self::sanitize(result)?; let result = result.trim_end_matches('\n'); if result != expected_result.as_str() { @@ -208,9 +206,8 @@ impl State { return self.session_end(id); }; - session_live.expecting = ReplSessionExpecting::Echo { - acc: String::new(), - last_query: entry.query.clone(), + session_live.expecting = ReplSessionExpecting::ClearLineBeforeResult { + cl_progress: ClearLineProgress::new(), expected_result: entry.expected_result, }; @@ -341,3 +338,32 @@ pub(crate) enum ExampleState { Repl(ReplExampleState), Expression(ExpressionExampleState), } + +const CLEAR_LINE: &str = "\r\u{1b}[K"; + +#[derive(Debug, Clone)] +pub struct ClearLineProgress(std::iter::Peekable>); + +impl ClearLineProgress { + fn character(mut self, ch: char) -> anyhow::Result { + let expected = self.0.next().unwrap(); + if ch != expected { + bail!("expected {expected:?}, got {ch:?}") + } + Ok(if self.0.peek().is_none() { + ClearLineProgressStatus::ReachedEnd + } else { + ClearLineProgressStatus::InProgress(self) + }) + } + + fn new() -> Self { + Self(CLEAR_LINE.chars().peekable()) + } +} + +#[derive(Debug, Clone)] +enum ClearLineProgressStatus { + InProgress(ClearLineProgress), + ReachedEnd, +} diff --git a/src/app/state/repl_state.rs b/src/app/state/repl_state.rs index f3ed2a1..654fa22 100644 --- a/src/app/state/repl_state.rs +++ b/src/app/state/repl_state.rs @@ -1,8 +1,10 @@ use crate::repl::{ - driver::{LFLine, ReplQuery}, + driver::LFLine, example::{ReplEntry, ReplExample, ReplExampleEntries}, }; +use super::ClearLineProgress; + #[derive(Debug)] pub(crate) struct ReplExampleState { pub(crate) example: ReplExample, @@ -44,14 +46,14 @@ pub(crate) struct ReplSessionLive { #[derive(Debug)] pub(crate) enum ReplSessionExpecting { - Nothing, - Prompt(String), - Echo { - acc: String, - last_query: ReplQuery, + ClearlineBeforeInitialPrompt { + cl_progress: ClearLineProgress, + }, + ClearLineBeforeResult { + cl_progress: ClearLineProgress, expected_result: ExpectedResult, }, - ResultAndNextPrompt { + ResultAndClearlineBeforeNextPrompt { acc: String, expected_result: ExpectedResult, }, @@ -61,7 +63,9 @@ impl ReplSessionLive { pub(crate) fn new(entries: ReplExampleEntries) -> Self { Self { iterator: entries.into_iter(), - expecting: ReplSessionExpecting::Prompt(String::new()), + expecting: ReplSessionExpecting::ClearlineBeforeInitialPrompt { + cl_progress: ClearLineProgress::new(), + }, } } } diff --git a/src/repl/driver.rs b/src/repl/driver.rs index 55b126a..951a14c 100644 --- a/src/repl/driver.rs +++ b/src/repl/driver.rs @@ -1,3 +1,8 @@ +use std::{ + os::fd::{FromRawFd, IntoRawFd}, + process::Stdio, +}; + use futures::{FutureExt, SinkExt, StreamExt}; use itertools::Itertools; use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -61,7 +66,7 @@ pub(crate) enum ReplCommand { #[derive(Debug)] pub(crate) enum ReplEvent { - Spawn(pty_process::Result), + Spawn(std::io::Result), Query(ExampleId, ReplQuery, anyhow::Result<()>), Kill(anyhow::Result), Read(ExampleId, u8), @@ -69,7 +74,7 @@ pub(crate) enum ReplEvent { } pub(crate) struct ReplDriver { - sessions: std::collections::BTreeMap, + sessions: std::collections::BTreeMap, sender: futures::channel::mpsc::UnboundedSender, } @@ -94,8 +99,8 @@ impl ReplDriver { self.command(command).await; } - for (id, (pty, _child)) in self.sessions.iter_mut() { - let byte = futures::poll!(std::pin::pin!(pty.read_u8())); + for (id, (_child, child_output)) in self.sessions.iter_mut() { + let byte = futures::poll!(std::pin::pin!(child_output.read_u8())); let std::task::Poll::Ready(byte) = byte else { continue; }; @@ -128,31 +133,16 @@ impl ReplDriver { } async fn spawn(&mut self, id: ExampleId) { - let pty = match pty_process::Pty::new() { - Ok(pty) => pty, - Err(error) => { - self.sender - .send(ReplEvent::Spawn(Err(error))) - .await - .unwrap(); - return; - } - }; + let (read_output, write_output) = nix::unistd::pipe().unwrap(); - let pts = match pty.pts() { - Ok(pts) => pts, - Err(error) => { - self.sender - .send(ReplEvent::Spawn(Err(error))) - .await - .unwrap(); - return; - } - }; - - let child = pty_process::Command::new(env!("NIX_CMD_PATH")) - .args(["repl", "--quiet"]) - .spawn(&pts); + let child = tokio::process::Command::new(env!("NIX_CMD_PATH")) + // even though a single `--quiet` would normally disable the pre-prompt message + // (at the time of writing `Nix 2.21.1`), two seem to be necessary here. + .args(["repl", "--quiet", "--quiet"]) + .stdin(Stdio::piped()) + .stdout(write_output.try_clone().unwrap()) + .stderr(write_output) + .spawn(); let child = match child { Err(error) => { @@ -165,13 +155,14 @@ impl ReplDriver { Ok(child) => child, }; - self.sessions.insert(id.clone(), (pty, child)); + let read_output = unsafe { tokio::fs::File::from_raw_fd(read_output.into_raw_fd()) }; + self.sessions.insert(id.clone(), (child, read_output)); self.sender.send(ReplEvent::Spawn(Ok(id))).await.unwrap(); } async fn query(&mut self, id: ExampleId, query: ReplQuery) { - let (pty, _child) = match self.sessions.get_mut(&id) { - Some(pty) => pty, + let child = match self.sessions.get_mut(&id) { + Some((child, _file)) => child, None => { let error = anyhow::anyhow!("no pty for {id:?}"); self.sender @@ -182,7 +173,13 @@ impl ReplDriver { } }; - let write = pty.write_all(query.as_bytes()).await; + let write = child + .stdin + .as_mut() + .unwrap() + .write_all(query.as_bytes()) + .await; + if let Err(error) = write { let error = anyhow::anyhow!("failed to query {error}"); self.sender