Skip to content

Commit 5bed327

Browse files
bnoordhuisaddaleax
authored andcommitted
test: fix pty test hangs on aix
Some pty tests persistently hung on the AIX CI buildbots. Fix that by adding a helper script that properly sets up the pty before spawning the script under test. On investigation I discovered that the test runner hung when it tried to close the slave pty's file descriptor, probably due to a bug in AIX's pty implementation. I could reproduce it with a short C program. The test runner also leaked file descriptors to the child process. I couldn't convince python's `subprocess.Popen()` to do what I wanted it to do so I opted to move the logic to a helper script that can do fork/setsid/etc. without having to worry about stomping on state in tools/test.py. In the process I also uncovered some bugs in the pty module of the python distro that ships with macOS 10.14, leading me to reimplement a sizable chunk of the functionality of that module. And last but not least, of course there are differences between ptys on different platforms and the helper script has to paper over that. Of course. Really, this commit took me longer to put together than I care to admit. Caveat emptor: this commit takes the hacky ^D feeding to the slave out of tools/test.py and puts it in the *.in input files. You can also feed other control characters to tests, like ^C or ^Z, simply by inserting them into the corresponding input file. I think that's nice. Fixes: nodejs/build#1820 Fixes: #28489 PR-URL: #28600 Reviewed-By: Richard Lau <[email protected]> Reviewed-By: Sam Roberts <[email protected]> Reviewed-By: Anna Henningsen <[email protected]>
1 parent d0b0230 commit 5bed327

File tree

6 files changed

+122
-105
lines changed

6 files changed

+122
-105
lines changed

test/pseudo-tty/pseudo-tty.status

-35
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,5 @@
11
prefix pseudo-tty
22

3-
[$system==aix]
4-
# https://github.com/nodejs/build/issues/1820#issuecomment-505998851
5-
# https://github.com/nodejs/node/pull/28469
6-
# https://github.com/nodejs/node/pull/28541
7-
console_colors: SKIP
8-
console-dumb-tty: SKIP
9-
no_dropped_stdio: SKIP
10-
no_interleaved_stdio: SKIP
11-
readline-dumb-tty: SKIP
12-
ref_keeps_node_running: SKIP
13-
repl-dumb-tty: SKIP
14-
stdin-setrawmode: SKIP
15-
test-assert-colors: SKIP
16-
test-assert-no-color: SKIP
17-
test-assert-position-indicator: SKIP
18-
test-async-wrap-getasyncid-tty: SKIP
19-
test-fatal-error: SKIP
20-
test-handle-wrap-isrefed-tty: SKIP
21-
test-readable-tty-keepalive: SKIP
22-
test-set-raw-mode-reset-process-exit: SKIP
23-
test-set-raw-mode-reset-signal: SKIP
24-
test-set-raw-mode-reset: SKIP
25-
test-stderr-stdout-handle-sigwinch: SKIP
26-
test-stdin-write: SKIP
27-
test-stdout-read: SKIP
28-
test-tty-color-support: SKIP
29-
test-tty-isatty: SKIP
30-
test-tty-stdin-call-end: SKIP
31-
test-tty-stdin-end: SKIP
32-
test-tty-stdout-end: SKIP
33-
test-tty-stdout-resize: SKIP
34-
test-tty-stream-constructors: SKIP
35-
test-tty-window-size: SKIP
36-
test-tty-wrap: SKIP
37-
383
[$system==solaris]
394
# https://github.com/nodejs/node/pull/16225 - `ioctl(fd, TIOCGWINSZ)` seems
405
# to fail with EINVAL on SmartOS when `fd` is a pty from python's pty module.

test/pseudo-tty/pty_helper.py

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import errno
2+
import os
3+
import pty
4+
import select
5+
import signal
6+
import sys
7+
import termios
8+
9+
STDIN = 0
10+
STDOUT = 1
11+
STDERR = 2
12+
13+
14+
def pipe(sfd, dfd):
15+
try:
16+
data = os.read(sfd, 256)
17+
except OSError as e:
18+
if e.errno != errno.EIO:
19+
raise
20+
return True # EOF
21+
22+
if not data:
23+
return True # EOF
24+
25+
if dfd == STDOUT:
26+
# Work around platform quirks. Some platforms echo ^D as \x04
27+
# (AIX, BSDs) and some don't (Linux).
28+
filt = lambda c: ord(c) > 31 or c in '\t\n\r\f'
29+
data = filter(filt, data)
30+
31+
while data:
32+
try:
33+
n = os.write(dfd, data)
34+
except OSError as e:
35+
if e.errno != errno.EIO:
36+
raise
37+
return True # EOF
38+
data = data[n:]
39+
40+
41+
if __name__ == '__main__':
42+
argv = sys.argv[1:]
43+
44+
# Make select() interruptable by SIGCHLD.
45+
signal.signal(signal.SIGCHLD, lambda nr, _: None)
46+
47+
master_fd, slave_fd = pty.openpty()
48+
assert master_fd > STDIN
49+
50+
mode = termios.tcgetattr(slave_fd)
51+
# Don't translate \n to \r\n.
52+
mode[1] = mode[1] & ~termios.ONLCR # oflag
53+
# Disable ECHOCTL. It's a BSD-ism that echoes e.g. \x04 as ^D but it
54+
# doesn't work on platforms like AIX and Linux. I checked Linux's tty
55+
# driver and it's a no-op, the driver is just oblivious to the flag.
56+
mode[3] = mode[3] & ~termios.ECHOCTL # lflag
57+
termios.tcsetattr(slave_fd, termios.TCSANOW, mode)
58+
59+
pid = os.fork()
60+
if not pid:
61+
os.setsid()
62+
os.close(master_fd)
63+
64+
# Ensure the pty is a controlling tty.
65+
name = os.ttyname(slave_fd)
66+
fd = os.open(name, os.O_RDWR)
67+
os.dup2(fd, slave_fd)
68+
os.close(fd)
69+
70+
os.dup2(slave_fd, STDIN)
71+
os.dup2(slave_fd, STDOUT)
72+
os.dup2(slave_fd, STDERR)
73+
74+
if slave_fd > STDERR:
75+
os.close(slave_fd)
76+
77+
os.execve(argv[0], argv, os.environ)
78+
raise Exception('unreachable')
79+
80+
os.close(slave_fd)
81+
82+
fds = [STDIN, master_fd]
83+
while fds:
84+
try:
85+
rfds, _, _ = select.select(fds, [], [])
86+
except select.error as e:
87+
if e[0] != errno.EINTR:
88+
raise
89+
if pid == os.waitpid(pid, os.WNOHANG)[0]:
90+
break
91+
92+
if STDIN in rfds:
93+
if pipe(STDIN, master_fd):
94+
fds.remove(STDIN)
95+
96+
if master_fd in rfds:
97+
if pipe(master_fd, STDOUT):
98+
break

test/pseudo-tty/test-stdout-read.in

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
Hello!
2+


test/pseudo-tty/test-stdout-read.out

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
Hello!
12
<Buffer 48 65 6c 6c 6f 21 0a>

test/pseudo-tty/testcfg.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@
2929

3030
import test
3131
import os
32-
from os.path import join, exists, basename, isdir
32+
from os.path import join, exists, basename, dirname, isdir
3333
import re
34+
import sys
3435
import utils
3536
from functools import reduce
3637

3738
FLAGS_PATTERN = re.compile(r"//\s+Flags:(.*)")
39+
PTY_HELPER = join(dirname(__file__), 'pty_helper.py')
3840

3941
class TTYTestCase(test.TestCase):
4042

@@ -108,16 +110,18 @@ def GetSource(self):
108110
+ open(self.expected).read())
109111

110112
def RunCommand(self, command, env):
111-
input_arg = None
113+
fd = None
112114
if self.input is not None and exists(self.input):
113-
input_arg = open(self.input).read()
115+
fd = os.open(self.input, os.O_RDONLY)
114116
full_command = self.context.processor(command)
117+
full_command = [sys.executable, PTY_HELPER] + full_command
115118
output = test.Execute(full_command,
116119
self.context,
117120
self.context.GetTimeout(self.mode),
118121
env,
119-
faketty=True,
120-
input=input_arg)
122+
stdin=fd)
123+
if fd is not None:
124+
os.close(fd)
121125
return test.TestOutput(self,
122126
full_command,
123127
output,

tools/test.py

+13-65
Original file line numberDiff line numberDiff line change
@@ -638,15 +638,10 @@ def RunProcess(context, timeout, args, **rest):
638638
prev_error_mode = Win32SetErrorMode(error_mode)
639639
Win32SetErrorMode(error_mode | prev_error_mode)
640640

641-
faketty = rest.pop('faketty', False)
642-
pty_out = rest.pop('pty_out')
643-
644641
process = subprocess.Popen(
645642
args = popen_args,
646643
**rest
647644
)
648-
if faketty:
649-
os.close(rest['stdout'])
650645
if utils.IsWindows() and context.suppress_dialogs and prev_error_mode != SEM_INVALID_VALUE:
651646
Win32SetErrorMode(prev_error_mode)
652647
# Compute the end time - if the process crosses this limit we
@@ -658,28 +653,6 @@ def RunProcess(context, timeout, args, **rest):
658653
# loop and keep track of whether or not it times out.
659654
exit_code = None
660655
sleep_time = INITIAL_SLEEP_TIME
661-
output = ''
662-
if faketty:
663-
while True:
664-
if time.time() >= end_time:
665-
# Kill the process and wait for it to exit.
666-
KillTimedOutProcess(context, process.pid)
667-
exit_code = process.wait()
668-
timed_out = True
669-
break
670-
671-
# source: http://stackoverflow.com/a/12471855/1903116
672-
# related: http://stackoverflow.com/q/11165521/1903116
673-
try:
674-
data = os.read(pty_out, 9999)
675-
except OSError as e:
676-
if e.errno != errno.EIO:
677-
raise
678-
break # EIO means EOF on some systems
679-
else:
680-
if not data: # EOF
681-
break
682-
output += data
683656

684657
while exit_code is None:
685658
if (not end_time is None) and (time.time() >= end_time):
@@ -693,7 +666,7 @@ def RunProcess(context, timeout, args, **rest):
693666
sleep_time = sleep_time * SLEEP_TIME_FACTOR
694667
if sleep_time > MAX_SLEEP_TIME:
695668
sleep_time = MAX_SLEEP_TIME
696-
return (process, exit_code, timed_out, output)
669+
return (process, exit_code, timed_out)
697670

698671

699672
def PrintError(str):
@@ -715,31 +688,12 @@ def CheckedUnlink(name):
715688
PrintError("os.unlink() " + str(e))
716689
break
717690

718-
def Execute(args, context, timeout=None, env=None, faketty=False, disable_core_files=False, input=None):
691+
def Execute(args, context, timeout=None, env=None, disable_core_files=False, stdin=None):
692+
(fd_out, outname) = tempfile.mkstemp()
693+
(fd_err, errname) = tempfile.mkstemp()
694+
719695
if env is None:
720696
env = {}
721-
if faketty:
722-
import pty
723-
(out_master, fd_out) = pty.openpty()
724-
fd_in = fd_err = fd_out
725-
pty_out = out_master
726-
727-
if input is not None:
728-
# Before writing input data, disable echo so the input doesn't show
729-
# up as part of the output.
730-
import termios
731-
attr = termios.tcgetattr(fd_in)
732-
attr[3] = attr[3] & ~termios.ECHO
733-
termios.tcsetattr(fd_in, termios.TCSADRAIN, attr)
734-
735-
os.write(pty_out, input)
736-
os.write(pty_out, '\x04') # End-of-file marker (Ctrl+D)
737-
else:
738-
(fd_out, outname) = tempfile.mkstemp()
739-
(fd_err, errname) = tempfile.mkstemp()
740-
fd_in = 0
741-
pty_out = None
742-
743697
env_copy = os.environ.copy()
744698

745699
# Remove NODE_PATH
@@ -758,28 +712,22 @@ def disableCoreFiles():
758712
resource.setrlimit(resource.RLIMIT_CORE, (0,0))
759713
preexec_fn = disableCoreFiles
760714

761-
(process, exit_code, timed_out, output) = RunProcess(
715+
(process, exit_code, timed_out) = RunProcess(
762716
context,
763717
timeout,
764718
args = args,
765-
stdin = fd_in,
719+
stdin = stdin,
766720
stdout = fd_out,
767721
stderr = fd_err,
768722
env = env_copy,
769-
faketty = faketty,
770-
pty_out = pty_out,
771723
preexec_fn = preexec_fn
772724
)
773-
if faketty:
774-
os.close(out_master)
775-
errors = ''
776-
else:
777-
os.close(fd_out)
778-
os.close(fd_err)
779-
output = open(outname).read()
780-
errors = open(errname).read()
781-
CheckedUnlink(outname)
782-
CheckedUnlink(errname)
725+
os.close(fd_out)
726+
os.close(fd_err)
727+
output = open(outname).read()
728+
errors = open(errname).read()
729+
CheckedUnlink(outname)
730+
CheckedUnlink(errname)
783731

784732
return CommandOutput(exit_code, timed_out, output, errors)
785733

0 commit comments

Comments
 (0)