Skip to content

Commit 7c4017b

Browse files
committed
Retry select/poll/fcntl on EINTR and add kqueue (#8721)
* Retry select/poll/fcntl on EINTR select() no longer shares one empty list for r/w/x and now calls select(2) even with empty sets so the timeout elapses. poll() rejects type instantiation, treats float timeout as milliseconds, and raises RuntimeError on concurrent poll(). fcntl, ioctl, flock, and lockf retry after EINTR. Assisted-by: Grok:grok-4.6 * Add select.kqueue and select.kevent Expose kqueue(2)/kevent(2) on BSD and macOS, including control() EINTR retry and marking open kqueue objects closed after fork. Assisted-by: Grok:grok-4.6 * Add login_tty and fix termios/syslog/mmap termios.error is Exception, not OSError. os.login_tty prepares a tty for a new session. syslog.openlog encodes ident as UTF-8. mmap.seek returns the new position. Assisted-by: Grok:grok-4.6 * Honor mmap trackfd on Unix When trackfd is false, do not keep a duplicated file descriptor. size() then fails with EBADF and resize() raises ValueError. Assisted-by: Grok:grok-4.6 * address review: portable kevent and kqueue close Build kevent values by field assignment so FreeBSD's ext array compiles. Normalize timespec nanoseconds, keep the kqueue fd live across control(), reinit the open list lock after fork, make trackfd keyword-only, and skip login_tty on Solaris/illumos. Assisted-by: Grok:grok-4.6 (cherry picked from commit 0ba7048) Assisted-by: Codex:gpt-6
1 parent cdad8e2 commit 7c4017b

15 files changed

Lines changed: 750 additions & 71 deletions

File tree

Lib/test/_test_eintr.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -452,15 +452,13 @@ def wait_func(signum):
452452
class SelectEINTRTest(EINTRBaseTest):
453453
""" EINTR tests for the select module. """
454454

455-
@unittest.expectedFailure # TODO: RUSTPYTHON
456455
def test_select(self):
457456
t0 = time.monotonic()
458457
select.select([], [], [], self.sleep_time)
459458
dt = time.monotonic() - t0
460459
self.stop_alarm()
461460
self.check_elapsed_time(dt)
462461

463-
@unittest.skip("TODO: RUSTPYTHON; timed out at the 10 minute mark")
464462
@unittest.skipIf(sys.platform == "darwin",
465463
"poll may fail on macOS; see issue #28087")
466464
@unittest.skipUnless(hasattr(select, 'poll'), 'need select.poll')
@@ -544,12 +542,10 @@ def _lock(self, lock_func, lock_name):
544542

545543
# Issue 35633: See https://bugs.python.org/issue35633#msg333662
546544
# skip test rather than accept PermissionError from all platforms
547-
@unittest.expectedFailure # TODO: RUSTPYTHON; InterruptedError: [Errno 4] Interrupted system call
548545
@unittest.skipIf(platform.system() == "AIX", "AIX returns PermissionError")
549546
def test_lockf(self):
550547
self._lock(fcntl.lockf, "lockf")
551548

552-
@unittest.expectedFailure # TODO: RUSTPYTHON; InterruptedError: [Errno 4] Interrupted system call
553549
def test_flock(self):
554550
self._lock(fcntl.flock, "flock")
555551

Lib/test/test_mmap.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ def tearDown(self):
4646
except OSError:
4747
pass
4848

49-
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'mmap' object has no attribute 'seekable'
5049
def test_basic(self):
5150
# Test mmap module on Unix systems and Windows
5251

@@ -272,7 +271,6 @@ def test_access_parameter(self):
272271
self.assertRaises(TypeError, m.write_byte, 0)
273272
m.close()
274273

275-
@unittest.expectedFailure # TODO: RUSTPYTHON
276274
@unittest.skipIf(os.name == 'nt', 'trackfd not present on Windows')
277275
def test_trackfd_parameter(self):
278276
size = 64
@@ -308,7 +306,6 @@ def test_trackfd_parameter(self):
308306
self.assertEqual(m.closed, True)
309307
self.assertEqual(os.stat(TESTFN).st_size, size)
310308

311-
@unittest.expectedFailure # TODO: RUSTPYTHON
312309
@unittest.skipIf(os.name == 'nt', 'trackfd not present on Windows')
313310
def test_trackfd_neg1(self):
314311
size = 64

Lib/test/test_poll.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,6 @@ def test_poll_c_limits(self):
185185
self.assertRaises(OverflowError, pollster.poll, INT_MAX + 1)
186186
self.assertRaises(OverflowError, pollster.poll, UINT_MAX + 1)
187187

188-
@unittest.skip("TODO: RUSTPYTHON; fd reallocation")
189188
@threading_helper.reap_threads
190189
def test_threaded_poll(self):
191190
r, w = os.pipe()

Lib/test/test_pty.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ def write_all(fd, data):
8989

9090
# Marginal testing of pty suite. Cannot do extensive 'do or fail' testing
9191
# because pty code is not too portable.
92-
@unittest.skipIf(getattr(tty, "tcgetwinsize", None) is None, "TODO: RUSTPYTHON; `tty.tcgetwinsize` is required for setUp")
9392
class PtyTest(unittest.TestCase):
9493
def setUp(self):
9594
old_sighup = signal.signal(signal.SIGHUP, self.handle_sighup)
@@ -195,11 +194,6 @@ def test_openpty(self):
195194
s2 = _readline(master_fd)
196195
self.assertEqual(b'For my pet fish, Eric.\n', normalize_output(s2))
197196

198-
# skip (not expectedFailure) because the test still forks a real child
199-
# process, which crashes on missing os.login_tty() inside the parallel
200-
# test runner's worker process, corrupting its JSON reporting channel
201-
# ("worker bug", reproducible under --slow-ci -j N; not under plain -m test).
202-
@unittest.skip("TODO: RUSTPYTHON; pty.fork() calls os.login_tty(), which is not implemented")
203197
def test_fork(self):
204198
debug("calling pty.fork()")
205199
pid, master_fd = pty.fork()
@@ -301,11 +295,6 @@ def test_master_read(self):
301295

302296
self.assertEqual(data, b"")
303297

304-
# skip (not expectedFailure) because the test still forks a real child
305-
# process, which crashes on missing os.login_tty() inside the parallel
306-
# test runner's worker process, corrupting its JSON reporting channel
307-
# ("worker bug", reproducible under --slow-ci -j N; not under plain -m test).
308-
@unittest.skip("TODO: RUSTPYTHON; pty.fork() calls os.login_tty(), which is not implemented")
309298
def test_spawn_doesnt_hang(self):
310299
# gh-140482: Do the test in a pty.fork() child to avoid messing
311300
# with the interactive test runner's terminal settings.

Lib/test/test_select.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ def test_select(self):
8282
@unittest.skipIf(
8383
support.is_emscripten, "Emscripten cannot select a fd multiple times."
8484
)
85-
@unittest.skip("TODO: RUSTPYTHON timed out")
8685
def test_select_mutated(self):
8786
a = []
8887
class F:
@@ -92,7 +91,6 @@ def fileno(self):
9291
a[:] = [F()] * 10
9392
self.assertEqual(select.select([], a, []), ([], a[:5], []))
9493

95-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: TypeError not raised by poll
9694
def test_disallow_instantiation(self):
9795
support.check_disallow_instantiation(self, type(select.poll()))
9896

Lib/test/test_syslog.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ class Test(unittest.TestCase):
1616
def tearDown(self):
1717
syslog.closelog()
1818

19-
@unittest.expectedFailure # TODO: RUSTPYTHON
2019
def test_openlog(self):
2120
syslog.openlog('python')
2221
# Issue #6697.

Lib/test/test_termios.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,6 @@ def test_ioctl_constants(self):
289289
with self.subTest(name=name):
290290
self.assertGreaterEqual(value, 0)
291291

292-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: <class 'termios.error'> is a subclass of <class 'OSError'>
293292
def test_exception(self):
294293
self.assertIsSubclass(termios.error, Exception)
295294
self.assertNotIsSubclass(termios.error, OSError)

crates/host_env/src/posix.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,16 @@ pub fn setresuid(ruid: u32, euid: u32, suid: u32) -> std::io::Result<()> {
10131013
.map_err(std::io::Error::from)
10141014
}
10151015

1016+
#[cfg(not(any(target_os = "wasi", target_os = "solaris", target_os = "illumos")))]
1017+
pub fn login_tty(fd: i32) -> std::io::Result<()> {
1018+
let ret = unsafe { libc::login_tty(fd) };
1019+
if ret < 0 {
1020+
Err(std::io::Error::last_os_error())
1021+
} else {
1022+
Ok(())
1023+
}
1024+
}
1025+
10161026
#[cfg(not(target_os = "redox"))]
10171027
pub fn openpty() -> std::io::Result<(OwnedFd, OwnedFd)> {
10181028
let pty = nix::pty::openpty(None, None).map_err(std::io::Error::from)?;

crates/host_env/src/select.rs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,3 +330,153 @@ pub mod epoll {
330330
}
331331
}
332332
}
333+
334+
#[cfg(any(
335+
target_os = "macos",
336+
target_os = "ios",
337+
target_os = "freebsd",
338+
target_os = "netbsd",
339+
target_os = "openbsd",
340+
target_os = "dragonfly",
341+
))]
342+
pub mod kqueue {
343+
use alloc::sync::{Arc, Weak};
344+
use core::sync::atomic::{AtomicI32, Ordering};
345+
use parking_lot::Mutex;
346+
use std::io;
347+
use std::os::fd::BorrowedFd;
348+
349+
pub use libc::{
350+
EV_ADD, EV_CLEAR, EV_DELETE, EV_DISABLE, EV_ENABLE, EV_EOF, EV_ERROR, EV_FLAG1, EV_ONESHOT,
351+
EV_SYSFLAGS, EVFILT_AIO, EVFILT_PROC, EVFILT_READ, EVFILT_SIGNAL, EVFILT_TIMER,
352+
EVFILT_VNODE, EVFILT_WRITE, NOTE_ATTRIB, NOTE_CHILD, NOTE_DELETE, NOTE_EXEC, NOTE_EXIT,
353+
NOTE_EXTEND, NOTE_FORK, NOTE_LINK, NOTE_LOWAT, NOTE_PCTRLMASK, NOTE_PDATAMASK, NOTE_RENAME,
354+
NOTE_REVOKE, NOTE_TRACK, NOTE_TRACKERR, NOTE_WRITE,
355+
};
356+
357+
#[derive(Copy, Clone, Debug)]
358+
pub struct Event {
359+
pub ident: usize,
360+
pub filter: i16,
361+
pub flags: u16,
362+
pub fflags: u32,
363+
pub data: isize,
364+
pub udata: usize,
365+
}
366+
367+
impl Event {
368+
pub fn to_libc(self) -> libc::kevent {
369+
// Field widths and optional `ext` differ across BSDs; assign
370+
// rather than using a struct literal.
371+
let mut ev: libc::kevent = unsafe { core::mem::zeroed() };
372+
ev.ident = self.ident as _;
373+
ev.filter = self.filter as _;
374+
ev.flags = self.flags as _;
375+
ev.fflags = self.fflags as _;
376+
ev.data = self.data as _;
377+
ev.udata = self.udata as *mut libc::c_void;
378+
ev
379+
}
380+
381+
#[allow(clippy::unnecessary_cast)]
382+
pub fn from_libc(e: libc::kevent) -> Self {
383+
Self {
384+
ident: e.ident as usize,
385+
filter: e.filter as i16,
386+
flags: e.flags as u16,
387+
fflags: e.fflags as u32,
388+
data: e.data as isize,
389+
udata: e.udata as usize,
390+
}
391+
}
392+
}
393+
394+
static OPEN: Mutex<Vec<Weak<AtomicI32>>> = Mutex::new(Vec::new());
395+
396+
fn register_open(cell: &Arc<AtomicI32>) {
397+
let mut open = OPEN.lock();
398+
open.retain(|w| w.strong_count() > 0);
399+
open.push(Arc::downgrade(cell));
400+
}
401+
402+
pub fn create() -> io::Result<Arc<AtomicI32>> {
403+
let fd = unsafe { libc::kqueue() };
404+
if fd < 0 {
405+
return Err(io::Error::last_os_error());
406+
}
407+
let borrowed = unsafe { BorrowedFd::borrow_raw(fd) };
408+
if let Err(err) = crate::posix::set_inheritable(borrowed, false) {
409+
let _ = unsafe { libc::close(fd) };
410+
return Err(err);
411+
}
412+
let cell = Arc::new(AtomicI32::new(fd));
413+
register_open(&cell);
414+
Ok(cell)
415+
}
416+
417+
pub fn from_fd(fd: i32) -> Arc<AtomicI32> {
418+
let cell = Arc::new(AtomicI32::new(fd));
419+
register_open(&cell);
420+
cell
421+
}
422+
423+
pub fn close(cell: &AtomicI32) -> io::Result<()> {
424+
let fd = cell.swap(-1, Ordering::SeqCst);
425+
if fd < 0 {
426+
return Ok(());
427+
}
428+
let ret = unsafe { libc::close(fd) };
429+
if ret < 0 {
430+
Err(io::Error::last_os_error())
431+
} else {
432+
Ok(())
433+
}
434+
}
435+
436+
pub fn fd(cell: &AtomicI32) -> i32 {
437+
cell.load(Ordering::SeqCst)
438+
}
439+
440+
pub fn kevent(
441+
kq: i32,
442+
changelist: &[Event],
443+
eventlist: &mut [Event],
444+
timeout: Option<&libc::timespec>,
445+
) -> io::Result<usize> {
446+
let chl: Vec<libc::kevent> = changelist.iter().copied().map(Event::to_libc).collect();
447+
let mut evl = vec![unsafe { core::mem::zeroed() }; eventlist.len()];
448+
let timeout = timeout.map_or(core::ptr::null(), |t| t);
449+
let ret = unsafe {
450+
libc::kevent(
451+
kq,
452+
chl.as_ptr(),
453+
chl.len() as _,
454+
evl.as_mut_ptr(),
455+
evl.len() as _,
456+
timeout,
457+
)
458+
};
459+
if ret < 0 {
460+
return Err(io::Error::last_os_error());
461+
}
462+
let n = ret as usize;
463+
for (dst, src) in eventlist.iter_mut().zip(evl.into_iter().take(n)) {
464+
*dst = Event::from_libc(src);
465+
}
466+
Ok(n)
467+
}
468+
469+
pub fn mark_closed_after_fork() {
470+
// After fork only this thread exists. If the parent held OPEN,
471+
// the child's copy stays locked until it is released.
472+
if OPEN.try_lock().is_none() {
473+
unsafe { OPEN.force_unlock() };
474+
}
475+
let mut open = OPEN.lock();
476+
for weak in open.drain(..) {
477+
if let Some(cell) = weak.upgrade() {
478+
cell.store(-1, Ordering::SeqCst);
479+
}
480+
}
481+
}
482+
}

0 commit comments

Comments
 (0)