@@ -104,7 +104,7 @@ def read_output() -> None:
104104 assert process .poll () is None , "" .join (lines )
105105 time .sleep (0.05 )
106106 assert url , "server did not announce its URL: " + "" .join (lines )
107- yield url , served
107+ yield url , served , process . pid
108108 finally :
109109 process .terminate ()
110110 process .wait (timeout = 10 )
@@ -115,7 +115,7 @@ def test_terminal_240_interactive_browser(
115115 terminal_server : Any , browser_name : str
116116) -> None :
117117 playwright = pytest .importorskip ("playwright.sync_api" )
118- url , expected_cwd = terminal_server
118+ url , expected_cwd , _ = terminal_server
119119 with playwright .sync_playwright () as manager :
120120 browser = getattr (manager , browser_name ).launch ()
121121 page = browser .new_page (viewport = {"width" : 1200 , "height" : 900 })
@@ -245,13 +245,49 @@ def wait_output(text: str) -> None:
245245 browser .close ()
246246
247247
248+ def _descendants (pid : int ) -> list [int ]:
249+ found : list [int ] = []
250+ pending = [pid ]
251+ while pending :
252+ parent = pending .pop ()
253+ for task in Path (f"/proc/{ parent } /task" ).glob ("*" ):
254+ try :
255+ children = (task / "children" ).read_text ().split ()
256+ except OSError :
257+ continue
258+ for child in map (int , children ):
259+ found .append (child )
260+ pending .append (child )
261+ return found
262+
263+
264+ def _pty_holders (pid : int ) -> list [int ]:
265+ holders = []
266+ for candidate in [pid , * _descendants (pid )]:
267+ try :
268+ fds = list (Path (f"/proc/{ candidate } /fd" ).iterdir ())
269+ except OSError :
270+ continue
271+ for fd in fds :
272+ try :
273+ if os .readlink (fd ) == "/dev/ptmx" :
274+ holders .append (candidate )
275+ break
276+ except OSError :
277+ continue
278+ return holders
279+
280+
248281@pytest .mark .skipif (os .name == "nt" , reason = "Unix stty regression" )
249- def test_terminal_240_disconnect_with_blocked_stdin (terminal_server : Any ) -> None :
282+ @pytest .mark .parametrize ("browser_name" , ["chromium" , "webkit" ])
283+ def test_terminal_240_disconnect_with_blocked_stdin (
284+ terminal_server : Any , browser_name : str
285+ ) -> None :
250286 """RED: four blocked writers leaked every slot; fifth upgrade was HTTP 429."""
251287 playwright = pytest .importorskip ("playwright.sync_api" )
252- url , _ = terminal_server
288+ url , _ , server_pid = terminal_server
253289 with playwright .sync_playwright () as manager :
254- browser = manager . chromium .launch ()
290+ browser = getattr ( manager , browser_name ) .launch ()
255291 page = browser .new_page ()
256292 page .goto (url )
257293 result = page .evaluate ("""async () => {
@@ -277,13 +313,38 @@ def test_terminal_240_disconnect_with_blocked_stdin(terminal_server: Any) -> Non
277313 }
278314 };
279315 });
280- await new Promise(resolve => setTimeout(resolve, 400));
281316 }
282- return await new Promise(resolve => {
283- const ws = new WebSocket(url);
284- ws.onopen = () => { ws.close(); resolve(true); };
285- ws.onerror = () => resolve(false);
286- });
317+ // Every slot must come back promptly, not just one of them, and long
318+ // before the blocked `sleep 30` foreground programs would exit.
319+ const started = performance.now();
320+ const deadline = started + 5000;
321+ while (performance.now() < deadline) {
322+ const sockets = [];
323+ const opened = await Promise.all([0, 1, 2, 3].map(() => new Promise(resolve => {
324+ const ws = new WebSocket(url);
325+ sockets.push(ws);
326+ ws.onopen = () => resolve(true);
327+ ws.onerror = () => resolve(false);
328+ })));
329+ sockets.forEach(ws => ws.close());
330+ if (opened.every(Boolean)) return performance.now() - started;
331+ await new Promise(resolve => setTimeout(resolve, 100));
332+ }
333+ return null;
287334 }""" )
288- assert result , "disconnected blocked writers leaked all terminal slots"
335+ assert result is not None , "disconnected blocked writers leaked terminal slots"
289336 browser .close ()
337+ deadline = time .monotonic () + 10
338+ holders = _pty_holders (server_pid )
339+ while holders and time .monotonic () < deadline :
340+ time .sleep (0.1 )
341+ holders = _pty_holders (server_pid )
342+ assert not holders , f"sessions outlived their clients: { holders } "
343+ sleepers = []
344+ for pid in _descendants (server_pid ):
345+ try :
346+ if b"sleep" in Path (f"/proc/{ pid } /cmdline" ).read_bytes ():
347+ sleepers .append (pid )
348+ except OSError :
349+ continue
350+ assert not sleepers , f"blocked foreground programs survived: { sleepers } "
0 commit comments