easyai-ai-gateway/scripts/go-watch.mjs

241 lines
5.7 KiB
JavaScript

#!/usr/bin/env node
import { spawn, spawnSync } from 'node:child_process';
import { readdir, stat } from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
const watchRoot = process.env.GO_WATCH_ROOT || process.cwd();
const intervalMs = Number(process.env.GO_WATCH_INTERVAL_MS || 700);
const debounceMs = Number(process.env.GO_WATCH_DEBOUNCE_MS || 250);
const shutdownGraceMs = Number(process.env.GO_WATCH_SHUTDOWN_GRACE_MS || 2500);
const restartDelayMs = Number(process.env.GO_WATCH_RESTART_DELAY_MS || 200);
const commandIndex = process.argv.indexOf('--');
const command = commandIndex >= 0 ? process.argv.slice(commandIndex + 1) : ['go', 'run', './cmd/gateway'];
const prestartCommand = process.env.GO_WATCH_PRESTART || '';
const ignoredDirs = new Set([
'.git',
'.nx',
'coverage',
'dist',
'node_modules',
'tmp',
'vendor',
]);
const watchedExtensions = new Set(['.go', '.mod', '.sum', '.sql']);
if (!command.length) {
console.error('[go-watch] missing command after --');
process.exit(1);
}
let child = null;
let knownSignature = '';
let restarting = false;
let restartQueued = false;
let pendingRestart = null;
let shuttingDown = false;
let stopped = false;
function log(message) {
console.log(`[go-watch] ${message}`);
}
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function collectFiles(dir, files = []) {
let entries = [];
try {
entries = await readdir(dir, { withFileTypes: true });
} catch {
return files;
}
for (const entry of entries) {
if (entry.name.startsWith('.') && entry.name !== '.env') {
continue;
}
if (entry.isDirectory()) {
if (!ignoredDirs.has(entry.name)) {
await collectFiles(path.join(dir, entry.name), files);
}
continue;
}
if (watchedExtensions.has(path.extname(entry.name))) {
files.push(path.join(dir, entry.name));
}
}
return files;
}
async function signature() {
const files = await collectFiles(watchRoot);
files.sort();
const parts = [];
for (const file of files) {
try {
const info = await stat(file);
parts.push(`${file}:${info.mtimeMs}:${info.size}`);
} catch {
parts.push(`${file}:deleted`);
}
}
return parts.join('\n');
}
function start() {
if (stopped) {
return;
}
if (prestartCommand) {
log(`prestart: ${prestartCommand}`);
const result = spawnSync(prestartCommand, {
cwd: watchRoot,
env: process.env,
shell: true,
stdio: 'inherit',
});
if (result.status !== 0) {
log(`prestart failed code=${result.status ?? 'null'} signal=${result.signal ?? 'null'}`);
return;
}
}
log(`starting: ${command.join(' ')}`);
child = spawn(command[0], command.slice(1), {
cwd: watchRoot,
detached: process.platform !== 'win32',
env: process.env,
stdio: 'inherit',
});
child.on('exit', (code, signal) => {
if (stopped || restarting) {
return;
}
log(`process exited code=${code ?? 'null'} signal=${signal ?? 'null'}`);
});
}
function signalProcessTree(processToStop, signal) {
if (!processToStop || processToStop.pid === undefined) {
return;
}
try {
if (process.platform === 'win32') {
processToStop.kill(signal);
} else {
process.kill(-processToStop.pid, signal);
}
} catch (error) {
if (error?.code !== 'ESRCH') {
console.error(`[go-watch] failed to send ${signal}`, error);
}
}
}
function stopProcessTree(processToStop) {
if (!processToStop || processToStop.exitCode !== null || processToStop.signalCode !== null) {
return Promise.resolve();
}
return new Promise((resolve) => {
let done = false;
const finish = () => {
if (done) {
return;
}
done = true;
clearTimeout(forceTimer);
clearTimeout(resolveTimer);
resolve();
};
const forceTimer = setTimeout(() => {
signalProcessTree(processToStop, 'SIGKILL');
}, shutdownGraceMs);
const resolveTimer = setTimeout(finish, shutdownGraceMs + 500);
forceTimer.unref();
resolveTimer.unref();
processToStop.once('exit', finish);
signalProcessTree(processToStop, 'SIGTERM');
});
}
async function restartNow() {
if (restarting) {
restartQueued = true;
return;
}
restarting = true;
log('change detected, restarting');
const previous = child;
child = null;
await stopProcessTree(previous);
if (restartDelayMs > 0) {
await sleep(restartDelayMs);
}
restarting = false;
if (!stopped) {
start();
}
if (restartQueued && !stopped) {
restartQueued = false;
restart();
}
}
function restart() {
if (pendingRestart) {
clearTimeout(pendingRestart);
}
pendingRestart = setTimeout(() => {
pendingRestart = null;
void restartNow();
}, debounceMs);
}
async function poll() {
try {
const nextSignature = await signature();
if (!knownSignature) {
knownSignature = nextSignature;
} else if (nextSignature !== knownSignature) {
knownSignature = nextSignature;
restart();
}
} catch (error) {
console.error('[go-watch] scan failed', error);
}
}
async function main() {
log(`watching ${watchRoot}`);
knownSignature = await signature();
start();
const timer = setInterval(poll, intervalMs);
const shutdown = async (exitCode) => {
if (shuttingDown) {
return;
}
shuttingDown = true;
stopped = true;
clearInterval(timer);
if (pendingRestart) {
clearTimeout(pendingRestart);
}
await stopProcessTree(child);
process.exit(exitCode);
};
process.on('SIGINT', () => {
void shutdown(130);
});
process.on('SIGTERM', () => {
void shutdown(143);
});
}
main().catch((error) => {
console.error('[go-watch] fatal', error);
process.exit(1);
});