241 lines
5.7 KiB
JavaScript
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);
|
|
});
|