77
88import contextlib
99import os
10+ import queue
1011import signal
1112import subprocess
13+ import threading
1214import time
1315
1416import click
1921from afterpython .utils import find_available_port , find_node_env
2022
2123
24+ def _stream_process_output (
25+ proc : subprocess .Popen ,
26+ output_queue : queue .Queue [str | None ],
27+ ready_event : threading .Event ,
28+ ):
29+ """Forward process output to the terminal and queue startup lines."""
30+ if proc .stdout is None :
31+ if not ready_event .is_set ():
32+ output_queue .put (None )
33+ return
34+
35+ for line in proc .stdout :
36+ click .echo (line , nl = False )
37+ if not ready_event .is_set ():
38+ output_queue .put (line )
39+
40+ if not ready_event .is_set ():
41+ output_queue .put (None )
42+
43+
44+ def _wait_for_myst_server (
45+ proc : subprocess .Popen ,
46+ content_type : str ,
47+ port : int ,
48+ output_queue : queue .Queue [str | None ],
49+ ready_event : threading .Event ,
50+ timeout : int = 300 ,
51+ ):
52+ """Wait until MyST reports that its dev server has started."""
53+ deadline = time .monotonic () + timeout
54+ ready_markers = (
55+ f"Server started on port { port } " ,
56+ f"http://localhost:{ port } " ,
57+ f"http://127.0.0.1:{ port } " ,
58+ )
59+
60+ try :
61+ while time .monotonic () < deadline :
62+ returncode = proc .poll ()
63+ if returncode is not None and output_queue .empty ():
64+ if returncode == 0 :
65+ click .echo (
66+ f"MyST { content_type } server exited before becoming ready"
67+ )
68+ return
69+ raise Exit (returncode )
70+
71+ try :
72+ line = output_queue .get (timeout = 0.5 )
73+ except queue .Empty :
74+ continue
75+
76+ if line is None :
77+ returncode = proc .poll ()
78+ if returncode not in (None , 0 ):
79+ raise Exit (returncode )
80+ click .echo (f"MyST { content_type } server exited before becoming ready" )
81+ return
82+
83+ if any (marker in line for marker in ready_markers ):
84+ click .echo (f"MyST { content_type } server is ready" )
85+ return
86+
87+ raise click .ClickException (
88+ f"Timed out waiting for MyST { content_type } server on port { port } "
89+ )
90+ finally :
91+ ready_event .set ()
92+
93+
2294@click .command (
2395 add_help_option = False , # disable click's --help option so that ap dev --help can work
2496 context_settings = dict (
@@ -189,6 +261,8 @@ def cleanup_processes():
189261 f"PUBLIC_{ content_type .upper ()} _URL=http://localhost:{ myst_port } \n "
190262 )
191263
264+ output_queue : queue .Queue [str | None ] = queue .Queue ()
265+ ready_event = threading .Event ()
192266 myst_process = subprocess .Popen (
193267 [
194268 "ap" ,
@@ -201,15 +275,20 @@ def cleanup_processes():
201275 # New session so cleanup_processes can SIGTERM the whole group and
202276 # take the grandchild `myst start` down with the wrapper.
203277 start_new_session = True ,
204- # stdout=subprocess.DEVNULL, # Suppress output (optional)
205- # stderr=subprocess.DEVNULL, # Suppress errors (optional)
278+ stdout = subprocess .PIPE ,
279+ stderr = subprocess .STDOUT ,
280+ text = True ,
281+ bufsize = 1 ,
206282 )
207283 myst_processes .append (myst_process )
208-
209- # NOTE: MyST internally uses additional ports beyond the one specified by --port.
210- # Without this delay, multiple MyST servers may attempt to bind to the same internal port,
211- # causing "address already in use" errors.
212- time .sleep (3 )
284+ threading .Thread (
285+ target = _stream_process_output ,
286+ args = (myst_process , output_queue , ready_event ),
287+ daemon = True ,
288+ ).start ()
289+ _wait_for_myst_server (
290+ myst_process , content_type , myst_port , output_queue , ready_event
291+ )
213292
214293 postbuild (dev_build = True )
215294
@@ -225,7 +304,7 @@ def cleanup_processes():
225304 click .echo (
226305 "Skipping website dev server (--no-website flag). Run 'pnpm dev' manually in afterpython/_website/ with your custom options."
227306 )
228- if enabled_content_types :
307+ if myst_processes :
229308 # Keep the process running to maintain MyST servers
230309 click .echo ("Press Ctrl+C to stop MyST servers..." )
231310 while True :
0 commit comments