diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index abca84bd..8ea0a0b9 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -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)); diff --git a/runtime/tests/check_debug_port_exclusivity.py b/runtime/tests/check_debug_port_exclusivity.py new file mode 100644 index 00000000..2632190f --- /dev/null +++ b/runtime/tests/check_debug_port_exclusivity.py @@ -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()