@@ -260,107 +260,17 @@ The launcher is a thin process supervisor. Its only jobs:
260260> silently breaking any in-flight transcription. Add `vendor/` (or the
261261> equivalent) to the watcher's ignore list before testing.
262262
263- **Python reference launcher** (adapt to the app's language ):
263+ The launcher logic in pseudocode (full Python and Node.js implementations in [reference.md](reference.md#reference-launchers) ):
264264
265- ```python
266- import os, secrets, socket, subprocess, sys, time, urllib.request
267- from pathlib import Path
268-
269- LEMOND_DIR = Path(__file__).parent / "vendor" / "lemonade"
270- LEMOND_BIN = LEMOND_DIR / ("lemond.exe" if sys.platform == "win32" else "lemond")
271-
272- def _free_port() -> int:
273- with socket.socket() as s:
274- s.bind(("127.0.0.1", 0))
275- return s.getsockname()[1]
276-
277- def start_lemond(retries: int = 3) -> tuple[subprocess.Popen, str, int]:
278- # _free_port releases the socket before lemond binds — another process
279- # can grab the port in that window. Retry with a fresh port on failure.
280- last_err: Exception | None = None
281- for _ in range(retries):
282- port = _free_port()
283- key = secrets.token_urlsafe(32)
284- env = {**os.environ, "LEMONADE_API_KEY": key}
285- proc = subprocess.Popen(
286- [str(LEMOND_BIN), str(LEMOND_DIR), "--port", str(port)],
287- env=env,
288- stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
289- )
290- try:
291- _wait_for_health(port, key, timeout_s=30)
292- return proc, key, port
293- except RuntimeError as e:
294- proc.kill()
295- proc.wait()
296- last_err = e
297- raise RuntimeError(f"lemond failed to start after {retries} attempts") from last_err
298-
299- def _wait_for_health(port: int, key: str, timeout_s: int) -> None:
300- url = f"http://127.0.0.1:{port}/api/v1/health"
301- req = urllib.request.Request(url, headers={"Authorization": f"Bearer {key}"})
302- deadline = time.monotonic() + timeout_s
303- while time.monotonic() < deadline:
304- try:
305- with urllib.request.urlopen(req, timeout=1) as r:
306- if r.status == 200:
307- return
308- except Exception:
309- time.sleep(0.25)
310- raise RuntimeError(f"lemond on port {port} did not become healthy within {timeout_s}s")
311265```
312-
313- ** Node.js reference launcher:**
314-
315- ``` js
316- import { spawn } from " node:child_process" ;
317- import { randomBytes } from " node:crypto" ;
318- import { createServer } from " node:net" ;
319- import path from " node:path" ;
320-
321- const LEMOND_DIR = path .join (import .meta.dirname, " vendor" , " lemonade" );
322- const LEMOND_BIN = path .join (LEMOND_DIR , process .platform === " win32" ? " lemond.exe" : " lemond" );
323-
324- const freePort = () => new Promise ((res ) => {
325- const s = createServer ().listen (0 , " 127.0.0.1" , () => {
326- const { port } = s .address (); s .close (() => res (port));
327- });
328- });
329-
330- // freePort releases the socket before lemond binds — retry with a fresh port on failure.
331- export async function startLemond (retries = 3 ) {
332- let lastErr;
333- for (let i = 0 ; i < retries; i++ ) {
334- const port = await freePort ();
335- const key = randomBytes (32 ).toString (" base64url" );
336- const proc = spawn (LEMOND_BIN , [LEMOND_DIR , " --port" , String (port)], {
337- env: { ... process .env , LEMONADE_API_KEY : key },
338- stdio: [" ignore" , " pipe" , " pipe" ],
339- });
340- try {
341- await waitForHealth (port, key, 30_000 );
342- return { proc, key, port };
343- } catch (e) {
344- proc .kill ();
345- lastErr = e;
346- }
347- }
348- throw new Error (` lemond failed to start after ${ retries} attempts: ${ lastErr? .message }` );
349- }
350-
351- async function waitForHealth(port, key, timeoutMs) {
352- const url = ` http: // 127.0.0.1:${port}/api/v1/health`;
353- const headers = { Authorization: ` Bearer ${ key} ` };
354- const deadline = Date .now () + timeoutMs;
355- while (Date .now () < deadline) {
356- try {
357- const r = await fetch (url, { headers });
358- if (r .ok ) return ;
359- } catch {}
360- await new Promise ((r ) => setTimeout (r, 250 ));
361- }
362- throw new Error (` lemond on port ${ port} did not become healthy within ${ timeoutMs} ms` );
363- }
266+ port = bind("127.0.0.1:0"), read port, close socket
267+ key = random_bytes(32)
268+ proc = spawn(lemond_bin, [ lemond_dir, "--port", port] , env={LEMONADE_API_KEY: key})
269+ poll GET /api/v1/health with Bearer key, retry for 90s, 250ms interval
270+ return proc, key, port
271+
272+ # On failure: kill proc, pick new port, retry up to 3 times
273+ # On app exit: proc.kill() (Windows) / proc.terminate() (Unix), then wait()
364274```
365275
366276## Step 5: Re-point the existing client at `lemond`
0 commit comments