Spaces:
Sleeping
Sleeping
| import { spawn } from "node:child_process"; | |
| import { existsSync, readdirSync, watch } from "node:fs"; | |
| import path from "node:path"; | |
| const RESTART_DEBOUNCE_MS = 300; | |
| export function resolvePython(repoRoot) { | |
| if (process.env.PYTHON) { | |
| return process.env.PYTHON; | |
| } | |
| const venvPython = path.join(repoRoot, ".venv", "bin", "python"); | |
| if (existsSync(venvPython)) { | |
| return venvPython; | |
| } | |
| return "python3"; | |
| } | |
| function watchPythonTree(rootDir, onChange) { | |
| const watchers = []; | |
| const watchDirectory = (dir) => { | |
| watchers.push( | |
| watch(dir, (_event, filename) => { | |
| if (!filename || !filename.endsWith(".py")) { | |
| return; | |
| } | |
| onChange(path.join(dir, filename)); | |
| }), | |
| ); | |
| for (const entry of readdirSync(dir, { withFileTypes: true })) { | |
| if (entry.isDirectory()) { | |
| watchDirectory(path.join(dir, entry.name)); | |
| } | |
| } | |
| }; | |
| watchDirectory(rootDir); | |
| return () => { | |
| for (const watcher of watchers) { | |
| watcher.close(); | |
| } | |
| }; | |
| } | |
| /** | |
| * Run the Chess Tutor API and restart it when Python sources change. | |
| */ | |
| export function startApiWithReload(repoRoot, { port = "8765", watchFiles = true } = {}) { | |
| const python = resolvePython(repoRoot); | |
| const srcDir = path.join(repoRoot, "src", "chess_tutor"); | |
| let proc = null; | |
| let stopping = false; | |
| let restartPending = false; | |
| let debounceTimer = null; | |
| let closeWatcher = null; | |
| function start() { | |
| proc = spawn(python, ["-m", "chess_tutor.web", "--api-only", "--port", port], { | |
| cwd: repoRoot, | |
| stdio: "inherit", | |
| }); | |
| proc.on("exit", (code) => { | |
| proc = null; | |
| if (stopping) { | |
| return; | |
| } | |
| if (restartPending) { | |
| restartPending = false; | |
| console.log("\n[dev] API reloaded.\n"); | |
| start(); | |
| return; | |
| } | |
| if (code !== 0 && code !== null) { | |
| process.exit(code); | |
| } | |
| }); | |
| } | |
| function scheduleRestart(changedPath) { | |
| if (stopping) { | |
| return; | |
| } | |
| clearTimeout(debounceTimer); | |
| debounceTimer = setTimeout(() => { | |
| const rel = path.relative(repoRoot, changedPath); | |
| console.log(`\n[dev] Python change detected (${rel}) — reloading API…`); | |
| restartPending = true; | |
| if (proc) { | |
| proc.kill("SIGTERM"); | |
| } else { | |
| start(); | |
| } | |
| }, RESTART_DEBOUNCE_MS); | |
| } | |
| if (watchFiles && existsSync(srcDir)) { | |
| closeWatcher = watchPythonTree(srcDir, scheduleRestart); | |
| console.log(`[dev] Python hot reload: watching ${srcDir}`); | |
| } | |
| start(); | |
| return { | |
| port, | |
| stop() { | |
| stopping = true; | |
| clearTimeout(debounceTimer); | |
| closeWatcher?.(); | |
| if (proc) { | |
| proc.kill("SIGTERM"); | |
| } | |
| }, | |
| }; | |
| } | |
| /** Poll until the API responds (avoids Vite proxy ECONNREFUSED on startup). */ | |
| export async function waitForApiReady(port = "8765", { timeoutMs = 60000, intervalMs = 200 } = {}) { | |
| const url = `http://127.0.0.1:${port}/api/version`; | |
| const deadline = Date.now() + timeoutMs; | |
| while (Date.now() < deadline) { | |
| try { | |
| const res = await fetch(url); | |
| if (res.ok) { | |
| return; | |
| } | |
| } catch { | |
| // API not listening yet | |
| } | |
| await new Promise((resolve) => setTimeout(resolve, intervalMs)); | |
| } | |
| throw new Error(`API did not become ready at ${url} within ${timeoutMs / 1000}s`); | |
| } | |