github-actions[bot]
deploy prod from 06dbd16a01ddcfe02b2d936c681e3e4eaa9b141f
8e756fd
Raw
History Blame Contribute Delete
3.35 kB
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`);
}