Files
iddaai-be/src/scripts/run-full-stack.ts
T
fahricansecer 182f4aae16
Deploy Iddaai Backend / build-and-deploy (push) Successful in 33s
first (part 3: src directory)
2026-04-16 15:12:27 +03:00

363 lines
8.9 KiB
TypeScript

import { spawn, type ChildProcess } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { access } from 'node:fs/promises';
import net from 'node:net';
import path from 'node:path';
import process from 'node:process';
interface ManagedProcess {
readonly name: string;
readonly child: ChildProcess;
}
const ROOT_DIR = process.cwd();
const AI_ENGINE_DIR = path.join(ROOT_DIR, 'ai-engine');
loadEnvFile(path.join(ROOT_DIR, '.env'));
const DEFAULT_AI_URL = process.env.AI_ENGINE_URL ?? 'http://127.0.0.1:8000';
const DEFAULT_API_PORT = Number(process.env.PORT ?? '3005');
const AI_ENGINE_PORT = resolveAiPort(DEFAULT_AI_URL);
const AI_START_TIMEOUT_MS = 120_000;
const NEST_START_TIMEOUT_MS = 90_000;
const HEALTH_POLL_INTERVAL_MS = 1_000;
let nestProcess: ManagedProcess | null = null;
let aiProcess: ManagedProcess | null = null;
let shuttingDown = false;
async function main(): Promise<void> {
ensureWindowsOrUnixShellAwareness();
const aiHealthUrl = `${DEFAULT_AI_URL}/health`;
const aiHost = resolveHost(DEFAULT_AI_URL);
const aiAlreadyHealthy = await isHealthy(aiHealthUrl);
if (aiAlreadyHealthy) {
log(`AI engine already running at ${aiHealthUrl}`);
} else {
const aiPortBusy = await isPortInUse(aiHost, AI_ENGINE_PORT);
if (aiPortBusy) {
throw new Error(
`AI engine port ${AI_ENGINE_PORT} is already in use but ${aiHealthUrl} is not healthy`,
);
}
const pythonCommand = await resolvePythonCommand();
aiProcess = {
name: 'ai-engine',
child: spawn(
pythonCommand.command,
[
...pythonCommand.args,
'-m',
'uvicorn',
'main:app',
'--host',
'0.0.0.0',
'--port',
String(AI_ENGINE_PORT),
...resolveAiExtraArgs(),
],
{
cwd: AI_ENGINE_DIR,
stdio: 'inherit',
env: {
...process.env,
PYTHONUNBUFFERED: '1',
PORT: String(AI_ENGINE_PORT),
},
},
),
};
attachExitHandlers(aiProcess);
log(`Waiting for AI engine health at ${aiHealthUrl}`);
await waitForHealth(aiHealthUrl, AI_START_TIMEOUT_MS);
log('AI engine is ready');
}
const nestHealthUrl = `http://127.0.0.1:${DEFAULT_API_PORT}/api/health/live`;
const nestAlreadyHealthy = await isHealthy(nestHealthUrl);
if (nestAlreadyHealthy) {
log(`NestJS already running at ${nestHealthUrl}`);
} else {
const nestPortBusy = await isPortInUse('127.0.0.1', DEFAULT_API_PORT);
if (nestPortBusy) {
throw new Error(
`NestJS port ${DEFAULT_API_PORT} is already in use but ${nestHealthUrl} is not healthy`,
);
}
nestProcess = {
name: 'nest',
child: spawnNestProcess(),
};
attachExitHandlers(nestProcess);
log(`Waiting for NestJS health at ${nestHealthUrl}`);
await waitForHealth(nestHealthUrl, NEST_START_TIMEOUT_MS);
log('NestJS is ready');
}
log('Full stack is running');
}
function ensureWindowsOrUnixShellAwareness(): void {
process.on('SIGINT', () => {
void shutdown('SIGINT');
});
process.on('SIGTERM', () => {
void shutdown('SIGTERM');
});
process.on('uncaughtException', (error: Error) => {
console.error('[full:run] Uncaught exception:', error);
void shutdown('uncaughtException', 1);
});
process.on('unhandledRejection', (reason: unknown) => {
console.error('[full:run] Unhandled rejection:', reason);
void shutdown('unhandledRejection', 1);
});
}
function resolveNestStartScript(): string {
return process.env.FULL_RUN_NEST_SCRIPT ?? 'start:dev';
}
function resolveAiExtraArgs(): string[] {
return process.env.FULL_RUN_AI_RELOAD === 'true' ? ['--reload'] : [];
}
function spawnNestProcess(): ChildProcess {
const nestScript = resolveNestStartScript();
if (process.platform === 'win32') {
return spawn('cmd.exe', ['/d', '/s', '/c', `npm run ${nestScript}`], {
cwd: ROOT_DIR,
stdio: 'inherit',
env: process.env,
});
}
return spawn('npm', ['run', nestScript], {
cwd: ROOT_DIR,
stdio: 'inherit',
env: process.env,
});
}
async function resolvePythonCommand(): Promise<{
command: string;
args: string[];
}> {
const explicitPython = process.env.AI_ENGINE_PYTHON?.trim();
if (explicitPython) {
return { command: explicitPython, args: [] };
}
const localVenvPython =
process.platform === 'win32'
? path.join(AI_ENGINE_DIR, 'venv', 'Scripts', 'python.exe')
: path.join(AI_ENGINE_DIR, 'venv', 'bin', 'python');
if (await pathExists(localVenvPython)) {
return { command: localVenvPython, args: [] };
}
return { command: process.platform === 'win32' ? 'python' : 'python3', args: [] };
}
function resolveAiPort(aiUrl: string): number {
const parsedUrl = new URL(aiUrl);
if (parsedUrl.port) {
return Number(parsedUrl.port);
}
return parsedUrl.protocol === 'https:' ? 443 : 80;
}
async function pathExists(targetPath: string): Promise<boolean> {
try {
await access(targetPath);
return true;
} catch {
return false;
}
}
function resolveHost(url: string): string {
const parsedUrl = new URL(url);
return parsedUrl.hostname;
}
function attachExitHandlers(managedProcess: ManagedProcess): void {
managedProcess.child.on('exit', (code: number | null, signal: NodeJS.Signals | null) => {
if (shuttingDown) {
return;
}
const detail =
signal !== null
? `signal=${signal}`
: `code=${code ?? 'unknown'}`;
console.error(`[full:run] ${managedProcess.name} exited unexpectedly (${detail})`);
void shutdown(`${managedProcess.name}-exit`, code ?? 1);
});
managedProcess.child.on('error', (error: Error) => {
if (shuttingDown) {
return;
}
console.error(`[full:run] Failed to start ${managedProcess.name}:`, error);
void shutdown(`${managedProcess.name}-error`, 1);
});
}
async function waitForHealth(url: string, timeoutMs: number): Promise<void> {
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
try {
const response = await fetch(url);
if (response.ok) {
return;
}
} catch {
// Service is still booting.
}
await delay(HEALTH_POLL_INTERVAL_MS);
}
throw new Error(`Health check timed out: ${url}`);
}
async function isHealthy(url: string): Promise<boolean> {
try {
const response = await fetch(url);
return response.ok;
} catch {
return false;
}
}
async function isPortInUse(host: string, port: number): Promise<boolean> {
return new Promise((resolve) => {
const socket = net.createConnection({ host, port });
socket.once('connect', () => {
socket.destroy();
resolve(true);
});
socket.once('error', () => {
resolve(false);
});
});
}
function delay(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function shutdown(reason: string, exitCode = 0): Promise<void> {
if (shuttingDown) {
return;
}
shuttingDown = true;
log(`Shutting down stack (${reason})`);
await stopProcess(nestProcess);
await stopProcess(aiProcess);
process.exit(exitCode);
}
async function stopProcess(managedProcess: ManagedProcess | null): Promise<void> {
if (!managedProcess) {
return;
}
const { child, name } = managedProcess;
if (child.killed || child.exitCode !== null) {
return;
}
child.kill('SIGTERM');
const stopped = await waitForProcessExit(child, 10_000);
if (!stopped) {
console.warn(`[full:run] ${name} did not stop gracefully, forcing termination`);
child.kill('SIGKILL');
await waitForProcessExit(child, 5_000);
}
}
function waitForProcessExit(child: ChildProcess, timeoutMs: number): Promise<boolean> {
return new Promise((resolve) => {
const timeout = setTimeout(() => {
cleanup();
resolve(false);
}, timeoutMs);
const onExit = () => {
cleanup();
resolve(true);
};
const cleanup = () => {
clearTimeout(timeout);
child.off('exit', onExit);
};
child.once('exit', onExit);
});
}
function log(message: string): void {
console.log(`[full:run] ${message}`);
}
function loadEnvFile(envPath: string): void {
try {
const content = readFileSync(envPath, 'utf8');
const lines = content.split(/\r?\n/u);
lines.forEach((line) => {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) {
return;
}
const separatorIndex = trimmed.indexOf('=');
if (separatorIndex === -1) {
return;
}
const key = trimmed.slice(0, separatorIndex).trim();
const rawValue = trimmed.slice(separatorIndex + 1).trim();
const normalizedValue = rawValue.replace(/^['"]|['"]$/gu, '');
if (!process.env[key]) {
process.env[key] = normalizedValue;
}
});
} catch {
// .env is optional for this script.
}
}
void main().catch((error: Error) => {
console.error('[full:run] Startup failed:', error);
void shutdown('startup-failed', 1);
});