Require exclusive Windows debug listener ports

This commit is contained in:
Matthew Stanley
2026-10-25 02:51:03 -07:00
parent a461512a03
commit bd8125e276
2 changed files with 113 additions and 0 deletions
+16
View File
@@ -16972,7 +16972,23 @@ void debug_server_init(int port)
#endif
int yes = 1;
#ifdef _WIN32
/* Winsock SO_REUSEADDR permits two listeners on one address/port, with
* indeterminate routing. Parallel QA commands must reach exactly one
* runtime. Exclusive ownership also refuses an already occupied port.
* https://learn.microsoft.com/windows/win32/winsock/so-exclusiveaddruse */
if (setsockopt(s_listen, SOL_SOCKET, SO_EXCLUSIVEADDRUSE,
(const char *)&yes, sizeof(yes)) != 0) {
s_listen_err = sock_error();
fprintf(stdout, "psxrecomp: debug server exclusive socket option FAILED (%d)\n",
s_listen_err);
sock_close(s_listen);
s_listen = SOCK_INVALID;
return;
}
#else
setsockopt(s_listen, SOL_SOCKET, SO_REUSEADDR, (const char *)&yes, sizeof(yes));
#endif
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
@@ -0,0 +1,97 @@
"""Windows integration gate using real hidden runtime processes.
Provide a disposable working directory/config and explicit BIOS/disc. Neither
the title's release tree nor real memory cards are used. Logs are retained in
--output; child processes are always stopped by their owned process handles.
"""
import argparse
import json
import os
from pathlib import Path
import socket
import subprocess
import time
p=argparse.ArgumentParser()
for name in ('exe','bios','disc','workdir','output'):
p.add_argument('--'+name,type=Path,required=True)
a=p.parse_args()
for name in ('exe','bios','disc','workdir','output'):
setattr(a,name,getattr(a,name).resolve())
if os.name != 'nt':
raise SystemExit('This integration gate targets Winsock on Windows.')
a.output.mkdir(parents=True,exist_ok=True)
children=[]
streams=[]
result={}
def launch(name,port):
out=a.output/(name+'.stdout.log'); err=a.output/(name+'.stderr.log')
stdout=out.open('wb'); stderr=err.open('wb'); streams.extend((stdout,stderr))
cards=a.output/(name+'-cards'); cards.mkdir(exist_ok=True)
env=dict(os.environ,SDL_AUDIODRIVER='dummy',PSX_OVERLAY_AUTOCOMPILE_OFF='1')
child=subprocess.Popen([str(a.exe),'--headless','--no-launcher','--bios',str(a.bios),
'--disc',str(a.disc),'--memcard-dir',str(cards),
'--debug-port',str(port)],cwd=a.workdir,env=env,
creationflags=subprocess.CREATE_NO_WINDOW,stdout=stdout,stderr=stderr)
children.append(child)
return child,out
def wait_log(child,path,needle):
end=time.monotonic()+20
while time.monotonic()<end:
text=path.read_text(errors='replace')
if needle in text: return text
if child.poll() is not None: raise AssertionError((child.returncode,text))
time.sleep(.1)
raise TimeoutError((needle,path.read_text(errors='replace')))
def stop(child):
if child.poll() is None:
child.terminate(); child.wait(timeout=10)
def unused_port():
with socket.socket() as s:
s.bind(('127.0.0.1',0)); return s.getsockname()[1]
def owners(port):
code=f'@(Get-NetTCPConnection -LocalPort {port} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess) | ConvertTo-Json -Compress'
output=subprocess.check_output(['powershell.exe','-NoProfile','-Command',code],text=True).strip()
value=json.loads(output) if output else []
return value if isinstance(value,list) else [value]
try:
# An existing legacy reusable listener must not be hijacked by this runtime.
with socket.socket() as legacy:
legacy.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
legacy.bind(('127.0.0.1',0)); legacy.listen()
port=legacy.getsockname()[1]
child,log=launch('occupied-legacy',port)
wait_log(child,log,f'debug server bind({port}) FAILED')
assert owners(port)==[os.getpid()],owners(port)
result['legacy_listener_preserved']=True
stop(child)
port=unused_port()
first,log=launch('first',port)
wait_log(first,log,f'LISTENING on 127.0.0.1:{port}')
assert owners(port)==[first.pid],owners(port)
with socket.socket() as competitor:
competitor.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
try:
competitor.bind(('127.0.0.1',port))
except OSError as error:
assert error.winerror in (10013,10048),error
result['reusable_competitor_error']=error.winerror
else:
raise AssertionError('A competing SO_REUSEADDR bind unexpectedly succeeded')
second,log=launch('second',port)
wait_log(second,log,f'debug server bind({port}) FAILED')
assert owners(port)==[first.pid],owners(port)
result['second_runtime_refused']=True
stop(second); stop(first)
restarted,log=launch('restarted',port)
wait_log(restarted,log,f'LISTENING on 127.0.0.1:{port}')
assert owners(port)==[restarted.pid],owners(port)
result['restart_after_owned_shutdown']=True
finally:
for child in children: stop(child)
for stream in streams: stream.close()
result['owned_processes_stopped']=all(c.poll() is not None for c in children)
(a.output/'result.json').write_text(json.dumps(result,indent=2)+'\n')
print(json.dumps(result))