Skip to content

Commit 6b90438

Browse files
use async process management to avoid blocking event loop on sea startup and shutdown
1 parent 380f5e0 commit 6b90438

1 file changed

Lines changed: 51 additions & 19 deletions

File tree

src/stagehand/lib/sea_server.py

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,45 @@ def _terminate_process(proc: subprocess.Popen[bytes]) -> None:
6565
pass
6666

6767

68+
def _terminate_process_async_atexit(proc: asyncio.subprocess.Process) -> None:
69+
if proc.returncode is not None:
70+
return
71+
72+
try:
73+
if sys.platform != "win32":
74+
os.killpg(proc.pid, signal.SIGTERM)
75+
else:
76+
proc.terminate()
77+
except Exception:
78+
pass
79+
80+
81+
async def _terminate_process_async(proc: asyncio.subprocess.Process) -> None:
82+
if proc.returncode is not None:
83+
return
84+
85+
try:
86+
if sys.platform != "win32":
87+
os.killpg(proc.pid, signal.SIGTERM)
88+
else:
89+
proc.terminate()
90+
await asyncio.wait_for(proc.wait(), timeout=3)
91+
return
92+
except Exception:
93+
pass
94+
95+
try:
96+
if sys.platform != "win32":
97+
os.killpg(proc.pid, signal.SIGKILL)
98+
else:
99+
proc.kill()
100+
finally:
101+
try:
102+
await asyncio.wait_for(proc.wait(), timeout=3)
103+
except Exception:
104+
pass
105+
106+
68107
def _wait_ready_sync(*, base_url: str, timeout_s: float) -> None:
69108
deadline = time.monotonic() + timeout_s
70109
with httpx.Client(timeout=1.0) as client:
@@ -111,6 +150,7 @@ def __init__(
111150
self._async_lock = asyncio.Lock()
112151

113152
self._proc: subprocess.Popen[bytes] | None = None
153+
self._async_proc: asyncio.subprocess.Process | None = None
114154
self._base_url: str | None = None
115155
self._atexit_registered: bool = False
116156

@@ -150,12 +190,12 @@ def ensure_running_sync(self) -> str:
150190

151191
async def ensure_running_async(self) -> str:
152192
async with self._async_lock:
153-
if self._proc is not None and self._proc.poll() is None and self._base_url is not None:
193+
if self._async_proc is not None and self._async_proc.returncode is None and self._base_url is not None:
154194
return self._base_url
155195

156196
base_url, proc = await self._start_async()
157197
self._base_url = base_url
158-
self._proc = proc
198+
self._async_proc = proc
159199
return base_url
160200

161201
def close(self) -> None:
@@ -174,10 +214,10 @@ async def aclose(self) -> None:
174214
return
175215

176216
async with self._async_lock:
177-
if self._proc is None:
217+
if self._async_proc is None:
178218
return
179-
_terminate_process(self._proc)
180-
self._proc = None
219+
await _terminate_process_async(self._async_proc)
220+
self._async_proc = None
181221
self._base_url = None
182222

183223
def _start_sync(self) -> tuple[str, subprocess.Popen[bytes]]:
@@ -219,7 +259,7 @@ def _start_sync(self) -> tuple[str, subprocess.Popen[bytes]]:
219259

220260
return base_url, proc
221261

222-
async def _start_async(self) -> tuple[str, subprocess.Popen[bytes]]:
262+
async def _start_async(self) -> tuple[str, asyncio.subprocess.Process]:
223263
if not self._binary_path.exists():
224264
raise FileNotFoundError(
225265
f"Stagehand SEA binary not found at {self._binary_path}. "
@@ -230,30 +270,22 @@ async def _start_async(self) -> tuple[str, subprocess.Popen[bytes]]:
230270
base_url = _build_base_url(host=self._config.host, port=port)
231271
proc_env = self._build_process_env(port=port)
232272

233-
preexec_fn = None
234-
creationflags = 0
235-
if sys.platform != "win32":
236-
preexec_fn = os.setsid
237-
else:
238-
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
239-
240-
proc = subprocess.Popen(
241-
[str(self._binary_path)],
273+
proc = await asyncio.create_subprocess_exec(
274+
str(self._binary_path),
242275
env=proc_env,
243276
stdout=None,
244277
stderr=None,
245-
preexec_fn=preexec_fn,
246-
creationflags=creationflags,
278+
start_new_session=True,
247279
)
248280

249281
if not self._atexit_registered:
250-
atexit.register(_terminate_process, proc)
282+
atexit.register(_terminate_process_async_atexit, proc)
251283
self._atexit_registered = True
252284

253285
try:
254286
await _wait_ready_async(base_url=base_url, timeout_s=self._config.ready_timeout_s)
255287
except Exception:
256-
_terminate_process(proc)
288+
await _terminate_process_async(proc)
257289
raise
258290

259291
return base_url, proc

0 commit comments

Comments
 (0)