Skip to content

Commit c58d9f7

Browse files
use async process management to avoid blocking event loop on sea startup and shutdown
1 parent b43ecf9 commit c58d9f7

File tree

1 file changed

+51
-19
lines changed

1 file changed

+51
-19
lines changed

src/stagehand/_custom/sea_server.py

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

9191

92+
def _terminate_process_async_atexit(proc: asyncio.subprocess.Process) -> None:
93+
if proc.returncode is not None:
94+
return
95+
96+
try:
97+
if sys.platform != "win32":
98+
os.killpg(proc.pid, signal.SIGTERM)
99+
else:
100+
proc.terminate()
101+
except Exception:
102+
pass
103+
104+
105+
async def _terminate_process_async(proc: asyncio.subprocess.Process) -> None:
106+
if proc.returncode is not None:
107+
return
108+
109+
try:
110+
if sys.platform != "win32":
111+
os.killpg(proc.pid, signal.SIGTERM)
112+
else:
113+
proc.terminate()
114+
await asyncio.wait_for(proc.wait(), timeout=3)
115+
return
116+
except Exception:
117+
pass
118+
119+
try:
120+
if sys.platform != "win32":
121+
os.killpg(proc.pid, signal.SIGKILL)
122+
else:
123+
proc.kill()
124+
finally:
125+
try:
126+
await asyncio.wait_for(proc.wait(), timeout=3)
127+
except Exception:
128+
pass
129+
130+
92131
def _wait_ready_sync(*, base_url: str, timeout_s: float) -> None:
93132
deadline = time.monotonic() + timeout_s
94133
with httpx.Client(timeout=1.0) as client:
@@ -138,6 +177,7 @@ def __init__(
138177
self._async_lock = asyncio.Lock()
139178

140179
self._proc: subprocess.Popen[bytes] | None = None
180+
self._async_proc: asyncio.subprocess.Process | None = None
141181
self._base_url: str | None = None
142182
self._atexit_registered: bool = False
143183

@@ -177,12 +217,12 @@ def ensure_running_sync(self) -> str:
177217

178218
async def ensure_running_async(self) -> str:
179219
async with self._async_lock:
180-
if self._proc is not None and self._proc.poll() is None and self._base_url is not None:
220+
if self._async_proc is not None and self._async_proc.returncode is None and self._base_url is not None:
181221
return self._base_url
182222

183223
base_url, proc = await self._start_async()
184224
self._base_url = base_url
185-
self._proc = proc
225+
self._async_proc = proc
186226
return base_url
187227

188228
def close(self) -> None:
@@ -201,10 +241,10 @@ async def aclose(self) -> None:
201241
return
202242

203243
async with self._async_lock:
204-
if self._proc is None:
244+
if self._async_proc is None:
205245
return
206-
_terminate_process(self._proc)
207-
self._proc = None
246+
await _terminate_process_async(self._async_proc)
247+
self._async_proc = None
208248
self._base_url = None
209249

210250
def _start_sync(self) -> tuple[str, subprocess.Popen[bytes]]:
@@ -246,7 +286,7 @@ def _start_sync(self) -> tuple[str, subprocess.Popen[bytes]]:
246286

247287
return base_url, proc
248288

249-
async def _start_async(self) -> tuple[str, subprocess.Popen[bytes]]:
289+
async def _start_async(self) -> tuple[str, asyncio.subprocess.Process]:
250290
if not self._binary_path.exists():
251291
raise FileNotFoundError(
252292
f"Stagehand SEA binary not found at {self._binary_path}. "
@@ -257,30 +297,22 @@ async def _start_async(self) -> tuple[str, subprocess.Popen[bytes]]:
257297
base_url = _build_base_url(host=self._config.host, port=port)
258298
proc_env = self._build_process_env(port=port)
259299

260-
preexec_fn = None
261-
creationflags = 0
262-
if sys.platform != "win32":
263-
preexec_fn = os.setsid
264-
else:
265-
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
266-
267-
proc = subprocess.Popen(
268-
[str(self._binary_path)],
300+
proc = await asyncio.create_subprocess_exec(
301+
str(self._binary_path),
269302
env=proc_env,
270303
stdout=None,
271304
stderr=None,
272-
preexec_fn=preexec_fn,
273-
creationflags=creationflags,
305+
start_new_session=True,
274306
)
275307

276308
if not self._atexit_registered:
277-
atexit.register(_terminate_process, proc)
309+
atexit.register(_terminate_process_async_atexit, proc)
278310
self._atexit_registered = True
279311

280312
try:
281313
await _wait_ready_async(base_url=base_url, timeout_s=self._config.ready_timeout_s)
282314
except Exception:
283-
_terminate_process(proc)
315+
await _terminate_process_async(proc)
284316
raise
285317

286318
return base_url, proc

0 commit comments

Comments
 (0)