diff --git a/Cargo.lock b/Cargo.lock index 1f634d5aea60..ab33d911d319 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -531,6 +531,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 = "cfg_aliases" version = "0.2.1" @@ -866,7 +872,7 @@ version = "3.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" dependencies = [ - "nix", + "nix 0.29.0", "windows-sys 0.59.0", ] @@ -980,6 +986,12 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dunce" version = "1.0.5" @@ -1101,6 +1113,17 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + [[package]] name = "filetime" version = "0.2.25" @@ -1522,7 +1545,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bdbbd5bc8c5749697ccaa352fa45aff8730cf21c68029c0eef1ffed7c3d6ba2" dependencies = [ "cfg-if", - "nix", + "nix 0.29.0", "widestring", "windows 0.57.0", ] @@ -2310,6 +2333,18 @@ dependencies = [ "rand", ] +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.8.0", + "cfg-if", + "cfg_aliases 0.1.1", + "libc", +] + [[package]] name = "nix" version = "0.29.0" @@ -2318,7 +2353,7 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.8.0", "cfg-if", - "cfg_aliases", + "cfg_aliases 0.2.1", "libc", ] @@ -2662,6 +2697,27 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "portable-pty" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "downcast-rs", + "filedescriptor", + "lazy_static", + "libc", + "log", + "nix 0.28.0", + "serial2", + "shared_library", + "shell-words", + "winapi", + "winreg 0.10.1", +] + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -2831,7 +2887,7 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904" dependencies = [ - "cfg_aliases", + "cfg_aliases 0.2.1", "libc", "once_cell", "socket2", @@ -3554,6 +3610,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serial2" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd0c773455b60177d1abe4c739cbfa316c4f2f0ef37465befcb72e8a15cdd02" +dependencies = [ + "cfg-if", + "libc", + "winapi", +] + [[package]] name = "sha2" version = "0.10.8" @@ -3574,12 +3641,28 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shared_library" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" +dependencies = [ + "lazy_static", + "libc", +] + [[package]] name = "shell-escape" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "shellexpand" version = "3.1.0" @@ -4549,9 +4632,10 @@ dependencies = [ "itertools 0.14.0", "jiff", "miette", - "nix", + "nix 0.29.0", "owo-colors", "petgraph", + "portable-pty", "predicates", "regex", "reqwest", @@ -4905,7 +4989,6 @@ name = "uv-console" version = "0.0.1" dependencies = [ "console", - "ctrlc", ] [[package]] @@ -5683,7 +5766,7 @@ dependencies = [ "tracing", "uv-fs", "uv-static", - "winreg", + "winreg 0.53.0", ] [[package]] @@ -6511,6 +6594,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "winreg" version = "0.53.0" diff --git a/crates/uv-console/Cargo.toml b/crates/uv-console/Cargo.toml index 597105ee9a40..fcfb81dd4a3c 100644 --- a/crates/uv-console/Cargo.toml +++ b/crates/uv-console/Cargo.toml @@ -11,5 +11,4 @@ doctest = false workspace = true [dependencies] -ctrlc = { workspace = true } console = { workspace = true } diff --git a/crates/uv-console/src/lib.rs b/crates/uv-console/src/lib.rs index 165edd1de193..5af8aeef9f85 100644 --- a/crates/uv-console/src/lib.rs +++ b/crates/uv-console/src/lib.rs @@ -6,29 +6,6 @@ use std::{cmp::Ordering, iter}; /// This is a slimmed-down version of `dialoguer::Confirm`, with the post-confirmation report /// enabled. pub fn confirm(message: &str, term: &Term, default: bool) -> std::io::Result { - // Set the Ctrl-C handler to exit the process. - let result = ctrlc::set_handler(move || { - let term = Term::stderr(); - term.show_cursor().ok(); - term.flush().ok(); - - #[allow(clippy::exit, clippy::cast_possible_wrap)] - std::process::exit(if cfg!(windows) { - 0xC000_013A_u32 as i32 - } else { - 130 - }); - }); - - match result { - Ok(()) => {} - Err(ctrlc::Error::MultipleHandlers) => { - // If multiple handlers were set, we assume that the existing handler is our - // confirmation handler, and continue. - } - Err(err) => return Err(std::io::Error::new(std::io::ErrorKind::Other, err)), - } - let prompt = format!( "{} {} {} {} {}", style("?".to_string()).for_stderr().yellow(), @@ -47,11 +24,23 @@ pub fn confirm(message: &str, term: &Term, default: bool) -> std::io::Result break true, Key::Char('n' | 'N') => break false, Key::Enter => break default, + Key::CtrlC => { + term.show_cursor()?; + term.write_str("\n")?; + term.flush()?; + + #[allow(clippy::exit, clippy::cast_possible_wrap)] + std::process::exit(if cfg!(windows) { + 0xC000_013A_u32 as i32 + } else { + 130 + }); + } _ => {} }; }; diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 61c66bc4f08e..3cf656b2f633 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -115,6 +115,7 @@ flate2 = { workspace = true, default-features = false } ignore = { version = "0.4.23" } indoc = { workspace = true } insta = { version = "1.40.0", features = ["filters", "json"] } +portable-pty = "0.9.0" predicates = { version = "3.1.2" } regex = { workspace = true } reqwest = { workspace = true, features = ["blocking"], default-features = false } diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 85b8dcc09394..282c0403c768 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -299,6 +299,16 @@ async fn run(mut cli: Cli) -> Result { anstream::ColorChoice::write_global(globals.color.into()); + (match globals.color { + uv_cli::ColorChoice::Auto => None, + uv_cli::ColorChoice::Always => Some(true), + uv_cli::ColorChoice::Never => Some(false), + }) + .inspect(|colors_enabled| { + console::set_colors_enabled(*colors_enabled); + console::set_colors_enabled_stderr(*colors_enabled); + }); + miette::set_hook(Box::new(|_| { Box::new( miette::MietteHandlerOpts::new() diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index f3a12c9a6ebd..a42391050faa 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -1,4 +1,4 @@ -use std::io::Cursor; +use std::io::{Cursor, Read}; use std::process::Command; use anyhow::Result; @@ -9097,3 +9097,68 @@ fn unsupported_git_scheme() { "### ); } + +/* This is a regression test for a bug where Ctrl-C'ing the installation +confirmation prompt would cause a crash due to the handling of Ctrl-C and the +resulting SIGINT. */ +#[test] +fn ctrl_c_install_confirmation_prompt() -> Result<()> { + const EXPECTED_PROMPT: &str = "? `pyproject.toml` looks like a local metadata file but was passed as a package name. Did you mean `-r pyproject.toml`? [y/n] › yes"; + + let pty_sys = portable_pty::native_pty_system(); + + let pair = pty_sys.openpty(portable_pty::PtySize::default())?; + + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.touch()?; + + let mut cmd = context.pip_install(); + cmd.arg("pyproject.toml"); + cmd.args(["--color", "never"]); // ANSI escapes make the expectations harder to read + + let mut argv = Vec::new(); + + argv.push(cmd.get_program().into()); + argv.extend(cmd.get_args().map(Into::into)); + + let mut cmdbuild = portable_pty::CommandBuilder::from_argv(argv); + + for (key, value) in cmd.get_envs() { + match value { + Some(value) => { + cmdbuild.env(key, value); + } + None => { + cmdbuild.env_remove(key); + } + } + } + + match cmd.get_current_dir() { + Some(cwd) => cmdbuild.cwd(cwd), + None => cmdbuild.clear_cwd(), + }; + + let mut child = pair.slave.spawn_command(cmdbuild)?; + let mut reader = pair.master.try_clone_reader()?; + let mut writer = pair.master.take_writer()?; + + let mut buffer = [0u8; EXPECTED_PROMPT.len()]; + reader.read_exact(&mut buffer)?; + let output = String::from_utf8_lossy(&buffer).to_string(); + + assert_eq!(output, EXPECTED_PROMPT); + + // 0x03 = ETX + writer.write_all(&[0x03])?; + writer.flush()?; + + let exit_status = child.wait()?; + + let expected_exit_code = if cfg!(windows) { 0xC000_013A_u32 } else { 130 }; + assert_eq!(exit_status.exit_code(), expected_exit_code); + + Ok(()) +}