From ce00640b22bdfbc1cb90af8ef1efa610b295c44c Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" <69878+youknowone@users.noreply.github.com> Date: Sun, 28 Dec 2025 09:08:03 +0900 Subject: [PATCH] fix input to check pty child (#6553) * fix input to check pty child * Auto-format: cargo fmt --all --------- Co-authored-by: github-actions[bot] --- crates/vm/src/py_io.rs | 9 ++++++--- crates/vm/src/stdlib/builtins.rs | 28 ++++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/crates/vm/src/py_io.rs b/crates/vm/src/py_io.rs index 6b82bbd47..a8063673a 100644 --- a/crates/vm/src/py_io.rs +++ b/crates/vm/src/py_io.rs @@ -70,11 +70,14 @@ pub fn file_readline(obj: &PyObject, size: Option, vm: &VirtualMachine) - }; let ret = match_class!(match ret { s @ PyStr => { - let s_val = s.as_str(); - if s_val.is_empty() { + // Use as_wtf8() to handle strings with surrogates (e.g., surrogateescape) + let s_wtf8 = s.as_wtf8(); + if s_wtf8.is_empty() { return Err(eof_err()); } - if let Some(no_nl) = s_val.strip_suffix('\n') { + // '\n' is ASCII, so we can check bytes directly + if s_wtf8.as_bytes().last() == Some(&b'\n') { + let no_nl = &s_wtf8[..s_wtf8.len() - 1]; vm.ctx.new_str(no_nl).into() } else { s.into() diff --git a/crates/vm/src/stdlib/builtins.rs b/crates/vm/src/stdlib/builtins.rs index 19f85af55..7cd91f8b4 100644 --- a/crates/vm/src/stdlib/builtins.rs +++ b/crates/vm/src/stdlib/builtins.rs @@ -7,8 +7,6 @@ pub use builtins::{ascii, print, reversed}; #[pymodule] mod builtins { - use std::io::IsTerminal; - use crate::{ AsObject, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, VirtualMachine, builtins::{ @@ -464,6 +462,8 @@ mod builtins { #[pyfunction] fn input(prompt: OptionalArg, vm: &VirtualMachine) -> PyResult { + use std::io::IsTerminal; + let stdin = sys::get_stdin(vm)?; let stdout = sys::get_stdout(vm)?; let stderr = sys::get_stderr(vm)?; @@ -476,8 +476,13 @@ mod builtins { .is_ok_and(|fd| fd == expected) }; - // everything is normal, we can just rely on rustyline to use stdin/stdout - if fd_matches(&stdin, 0) && fd_matches(&stdout, 1) && std::io::stdin().is_terminal() { + // Check if we should use rustyline (interactive terminal, not PTY child) + let use_rustyline = fd_matches(&stdin, 0) + && fd_matches(&stdout, 1) + && std::io::stdin().is_terminal() + && !is_pty_child(); + + if use_rustyline { let prompt = prompt.as_ref().map_or("", |s| s.as_str()); let mut readline = Readline::new(()); match readline.readline(prompt) { @@ -502,6 +507,21 @@ mod builtins { } } + /// Check if we're running in a PTY child process (e.g., after pty.fork()). + /// pty.fork() calls setsid(), making the child a session leader. + /// In this case, rustyline may hang because it uses raw mode. + #[cfg(unix)] + fn is_pty_child() -> bool { + use nix::unistd::{getpid, getsid}; + // If this process is a session leader, we're likely in a PTY child + getsid(None) == Ok(getpid()) + } + + #[cfg(not(unix))] + fn is_pty_child() -> bool { + false + } + #[pyfunction] fn isinstance(obj: PyObjectRef, typ: PyObjectRef, vm: &VirtualMachine) -> PyResult { obj.is_instance(&typ, vm)