Skip to content

Commit 6617496

Browse files
authored
nixos/test-driver: stop blackholing vde_switch stderr (#467248)
2 parents 4ba5b89 + 2a50c1a commit 6617496

File tree

1 file changed

+78
-18
lines changed
  • nixos/lib/test-driver/src/test_driver

1 file changed

+78
-18
lines changed

nixos/lib/test-driver/src/test_driver/vlan.py

Lines changed: 78 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,43 @@
1+
import datetime as dt
2+
import fcntl
13
import io
24
import os
3-
import pty
5+
import select
46
import subprocess
7+
import typing
58
from pathlib import Path
69

710
from test_driver.logger import AbstractLogger
811

912

13+
def readline_with_timeout(
14+
readable: typing.IO[str], timeout: dt.timedelta
15+
) -> typing.Generator[str]:
16+
"""
17+
Read a line from `readable` within the given `timeout`, otherwise raises `TimeoutError`.
18+
19+
Note: while the generator is running, `readable` will be in nonblocking mode.
20+
"""
21+
fd = readable.fileno()
22+
og_flags = fcntl.fcntl(fd, fcntl.F_GETFL)
23+
fcntl.fcntl(fd, fcntl.F_SETFL, og_flags | os.O_NONBLOCK)
24+
25+
try:
26+
while True:
27+
ready, _, _ = select.select([readable], [], [], timeout.total_seconds())
28+
if len(ready) == 0:
29+
raise TimeoutError()
30+
31+
# Under the hood, `readline` may read more than one line from the file descriptor,
32+
# so we cannot just return to the `select`, as it may block, despite there being more
33+
# lines buffered. So, read all the lines before returning to the select. This only
34+
# works if the file descriptor is in non-blocking mode!
35+
while line := readable.readline():
36+
yield line
37+
finally:
38+
fcntl.fcntl(fd, fcntl.F_SETFL, og_flags)
39+
40+
1041
class VLan:
1142
"""This class handles a VLAN that the run-vm scripts identify via its
1243
number handles. The network's lifetime equals the object's lifetime.
@@ -33,33 +64,62 @@ def __init__(self, nr: int, tmp_dir: Path, logger: AbstractLogger):
3364
os.environ[f"QEMU_VDE_SOCKET_{self.nr}"] = str(self.socket_dir)
3465

3566
self.logger.info("start vlan")
36-
pty_master, pty_slave = pty.openpty()
3767

38-
# The --hub is required for the scenario determined by
39-
# nixos/tests/networking.nix vlan-ping.
40-
# VLAN Tagged traffic (802.1Q) seams to be blocked if a vde_switch is
41-
# used without the hub mode (flood packets to all ports).
4268
self.process = subprocess.Popen(
43-
["vde_switch", "-s", self.socket_dir, "--dirmode", "0700", "--hub"],
44-
stdin=pty_slave,
69+
[
70+
"vde_switch",
71+
"--sock",
72+
self.socket_dir,
73+
"--dirmode",
74+
"0700",
75+
# The --hub is required for the scenario determined by
76+
# nixos/tests/networkd-and-scripted.nix vlan-ping.
77+
# VLAN Tagged traffic (802.1Q) seems to be blocked if a vde_switch is
78+
# used without the hub mode (flood packets to all ports).
79+
"--hub",
80+
],
81+
bufsize=1, # Line buffered.
82+
stdin=subprocess.PIPE,
4583
stdout=subprocess.PIPE,
46-
stderr=subprocess.PIPE,
47-
shell=False,
84+
stderr=None, # Do not swallow stderr.
85+
text=True,
4886
)
4987
self.pid = self.process.pid
50-
self.fd = os.fdopen(pty_master, "w")
51-
self.fd.write("version\n")
5288

53-
# TODO: perl version checks if this can be read from
54-
# an if not, dies. we could hang here forever. Fix it.
89+
assert self.process.stdin is not None
90+
self.process.stdin.write("showinfo\n")
91+
92+
# showinfo's output looks like this:
93+
#
94+
# ```
95+
# vde$ showinfo
96+
# 0000 DATA END WITH '.'
97+
# VDE switch V.2.3.3
98+
# (C) Virtual Square Team (coord. R. Davoli) 2005,2006,2007 - GPLv2
99+
#
100+
# pid 82406 MAC 00:ff:62:25:47:55 uptime 45
101+
# .
102+
# 1000 Success
103+
# ```
104+
#
105+
# We read past all the output until we get to the `1000 Success`.
106+
# This serves 2 purposes:
107+
# 1. It's a nice sanity check that `vde_switch` is actually working.
108+
# 2. By the time we're done, `vde_switch` will have created the
109+
# `ctl` socket in `socket_dir`, so we don't have to wait for it to exist.
55110
assert self.process.stdout is not None
56-
self.process.stdout.readline()
57-
if not (self.socket_dir / "ctl").exists():
58-
self.logger.error("cannot start vde_switch")
111+
for line in readline_with_timeout(
112+
self.process.stdout, timeout=dt.timedelta(seconds=5)
113+
):
114+
if "1000 Success" in line:
115+
break
116+
117+
assert (self.socket_dir / "ctl").exists(), "cannot start vde_switch"
59118

60119
self.logger.info(f"running vlan (pid {self.pid}; ctl {self.socket_dir})")
61120

62121
def stop(self) -> None:
63122
self.logger.info(f"kill vlan (pid {self.pid})")
64-
self.fd.close()
123+
assert self.process.stdin is not None
124+
self.process.stdin.close()
65125
self.process.terminate()

0 commit comments

Comments
 (0)