Skip to content

Commit 51307bf

Browse files
fix: run myst servers sequencially and only run project website in the end to make sure everything is built correctly
1 parent 2537f47 commit 51307bf

1 file changed

Lines changed: 87 additions & 8 deletions

File tree

  • src/afterpython/cli/commands

src/afterpython/cli/commands/dev.py

Lines changed: 87 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77

88
import contextlib
99
import os
10+
import queue
1011
import signal
1112
import subprocess
13+
import threading
1214
import time
1315

1416
import click
@@ -19,6 +21,76 @@
1921
from 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

Comments
 (0)