From d09e9b814c551bc0fdf405729158bd70286fd4f3 Mon Sep 17 00:00:00 2001 From: Sumi Jeong <125195487+sigmaith@users.noreply.github.com> Date: Wed, 22 Jul 2026 21:53:45 +0900 Subject: [PATCH 1/6] Implement termios.tcgetwinsize/tcsetwinsize Adds ioctl(TIOCGWINSZ/TIOCSWINSZ) wrappers in host_env and the corresponding Python-facing functions in the termios stdlib module, removing the associated expectedFailure markers in test_termios.py. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01RbzcYLnX5tMfTM9BP7FDTT --- Lib/test/test_termios.py | 4 ---- crates/host_env/src/termios.rs | 24 ++++++++++++++++++++++++ crates/stdlib/src/termios.rs | 27 +++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_termios.py b/Lib/test/test_termios.py index 216609719ac..1207855fa2d 100644 --- a/Lib/test/test_termios.py +++ b/Lib/test/test_termios.py @@ -221,7 +221,6 @@ def writer(): 'output was not resumed') self.assertEqual(os.read(rfd, 1024), b'def') - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module 'termios' has no attribute 'tcgetwinsize' def test_tcgetwinsize(self): size = termios.tcgetwinsize(self.fd) self.assertIsInstance(size, tuple) @@ -230,7 +229,6 @@ def test_tcgetwinsize(self): self.assertIsInstance(size[1], int) self.assertEqual(termios.tcgetwinsize(self.stream), size) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module 'termios' has no attribute 'tcgetwinsize' def test_tcgetwinsize_errors(self): self.assertRaisesTermiosError(errno.ENOTTY, termios.tcgetwinsize, self.bad_fd) self.assertRaises(ValueError, termios.tcgetwinsize, -1) @@ -238,14 +236,12 @@ def test_tcgetwinsize_errors(self): self.assertRaises(TypeError, termios.tcgetwinsize, object()) self.assertRaises(TypeError, termios.tcgetwinsize) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module 'termios' has no attribute 'tcgetwinsize' def test_tcsetwinsize(self): size = termios.tcgetwinsize(self.fd) termios.tcsetwinsize(self.fd, size) termios.tcsetwinsize(self.fd, list(size)) termios.tcsetwinsize(self.stream, size) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module 'termios' has no attribute 'tcgetwinsize' def test_tcsetwinsize_errors(self): size = termios.tcgetwinsize(self.fd) self.assertRaises(TypeError, termios.tcsetwinsize, self.fd, size[:-1]) diff --git a/crates/host_env/src/termios.rs b/crates/host_env/src/termios.rs index 0a96078be94..138a98b88df 100644 --- a/crates/host_env/src/termios.rs +++ b/crates/host_env/src/termios.rs @@ -165,3 +165,27 @@ pub fn tcflush(fd: i32, queue: i32) -> std::io::Result<()> { pub fn tcflow(fd: i32, action: i32) -> std::io::Result<()> { ::termios::tcflow(fd, action) } + +pub fn tcgetwinsize(fd: i32) -> std::io::Result<(u16, u16)> { + let mut size: libc::winsize = unsafe { std::mem::zeroed() }; + let ret = unsafe { libc::ioctl(fd, TIOCGWINSZ as _, &mut size) }; + if ret != 0 { + return Err(std::io::Error::last_os_error()); + } + Ok((size.ws_row, size.ws_col)) +} + +pub fn tcsetwinsize(fd: i32, row: u16, col: u16) -> std::io::Result<()> { + let mut size: libc::winsize = unsafe { std::mem::zeroed() }; + let ret = unsafe { libc::ioctl(fd, TIOCGWINSZ as _, &mut size) }; + if ret != 0 { + return Err(std::io::Error::last_os_error()); + } + size.ws_row = row; + size.ws_col = col; + let ret = unsafe { libc::ioctl(fd, TIOCSWINSZ as _, &size) }; + if ret != 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) +} diff --git a/crates/stdlib/src/termios.rs b/crates/stdlib/src/termios.rs index 2207276f81e..b2bd426f768 100644 --- a/crates/stdlib/src/termios.rs +++ b/crates/stdlib/src/termios.rs @@ -268,6 +268,33 @@ mod termios { Ok(()) } + #[pyfunction] + fn tcgetwinsize(fd: PyObjectRef, vm: &VirtualMachine) -> PyResult<(u16, u16)> { + let fd = Fildes::try_from_object(vm, fd).map(Into::into)?; + let size = host_termios::tcgetwinsize(fd).map_err(|e| termios_error(e, vm))?; + Ok(size) + } + + #[pyfunction] + fn tcsetwinsize(fd: PyObjectRef, size: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + let fd = Fildes::try_from_object(vm, fd).map(Into::into)?; + let size = vm.extract_elements_with(&size, Ok)?; + let [row, col] = <[PyObjectRef; 2]>::try_from(size) + .map_err(|_| vm.new_type_error("tcsetwinsize: size must be 2 element tuple/list"))?; + + let row: u16 = row + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("tcsetwinsize: winsize values must be integers"))? + .try_to_primitive(vm)?; + let col: u16 = col + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("tcsetwinsize: winsize values must be integers"))? + .try_to_primitive(vm)?; + + host_termios::tcsetwinsize(fd, row, col).map_err(|e| termios_error(e, vm))?; + Ok(()) + } + fn termios_error(err: std::io::Error, vm: &VirtualMachine) -> PyBaseExceptionRef { vm.new_os_subtype_error( error_type(vm), From 0614732c06b97a8f0d913306e97cb519ffd7af16 Mon Sep 17 00:00:00 2001 From: Sumi Jeong <125195487+sigmaith@users.noreply.github.com> Date: Wed, 22 Jul 2026 22:18:52 +0900 Subject: [PATCH 2/6] Use ret < 0 convention for ioctl error checks in termios winsize POSIX ioctl only guarantees -1 on failure, not exactly 0 on success; matches the existing check_libc_neg convention used elsewhere in host_env (e.g. fcntl.rs, posix.rs::get_terminal_size). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01RbzcYLnX5tMfTM9BP7FDTT --- crates/host_env/src/termios.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/host_env/src/termios.rs b/crates/host_env/src/termios.rs index 138a98b88df..2b2dae0ee50 100644 --- a/crates/host_env/src/termios.rs +++ b/crates/host_env/src/termios.rs @@ -169,7 +169,7 @@ pub fn tcflow(fd: i32, action: i32) -> std::io::Result<()> { pub fn tcgetwinsize(fd: i32) -> std::io::Result<(u16, u16)> { let mut size: libc::winsize = unsafe { std::mem::zeroed() }; let ret = unsafe { libc::ioctl(fd, TIOCGWINSZ as _, &mut size) }; - if ret != 0 { + if ret < 0 { return Err(std::io::Error::last_os_error()); } Ok((size.ws_row, size.ws_col)) @@ -178,13 +178,13 @@ pub fn tcgetwinsize(fd: i32) -> std::io::Result<(u16, u16)> { pub fn tcsetwinsize(fd: i32, row: u16, col: u16) -> std::io::Result<()> { let mut size: libc::winsize = unsafe { std::mem::zeroed() }; let ret = unsafe { libc::ioctl(fd, TIOCGWINSZ as _, &mut size) }; - if ret != 0 { + if ret < 0 { return Err(std::io::Error::last_os_error()); } size.ws_row = row; size.ws_col = col; let ret = unsafe { libc::ioctl(fd, TIOCSWINSZ as _, &size) }; - if ret != 0 { + if ret < 0 { return Err(std::io::Error::last_os_error()); } Ok(()) From 2c555e55af5b27649dd9dac6d71224e09524f710 Mon Sep 17 00:00:00 2001 From: Sumi Jeong <125195487+sigmaith@users.noreply.github.com> Date: Wed, 22 Jul 2026 22:26:31 +0900 Subject: [PATCH 3/6] Reject non-sequence size in tcsetwinsize extract_elements_with also accepted dicts (treating keys as elements). Switch to try_sequence(), matching CPython's PySequence_Check, so only real sequences (tuple/list/etc.) are accepted. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01RbzcYLnX5tMfTM9BP7FDTT --- crates/stdlib/src/termios.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/stdlib/src/termios.rs b/crates/stdlib/src/termios.rs index b2bd426f768..5ec4ff77470 100644 --- a/crates/stdlib/src/termios.rs +++ b/crates/stdlib/src/termios.rs @@ -278,9 +278,12 @@ mod termios { #[pyfunction] fn tcsetwinsize(fd: PyObjectRef, size: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { let fd = Fildes::try_from_object(vm, fd).map(Into::into)?; - let size = vm.extract_elements_with(&size, Ok)?; - let [row, col] = <[PyObjectRef; 2]>::try_from(size) - .map_err(|_| vm.new_type_error("tcsetwinsize: size must be 2 element tuple/list"))?; + let seq = size.try_sequence(vm)?; + if seq.length(vm)? != 2 { + return Err(vm.new_type_error("tcsetwinsize: size must be a 2 element sequence")); + } + let row = seq.get_item(0, vm)?; + let col = seq.get_item(1, vm)?; let row: u16 = row .downcast_ref::() From 414a86944d7afadf2e887086469badeb184400de Mon Sep 17 00:00:00 2001 From: Sumi Jeong <125195487+sigmaith@users.noreply.github.com> Date: Wed, 22 Jul 2026 23:14:16 +0900 Subject: [PATCH 4/6] Use `core::mem::zeroed` instead of `std::mem::zeroed` `std::mem` just re-exports `core::mem`, but clippy prefers importing from core when there's no OS dependency. This was breaking the wasm CI build. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01RbzcYLnX5tMfTM9BP7FDTT --- crates/host_env/src/termios.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/host_env/src/termios.rs b/crates/host_env/src/termios.rs index 2b2dae0ee50..612f68be924 100644 --- a/crates/host_env/src/termios.rs +++ b/crates/host_env/src/termios.rs @@ -167,7 +167,7 @@ pub fn tcflow(fd: i32, action: i32) -> std::io::Result<()> { } pub fn tcgetwinsize(fd: i32) -> std::io::Result<(u16, u16)> { - let mut size: libc::winsize = unsafe { std::mem::zeroed() }; + let mut size: libc::winsize = unsafe { core::mem::zeroed() }; let ret = unsafe { libc::ioctl(fd, TIOCGWINSZ as _, &mut size) }; if ret < 0 { return Err(std::io::Error::last_os_error()); @@ -176,7 +176,7 @@ pub fn tcgetwinsize(fd: i32) -> std::io::Result<(u16, u16)> { } pub fn tcsetwinsize(fd: i32, row: u16, col: u16) -> std::io::Result<()> { - let mut size: libc::winsize = unsafe { std::mem::zeroed() }; + let mut size: libc::winsize = unsafe { core::mem::zeroed() }; let ret = unsafe { libc::ioctl(fd, TIOCGWINSZ as _, &mut size) }; if ret < 0 { return Err(std::io::Error::last_os_error()); From 8af3f225a26bd32e2c1ee264e479809b1c7ee54e Mon Sep 17 00:00:00 2001 From: Sumi Jeong <125195487+sigmaith@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:16:33 +0900 Subject: [PATCH 5/6] Use try_index for tcsetwinsize row/col conversion CPython's PyLong_AsLong calls __index__ on non-int objects before converting, so downcast_ref:: was stricter than CPython. try_index matches that behavior and is more concise. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01RbzcYLnX5tMfTM9BP7FDTT --- crates/stdlib/src/termios.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/stdlib/src/termios.rs b/crates/stdlib/src/termios.rs index 5ec4ff77470..6fde771a987 100644 --- a/crates/stdlib/src/termios.rs +++ b/crates/stdlib/src/termios.rs @@ -285,14 +285,8 @@ mod termios { let row = seq.get_item(0, vm)?; let col = seq.get_item(1, vm)?; - let row: u16 = row - .downcast_ref::() - .ok_or_else(|| vm.new_type_error("tcsetwinsize: winsize values must be integers"))? - .try_to_primitive(vm)?; - let col: u16 = col - .downcast_ref::() - .ok_or_else(|| vm.new_type_error("tcsetwinsize: winsize values must be integers"))? - .try_to_primitive(vm)?; + let row: u16 = row.try_index(vm)?.try_to_primitive(vm)?; + let col: u16 = col.try_index(vm)?.try_to_primitive(vm)?; host_termios::tcsetwinsize(fd, row, col).map_err(|e| termios_error(e, vm))?; Ok(()) From cbe201b7f1b5b4c05c6f2b4992b4f4ca91d22248 Mon Sep 17 00:00:00 2001 From: Sumi Jeong <125195487+sigmaith@users.noreply.github.com> Date: Thu, 23 Jul 2026 16:40:53 +0900 Subject: [PATCH 6/6] Mark test_fork/test_spawn_doesnt_hang as expected failures Both were hidden by a skipIf on tty.tcgetwinsize, which now exists. Root cause: pty.fork() calls os.login_tty(), which isn't implemented, so the forked child crashes before the test body runs. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01RbzcYLnX5tMfTM9BP7FDTT --- Lib/test/test_pty.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/test_pty.py b/Lib/test/test_pty.py index 1126404fee7..0a915d0199d 100644 --- a/Lib/test/test_pty.py +++ b/Lib/test/test_pty.py @@ -195,6 +195,7 @@ def test_openpty(self): s2 = _readline(master_fd) self.assertEqual(b'For my pet fish, Eric.\n', normalize_output(s2)) + @unittest.expectedFailure # TODO: RUSTPYTHON; pty.fork() child is not made a session leader def test_fork(self): debug("calling pty.fork()") pid, master_fd = pty.fork() @@ -296,6 +297,7 @@ def test_master_read(self): self.assertEqual(data, b"") + @unittest.expectedFailure # TODO: RUSTPYTHON; pty.fork() child is not made a session leader def test_spawn_doesnt_hang(self): # gh-140482: Do the test in a pty.fork() child to avoid messing # with the interactive test runner's terminal settings.