Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions Lib/test/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -4224,10 +4224,6 @@ class PyTextIOWrapperTest(TextIOWrapperTest):
io = pyio
shutdown_error = "LookupError: unknown encoding: ascii"

Comment thread
1ndahous3 marked this conversation as resolved.
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON; os.set_blocking not available on Windows")
def test_read_non_blocking(self):
return super().test_read_non_blocking()


class IncrementalNewlineDecoderTest(unittest.TestCase):

Expand Down
53 changes: 49 additions & 4 deletions crates/host_env/src/crt_fd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ mod c {
unsafe extern "C" {
#[link_name = "_chsize_s"]
pub(super) fn ftruncate(fd: i32, len: i64) -> i32;
pub(super) fn _get_doserrno(value: *mut c_ulong) -> c_int;
pub(super) fn _set_doserrno(value: c_ulong) -> c_int;
}
}

Expand Down Expand Up @@ -427,14 +429,57 @@ pub fn as_handle(fd: Borrowed<'_>) -> io::Result<BorrowedHandle<'_>> {

fn _write(fd: Raw, buf: &[u8]) -> io::Result<usize> {
let count = cmp::min(buf.len(), MAX_RW);
let n = cvt(unsafe { suppress_iph!(c::write(fd, buf.as_ptr() as _, count as _)) })?;
Ok(n as usize)
#[cfg(windows)]
{
let mut count = count;
loop {
unsafe { c::_set_doserrno(0) };
let n = unsafe { suppress_iph!(c::write(fd, buf.as_ptr() as _, count as _)) };
if n >= 0 {
return Ok(n as usize);
}
let errno = crate::os::get_errno();
// The CRT reports a successful zero-byte WriteFile as ENOSPC.
// A nonblocking pipe may still accept a smaller prefix.
if errno != libc::ENOSPC || dos_error() != 0 {
return Err(crate::os::io_error_from_errno(errno));
}
count /= 2;
if count == 0 {
return Err(crate::os::io_error_from_errno(libc::EAGAIN));
}
}
}
#[cfg(not(windows))]
{
let n = cvt(unsafe { suppress_iph!(c::write(fd, buf.as_ptr() as _, count as _)) })?;
Ok(n as usize)
}
}

fn _read(fd: Raw, buf: &mut [u8]) -> io::Result<usize> {
let count = cmp::min(buf.len(), MAX_RW);
let n = cvt(unsafe { suppress_iph!(libc::read(fd, buf.as_mut_ptr() as _, count as _)) })?;
Ok(n as usize)
#[cfg(windows)]
unsafe {
c::_set_doserrno(0);
}
let n = unsafe { suppress_iph!(libc::read(fd, buf.as_mut_ptr() as _, count as _)) };
#[cfg(windows)]
if n < 0
&& crate::os::get_errno() == libc::EINVAL
&& dos_error() == windows_sys::Win32::Foundation::ERROR_NO_DATA
{
return Err(crate::os::io_error_from_errno(libc::EAGAIN));
}
cvt(n).map(|n| n as usize)
}

#[cfg(windows)]
fn dos_error() -> u32 {
let mut error = 0;
// SAFETY: the CRT writes to a valid unsigned long on the current thread.
unsafe { c::_get_doserrno(&mut error) };
error
}

pub fn write(fd: Borrowed<'_>, buf: &[u8]) -> io::Result<usize> {
Expand Down
20 changes: 20 additions & 0 deletions crates/host_env/src/nt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use crate::{
windows::{CheckWin32Bool, CheckWin32Handle, CheckWin32Sentinel, HandleToOwned},
};
use libc::intptr_t;

use widestring::WideCString;
use windows_sys::{
Win32::{
Expand Down Expand Up @@ -75,6 +76,25 @@ pub use windows_sys::Win32::Storage::FileSystem::{
FILE_ATTRIBUTE_TEMPORARY, FILE_ATTRIBUTE_VIRTUAL,
};

pub fn get_blocking(fd: crt_fd::Borrowed<'_>) -> io::Result<bool> {
let handle = crt_fd::as_handle(fd)?;
let mode = crate::winapi::get_named_pipe_handle_state(handle.as_raw_handle())?;
Ok(mode & windows_sys::Win32::System::Pipes::PIPE_NOWAIT == 0)
}

pub fn set_blocking(fd: crt_fd::Borrowed<'_>, blocking: bool) -> io::Result<()> {
use windows_sys::Win32::System::Pipes::PIPE_NOWAIT;

let handle = crt_fd::as_handle(fd)?;
let mode = crate::winapi::get_named_pipe_handle_state(handle.as_raw_handle())?;
let mode = if blocking {
mode & !PIPE_NOWAIT
} else {
mode | PIPE_NOWAIT
};
crate::winapi::set_named_pipe_handle_state(handle.as_raw_handle(), Some(mode), None, None)
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
pub type Handle = HANDLE;
pub const MAX_PATH_USIZE: usize = MAX_PATH as usize;
pub const ERROR_INVALID_HANDLE_I32: i32 = ERROR_INVALID_HANDLE as i32;
Expand Down
10 changes: 7 additions & 3 deletions crates/host_env/src/os.rs
Original file line number Diff line number Diff line change
Expand Up @@ -698,13 +698,17 @@ impl core::error::Error for CrtErrno {}

/// Build an [`io::Error`] that preserves a raw C runtime `errno`.
///
/// The [`io::ErrorKind`] is derived from the closest Win32 mapping so callers
/// matching on `kind()` keep working, while the exact `errno` is preserved for
/// The [`io::ErrorKind`] preserves would-block semantics and otherwise uses the
/// closest Win32 mapping. The exact `errno` is preserved for
/// [`ErrorExt::posix_errno`].
#[cfg(windows)]
#[must_use]
pub fn io_error_from_errno(errno: i32) -> io::Error {
let kind = io::Error::from_raw_os_error(errno_to_winerror(errno)).kind();
let kind = if errno == libc::EAGAIN {
io::ErrorKind::WouldBlock
} else {
io::Error::from_raw_os_error(errno_to_winerror(errno)).kind()
};
io::Error::new(kind, CrtErrno(errno))
}

Expand Down
12 changes: 12 additions & 0 deletions crates/vm/src/stdlib/nt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ pub(crate) mod module {
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use std::os::windows::io::AsRawHandle;

#[pyfunction]
fn get_blocking(fd: crt_fd::Borrowed<'_>, vm: &VirtualMachine) -> PyResult<bool> {
vm.allow_threads(|| host_nt::get_blocking(fd))
.map_err(|err| err.to_pyexception(vm))
}

#[pyfunction]
fn set_blocking(fd: crt_fd::Borrowed<'_>, blocking: bool, vm: &VirtualMachine) -> PyResult<()> {
vm.allow_threads(|| host_nt::set_blocking(fd, blocking))
.map_err(|err| err.to_pyexception(vm))
}

#[pyattr]
use rustpython_host_env::os::{
O_BINARY, O_NOINHERIT, O_RANDOM, O_SEQUENTIAL, O_TEMPORARY, O_TEXT,
Expand Down
31 changes: 31 additions & 0 deletions extra_tests/snippets/stdlib_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,3 +758,34 @@ def __index__(self):
assert hasattr(os, "mkfifo")
if hasattr(os, "mkfifo"):
mkfifo_paths_and_directory_descriptors()


import errno


def nonblocking_windows_pipe():

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does test_read_non_blocking cover this test? then we usaully don't add original test

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, there was overlap. test_read_non_blocking covers the empty-pipe case through TextIOWrapper, and test_nonblock_pipe_write_bigbuf / test_nonblock_pipe_write_smallbuf cover partial/full writes and draining. In 2a37fafd3 I removed those higher-level checks from the snippet. The remaining small test checks Windows pipe blocking-mode toggles and the direct os.read/os.write edge cases, including EAGAIN and zero-length I/O.

reader, writer = os.pipe()
try:
for fd in (reader, writer):
assert os.get_blocking(fd)
os.set_blocking(fd, False)
assert not os.get_blocking(fd)
os.set_blocking(fd, True)
assert os.get_blocking(fd)
os.set_blocking(fd, False)

assert os.read(reader, 0) == b""
assert os.write(writer, b"") == 0
try:
os.read(reader, 1)
except BlockingIOError as error:
assert error.errno == errno.EAGAIN
else:
raise AssertionError("empty nonblocking pipe did not raise")
finally:
os.close(reader)
os.close(writer)


if os.name == "nt":
nonblocking_windows_pipe()
Loading