2026-09-13 16:38:32 +00:00
|
|
|
"""Headless browser shutdown regression; never opens visible windows or external sites."""
|
|
|
|
|
import json, os, subprocess, time
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
HELPER=Path(__file__).resolve().parents[1]/'tools/playwright/browser_helper.mjs'
|
|
|
|
|
def processes():
|
|
|
|
|
rows={}
|
|
|
|
|
for line in subprocess.check_output(['ps','-axo','pid=,ppid=,command='],text=True).splitlines():
|
|
|
|
|
pid,parent,cmd=line.strip().split(None,2);rows[int(pid)]=(int(parent),cmd)
|
|
|
|
|
return rows
|
|
|
|
|
def main():
|
2026-09-15 05:20:44 +00:00
|
|
|
env={**os.environ,'LAZYBOY_BROWSER_HEADED':'0'};env.pop('LAZYBOY_BROWSER_PROFILE',None)
|
2026-09-13 16:38:32 +00:00
|
|
|
for mode in ('close','eof','signal','single'):
|
|
|
|
|
args=['node',str(HELPER)]
|
|
|
|
|
if mode=='single':args+=['--cmd',json.dumps({'id':'1','op':'navigate','url':'data:text/html,cleanup fixture'})]
|
|
|
|
|
p=subprocess.Popen(args,env=env,stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True)
|
|
|
|
|
tracked=set()
|
|
|
|
|
try:
|
|
|
|
|
if mode=='single':
|
|
|
|
|
while p.poll() is None:
|
|
|
|
|
tracked.update(pid for pid,(parent,cmd) in processes().items() if parent==p.pid and ('chrome' in cmd.lower() or 'chromium' in cmd.lower()))
|
|
|
|
|
time.sleep(.02)
|
|
|
|
|
assert p.returncode==0,p.stderr.read()
|
|
|
|
|
else:
|
|
|
|
|
p.stdin.write(json.dumps({'id':'1','op':'navigate','url':'data:text/html,cleanup fixture'})+'\n');p.stdin.flush()
|
|
|
|
|
assert json.loads(p.stdout.readline())['ok']
|
|
|
|
|
tracked={pid for pid,(parent,cmd) in processes().items() if parent==p.pid and ('chrome' in cmd.lower() or 'chromium' in cmd.lower())}
|
|
|
|
|
assert tracked,'must observe actual browser process'
|
|
|
|
|
if mode=='close':
|
|
|
|
|
p.stdin.write('{"id":"2","op":"close"}\n');p.stdin.flush();assert json.loads(p.stdout.readline())['ok'];p.stdin.close()
|
|
|
|
|
elif mode=='eof':p.stdin.close()
|
|
|
|
|
else:p.terminate()
|
|
|
|
|
p.wait(timeout=10)
|
|
|
|
|
deadline=time.monotonic()+5
|
|
|
|
|
while tracked.intersection(processes()) and time.monotonic()<deadline:time.sleep(.05)
|
|
|
|
|
assert not tracked.intersection(processes()),(mode,tracked)
|
|
|
|
|
print('PASS browser processes exit on',mode,flush=True)
|
|
|
|
|
finally:
|
|
|
|
|
if p.poll() is None:p.terminate();p.wait(timeout=10)
|
|
|
|
|
if __name__=='__main__':main()
|