Skip to content

Commit f441dca

Browse files
fix: raise EIO without strace
Signed-off-by: yihong0618 <zouzou0208@gmail.com> Co-authored-by: graymon <greyschwinger@gmail.com>
1 parent d54f137 commit f441dca

1 file changed

Lines changed: 101 additions & 48 deletions

File tree

Lib/test/test_pyrepl/test_unix_console.py

Lines changed: 101 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
import termios
77
from functools import partial
88
from test.support import os_helper, force_not_colorized_test_class
9-
from test.support.strace_helper import requires_strace
109
import subprocess
11-
import shutil
1210
import signal
1311
import textwrap
1412

@@ -339,38 +337,104 @@ def test_eio_error_handling_in_restore(self, mock_tcgetattr, mock_tcsetattr):
339337
self.fail("EIO error should have been handled gracefully in restore()")
340338
raise
341339

342-
@requires_strace()
343-
class TestEIOWithStrace(unittest.TestCase):
344-
def _attach_strace(self, pid):
345-
strace = shutil.which("strace")
346-
cmd = [strace, "-qq", "-p", str(pid), "-e", "inject=read:error=EIO:when=1", "-o", "/dev/null"]
347-
try:
348-
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
349-
try:
350-
p.communicate(timeout=0.15)
351-
except subprocess.TimeoutExpired:
352-
return p
353-
except (OSError, subprocess.SubprocessError):
354-
pass
355-
except (OSError, subprocess.SubprocessError):
356-
pass
357-
358-
# Fallback command
359-
cmd = [strace, "-qq", "-p", str(pid), "-e", "fault=read:error=EIO:when=1", "-o", "/dev/null"]
360-
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
361-
try:
362-
p.communicate(timeout=0.15)
363-
except subprocess.TimeoutExpired:
364-
pass
365-
return p
366-
340+
@unittest.skipUnless(sys.platform == "linux", "Only valid on Linux")
367341
def test_repl_eio(self):
342+
# Use the pty-based approach to simulate EIO error
368343
child_code = textwrap.dedent("""
369-
import signal, sys
370-
signal.signal(signal.SIGUSR1, lambda *a: None)
344+
import os, sys, pty, fcntl, termios, signal, time, errno
345+
346+
def handler(sig, f):
347+
pass
348+
349+
def create_eio_condition():
350+
try:
351+
# Try to create a condition that will actually produce EIO
352+
# Method: Use your original script's approach with modifications
353+
master_fd, slave_fd = pty.openpty()
354+
# Fork a child that will manipulate the pty
355+
child_pid = os.fork()
356+
if child_pid == 0:
357+
# Child process
358+
try:
359+
# Set up session and control terminal like your script
360+
os.setsid()
361+
fcntl.ioctl(slave_fd, termios.TIOCSCTTY, 0)
362+
# Get process group
363+
p2_pgid = os.getpgrp()
364+
# Fork grandchild
365+
grandchild_pid = os.fork()
366+
if grandchild_pid == 0:
367+
# Grandchild - set up process group
368+
os.setpgid(0, 0)
369+
# Redirect stdin to slave
370+
os.dup2(slave_fd, 0)
371+
if slave_fd > 2:
372+
os.close(slave_fd)
373+
# Fork great-grandchild for terminal control manipulation
374+
ggc_pid = os.fork()
375+
if ggc_pid == 0:
376+
# Great-grandchild - just exit quickly
377+
sys.exit(0)
378+
else:
379+
# Back to grandchild
380+
try:
381+
os.tcsetpgrp(0, p2_pgid)
382+
except:
383+
pass
384+
sys.exit(0)
385+
else:
386+
# Back to child
387+
try:
388+
os.setpgid(grandchild_pid, grandchild_pid)
389+
except ProcessLookupError:
390+
pass
391+
os.tcsetpgrp(slave_fd, grandchild_pid)
392+
if slave_fd > 2:
393+
os.close(slave_fd)
394+
os.waitpid(grandchild_pid, 0)
395+
# Manipulate terminal control to create EIO condition
396+
os.tcsetpgrp(master_fd, p2_pgid)
397+
# Now try to read from master - this might cause EIO
398+
try:
399+
os.read(master_fd, 1)
400+
except OSError as e:
401+
if e.errno == errno.EIO:
402+
print(f"Setup created EIO condition: {e}", file=sys.stderr)
403+
sys.exit(0)
404+
except Exception as setup_e:
405+
print(f"Setup error: {setup_e}", file=sys.stderr)
406+
sys.exit(1)
407+
else:
408+
# Parent process
409+
os.close(slave_fd)
410+
os.waitpid(child_pid, 0)
411+
# Now replace stdin with master_fd and try to read
412+
os.dup2(master_fd, 0)
413+
os.close(master_fd)
414+
# This should now trigger EIO
415+
result = input()
416+
print(f"Unexpectedly got input: {repr(result)}", file=sys.stderr)
417+
sys.exit(0)
418+
except OSError as e:
419+
if e.errno == errno.EIO:
420+
print(f"Got EIO: {e}", file=sys.stderr)
421+
sys.exit(1)
422+
elif e.errno == errno.ENXIO:
423+
print(f"Got ENXIO (no such device): {e}", file=sys.stderr)
424+
sys.exit(1) # Treat ENXIO as success too
425+
else:
426+
print(f"Got other OSError: errno={e.errno} {e}", file=sys.stderr)
427+
sys.exit(2)
428+
except EOFError as e:
429+
print(f"Got EOFError: {e}", file=sys.stderr)
430+
sys.exit(3)
431+
except Exception as e:
432+
print(f"Got unexpected error: {type(e).__name__}: {e}", file=sys.stderr)
433+
sys.exit(4)
434+
# Set up signal handler for coordination
435+
signal.signal(signal.SIGUSR1, lambda *a: create_eio_condition())
371436
print("READY", flush=True)
372437
signal.pause()
373-
input()
374438
""").strip()
375439

376440
proc = subprocess.Popen(
@@ -381,22 +445,11 @@ def test_repl_eio(self):
381445
text=True
382446
)
383447

384-
ready = False
385-
while not ready:
386-
line = proc.stdout.readline().strip()
387-
if line == "READY":
388-
ready = True
389-
break
390-
if proc.poll() is not None:
391-
self.fail("Child process exited unexpectedly")
448+
ready_line = proc.stdout.readline().strip()
449+
if ready_line != "READY" or proc.poll() is not None:
450+
self.fail("Child process failed to start properly")
392451

393-
tracer = self._attach_strace(proc.pid)
394-
395-
try:
396-
os.kill(proc.pid, signal.SIGUSR1)
397-
_, err = proc.communicate(timeout=5)
398-
finally:
399-
if tracer and tracer.poll() is None:
400-
tracer.terminate()
401-
self.assertNotEqual(proc.returncode, 0)
402-
self.assertTrue("Errno 5" in err or "Input/output error" in err or "EOFError" in err, err)
452+
os.kill(proc.pid, signal.SIGUSR1)
453+
_, err = proc.communicate(timeout=5) # sleep for pty to settle
454+
self.assertEqual(proc.returncode, 1, f"Expected EIO error, got return code {proc.returncode}")
455+
self.assertIn("Got EIO:", err, f"Expected EIO error message in stderr: {err}")

0 commit comments

Comments
 (0)