Remove outdated documentation files for companies and migration guide; implement server session management features including server assignment and session status updates; enhance database schema for servers and server sessions with new fields and validation; add auto-assign functionality for unassigned sessions.
This commit is contained in:
@@ -0,0 +1,691 @@
|
||||
import got, { RequestError } from "got";
|
||||
import os from "os";
|
||||
import { execSync, spawn, ChildProcess } from "child_process";
|
||||
import { existsSync } from "fs";
|
||||
|
||||
// Конфигурация
|
||||
const API_URL = process.env.API_URL || "http://localhost:3000";
|
||||
const SERVER_TYPE = (process.env.SERVER_TYPE || "stream") as "stream" | "local";
|
||||
const SERVER_LOCATION =
|
||||
(process.env.SERVER_LOCATION as "ru1" | "uae1" | undefined) || undefined;
|
||||
const SERVER_TIER =
|
||||
(process.env.SERVER_TIER as "demo" | "prod" | undefined) || undefined;
|
||||
const BRANCH_ID = process.env.BRANCH_ID || undefined;
|
||||
const LOCAL_IP = getLocalIp();
|
||||
const HOSTNAME = os.hostname();
|
||||
const REGISTER_INTERVAL_MS = parseInt(
|
||||
process.env.REGISTER_INTERVAL_MS || "30000",
|
||||
10
|
||||
); // 30 секунд по умолчанию
|
||||
const GPU_UPDATE_INTERVAL_MS = parseInt(
|
||||
process.env.GPU_UPDATE_INTERVAL_MS || "1000",
|
||||
10
|
||||
); // 1 секунда по умолчанию
|
||||
const SESSION_CHECK_INTERVAL_MS = parseInt(
|
||||
process.env.SESSION_CHECK_INTERVAL_MS || "1000",
|
||||
10
|
||||
); // 1 секунда по умолчанию
|
||||
|
||||
// ID зарегистрированного сервера (заполняется после регистрации)
|
||||
let SERVER_ID: string | null = null;
|
||||
|
||||
// Карта активных процессов: sessionId -> процесс приложения
|
||||
const activeProcesses = new Map<string, ChildProcess>();
|
||||
|
||||
// Карта сессий в процессе запуска/остановки для предотвращения дублирования
|
||||
const processingSessions = new Set<string>();
|
||||
|
||||
// Карта последнего времени логирования запланированных сессий (для уменьшения спама в логах)
|
||||
const lastScheduledLogTime = new Map<string, number>();
|
||||
|
||||
interface ServerRegistrationData {
|
||||
localIp: string;
|
||||
hostname: string;
|
||||
type: "stream" | "local";
|
||||
gpuFreeMb: number;
|
||||
branchId?: string;
|
||||
location?: "ru1" | "uae1";
|
||||
tier?: "demo" | "prod";
|
||||
}
|
||||
|
||||
interface ServerRegistrationResponse {
|
||||
server: {
|
||||
id: string;
|
||||
localIp: string;
|
||||
hostname: string;
|
||||
type: "stream" | "local";
|
||||
gpuFreeMb: number;
|
||||
branchId?: string;
|
||||
location?: "ru1" | "uae1";
|
||||
tier?: "demo" | "prod";
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
registered: boolean;
|
||||
}
|
||||
|
||||
interface SessionData {
|
||||
id: string;
|
||||
serverId: string | null; // Nullable - для stream сессий назначается динамически
|
||||
appId: string;
|
||||
userId: string;
|
||||
startAt: string;
|
||||
endAt: string | null;
|
||||
appPid: number | null;
|
||||
cirrusPid: number | null;
|
||||
mode: "stream" | "local";
|
||||
status: "starting" | "started" | "ending" | "ended";
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
app: {
|
||||
id: string;
|
||||
name: string;
|
||||
title: string;
|
||||
gpuLimitMb: number | null;
|
||||
psVersion: number | null;
|
||||
};
|
||||
user: {
|
||||
id: string;
|
||||
email: string;
|
||||
role: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить локальный IP адрес
|
||||
*/
|
||||
function getLocalIp(): string {
|
||||
const interfaces = os.networkInterfaces();
|
||||
|
||||
for (const name of Object.keys(interfaces)) {
|
||||
const netInterface = interfaces[name];
|
||||
if (!netInterface) continue;
|
||||
|
||||
for (const iface of netInterface) {
|
||||
// Пропустить внутренние и non-IPv4 адреса
|
||||
if (iface.family === "IPv4" && !iface.internal) {
|
||||
return iface.address;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "127.0.0.1";
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить свободную память GPU через nvidia-smi
|
||||
* Возвращает количество свободной памяти в МБ
|
||||
* Выбрасывает ошибку, если не удалось получить данные
|
||||
*/
|
||||
function getGpuFreeMb(): number {
|
||||
try {
|
||||
// Выполняем nvidia-smi с форматом вывода только свободной памяти
|
||||
// --query-gpu=memory.free - запрашиваем свободную память
|
||||
// --format=csv,noheader,nounits - CSV формат без заголовков и единиц измерения
|
||||
const output = execSync(
|
||||
"nvidia-smi --query-gpu=memory.free --format=csv,noheader,nounits",
|
||||
{
|
||||
encoding: "utf-8",
|
||||
timeout: 5000, // 5 секунд таймаут
|
||||
}
|
||||
);
|
||||
|
||||
// Парсим вывод (может быть несколько GPU, берём первый)
|
||||
const lines = output.trim().split("\n");
|
||||
if (lines.length > 0 && lines[0]) {
|
||||
const freeMb = parseInt(lines[0].trim(), 10);
|
||||
if (!isNaN(freeMb) && freeMb >= 0) {
|
||||
return freeMb;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Не удалось распарсить вывод nvidia-smi");
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] ❌ Критическая ошибка при получении данных GPU:`,
|
||||
error instanceof Error ? error.message : error
|
||||
);
|
||||
throw new Error(
|
||||
`Невозможно получить данные о GPU через nvidia-smi: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Регистрация сервера на главном сервере
|
||||
*/
|
||||
async function registerServer(isRecursive: boolean = false): Promise<void> {
|
||||
// Валидация для stream-серверов
|
||||
if (SERVER_TYPE === "stream" && !SERVER_LOCATION) {
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] ❌ Ошибка: для stream-серверов обязательно должен быть указан SERVER_LOCATION`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Валидация для local-серверов
|
||||
if (SERVER_TYPE === "local" && !BRANCH_ID) {
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] ❌ Ошибка: для local-серверов обязательно должен быть указан BRANCH_ID`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Установить tier по умолчанию для stream-серверов
|
||||
const tier = SERVER_TYPE === "stream" && !SERVER_TIER ? "demo" : SERVER_TIER;
|
||||
|
||||
// Получаем актуальное значение свободной GPU памяти
|
||||
const gpuFreeMb = getGpuFreeMb();
|
||||
|
||||
const registrationData: ServerRegistrationData = {
|
||||
localIp: LOCAL_IP,
|
||||
hostname: HOSTNAME,
|
||||
type: SERVER_TYPE,
|
||||
gpuFreeMb: gpuFreeMb,
|
||||
branchId: BRANCH_ID,
|
||||
location: SERVER_LOCATION,
|
||||
tier: tier,
|
||||
};
|
||||
|
||||
console.log(`[${new Date().toISOString()}] Регистрация сервера...`);
|
||||
console.log("Данные:", JSON.stringify(registrationData, null, 2));
|
||||
|
||||
try {
|
||||
const response = await got
|
||||
.post(`${API_URL}/servers/register`, {
|
||||
json: registrationData,
|
||||
timeout: {
|
||||
request: 10000, // 10 секунд таймаут
|
||||
},
|
||||
retry: {
|
||||
limit: 3,
|
||||
methods: ["POST"],
|
||||
statusCodes: [408, 413, 429, 500, 502, 503, 504],
|
||||
},
|
||||
})
|
||||
.json<ServerRegistrationResponse>();
|
||||
|
||||
// Сохраняем ID сервера для дальнейших обновлений
|
||||
SERVER_ID = response.server.id;
|
||||
|
||||
if (response.registered) {
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ✅ Сервер успешно зарегистрирован`
|
||||
);
|
||||
console.log("ID сервера:", response.server.id);
|
||||
} else {
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] 🔄 Информация о сервере обновлена`
|
||||
);
|
||||
}
|
||||
|
||||
// При первом запуске запускаем обновление GPU и проверку сессий после успешной регистрации
|
||||
if (!isRecursive && SERVER_ID) {
|
||||
updateGpuMemory();
|
||||
checkSessions();
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] ❌ Ошибка регистрации сервера:`,
|
||||
error
|
||||
);
|
||||
|
||||
if (error instanceof RequestError) {
|
||||
console.error("Детали ошибки:", {
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
});
|
||||
} else if (error instanceof Error) {
|
||||
console.error("Ошибка:", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Планируем следующую регистрацию после завершения текущей
|
||||
setTimeout(() => registerServer(true), REGISTER_INTERVAL_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить информацию о свободной памяти GPU на сервере
|
||||
*/
|
||||
async function updateGpuMemory(): Promise<void> {
|
||||
try {
|
||||
// Получаем актуальное значение свободной GPU памяти
|
||||
const gpuFreeMb = getGpuFreeMb();
|
||||
|
||||
await got.patch(`${API_URL}/servers/${SERVER_ID}/gpu`, {
|
||||
json: { gpuFreeMb },
|
||||
timeout: {
|
||||
request: 5000, // 5 секунд таймаут
|
||||
},
|
||||
retry: {
|
||||
limit: 2,
|
||||
methods: ["PATCH"],
|
||||
statusCodes: [408, 429, 500, 502, 503, 504],
|
||||
},
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] 🎮 GPU память обновлена: ${gpuFreeMb} MB`
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] ❌ Ошибка обновления GPU памяти:`,
|
||||
error instanceof RequestError ? error.message : error
|
||||
);
|
||||
}
|
||||
|
||||
// Планируем следующее обновление после завершения текущего
|
||||
setTimeout(() => updateGpuMemory(), GPU_UPDATE_INTERVAL_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить сессии для этого сервера
|
||||
*/
|
||||
async function fetchSessions(): Promise<SessionData[]> {
|
||||
if (!SERVER_ID) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await got
|
||||
.get(`${API_URL}/servers/${SERVER_ID}/sessions`, {
|
||||
timeout: {
|
||||
request: 5000,
|
||||
},
|
||||
})
|
||||
.json<{ sessions: SessionData[] }>();
|
||||
|
||||
return response.sessions;
|
||||
} catch (error: unknown) {
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] ❌ Ошибка получения сессий:`,
|
||||
error instanceof RequestError ? error.message : error
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить статус сессии на главном сервере
|
||||
*/
|
||||
async function updateSessionStatus(
|
||||
sessionId: string,
|
||||
status: "starting" | "started" | "ending" | "ended",
|
||||
appPid?: number,
|
||||
cirrusPid?: number
|
||||
): Promise<void> {
|
||||
try {
|
||||
await got.patch(`${API_URL}/sessions/${sessionId}/status`, {
|
||||
json: {
|
||||
status,
|
||||
appPid,
|
||||
cirrusPid,
|
||||
},
|
||||
timeout: {
|
||||
request: 5000,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ✅ Статус сессии ${sessionId} обновлен на "${status}"`
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] ❌ Ошибка обновления статуса сессии:`,
|
||||
error instanceof RequestError ? error.message : error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запустить приложение для сессии
|
||||
*/
|
||||
async function startApplication(session: SessionData): Promise<void> {
|
||||
const { id: sessionId, app, serverId } = session;
|
||||
|
||||
// Проверить, не обрабатывается ли уже эта сессия
|
||||
if (processingSessions.has(sessionId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверить, не запущено ли уже приложение для этой сессии
|
||||
if (activeProcesses.has(sessionId)) {
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ⚠️ Приложение для сессии ${sessionId} уже запущено`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверить, что сервер назначен
|
||||
// Main server автоматически назначает серверы для готовых к запуску сессий
|
||||
if (!serverId) {
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ⏳ Сессия ${sessionId} ожидает назначения сервера main сервером`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверить, что сессия назначена именно этому серверу
|
||||
if (serverId !== SERVER_ID) {
|
||||
// Это нормально - сессия назначена другому серверу
|
||||
return;
|
||||
}
|
||||
|
||||
processingSessions.add(sessionId);
|
||||
|
||||
try {
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] 🚀 Запуск приложения "${app.name}" для сессии ${sessionId} (активных процессов: ${activeProcesses.size})`
|
||||
);
|
||||
|
||||
// Формируем путь к exe файлу приложения
|
||||
// Путь: C:\apps\{appName}\{appName}.exe
|
||||
const appPath = `C:\\apps\\${app.name}\\${app.name}.exe`;
|
||||
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] 📂 Путь к приложению: ${appPath}`
|
||||
);
|
||||
|
||||
// Проверяем существование exe файла
|
||||
if (!existsSync(appPath)) {
|
||||
throw new Error(
|
||||
`Файл приложения не найден: ${appPath}. Убедитесь, что приложение установлено.`
|
||||
);
|
||||
}
|
||||
|
||||
// Запускаем exe приложение
|
||||
// Используем 'pipe' для stderr чтобы видеть ошибки, но 'ignore' для stdin/stdout
|
||||
const appProcess = spawn(appPath, [], {
|
||||
detached: false,
|
||||
stdio: ["ignore", "ignore", "pipe"], // stdin: ignore, stdout: ignore, stderr: pipe
|
||||
windowsHide: true, // Скрывать окно консоли на Windows
|
||||
cwd: `C:\\apps\\${app.name}`, // Устанавливаем рабочую директорию приложения
|
||||
});
|
||||
|
||||
const appPid = appProcess.pid;
|
||||
|
||||
if (!appPid) {
|
||||
throw new Error("Не удалось получить PID процесса");
|
||||
}
|
||||
|
||||
// Сохранить процесс в карте активных процессов
|
||||
activeProcesses.set(sessionId, appProcess);
|
||||
|
||||
// Логирование stderr для диагностики
|
||||
if (appProcess.stderr) {
|
||||
appProcess.stderr.on("data", (data) => {
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] 🔴 STDERR [${sessionId}]: ${data.toString().trim()}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Обработка завершения процесса
|
||||
appProcess.on("exit", async (code, signal) => {
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] 🛑 Приложение для сессии ${sessionId} завершилось с кодом ${code}${signal ? ` (сигнал: ${signal})` : ""}`
|
||||
);
|
||||
|
||||
if (code !== 0 && code !== null) {
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] ⚠️ Приложение завершилось с ошибкой! Код выхода: ${code}`
|
||||
);
|
||||
}
|
||||
|
||||
activeProcesses.delete(sessionId);
|
||||
|
||||
// Обновить статус на "ended"
|
||||
await updateSessionStatus(sessionId, "ended");
|
||||
});
|
||||
|
||||
appProcess.on("error", async (error) => {
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] ❌ Ошибка процесса для сессии ${sessionId}:`,
|
||||
error
|
||||
);
|
||||
activeProcesses.delete(sessionId);
|
||||
|
||||
// Обновить статус на "ended" в случае ошибки
|
||||
await updateSessionStatus(sessionId, "ended");
|
||||
});
|
||||
|
||||
// Обновить статус на "started" с PID
|
||||
await updateSessionStatus(sessionId, "started", appPid);
|
||||
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ✅ Приложение запущено с PID ${appPid} (всего активных: ${activeProcesses.size})`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] ❌ Ошибка запуска приложения:`,
|
||||
error instanceof Error ? error.message : error
|
||||
);
|
||||
// Обновить статус на "ended" в случае ошибки
|
||||
await updateSessionStatus(sessionId, "ended");
|
||||
} finally {
|
||||
processingSessions.delete(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Убить процесс и всё его дерево дочерних процессов (для Windows)
|
||||
*/
|
||||
function killProcessTree(pid: number): void {
|
||||
try {
|
||||
// На Windows используем taskkill с флагом /T для убийства дерева процессов
|
||||
// /F - принудительное завершение
|
||||
// /T - завершить указанный процесс и все дочерние процессы
|
||||
execSync(`taskkill /pid ${pid} /T /F`, {
|
||||
stdio: "ignore",
|
||||
timeout: 10000,
|
||||
});
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ✅ Дерево процессов для PID ${pid} успешно завершено`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] ⚠️ Ошибка при завершении дерева процессов PID ${pid}:`,
|
||||
error instanceof Error ? error.message : error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Остановить приложение для сессии
|
||||
*/
|
||||
async function stopApplication(session: SessionData): Promise<void> {
|
||||
const { id: sessionId, appPid } = session;
|
||||
|
||||
// Проверить, не обрабатывается ли уже эта сессия
|
||||
if (processingSessions.has(sessionId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const appProcess = activeProcesses.get(sessionId);
|
||||
|
||||
if (!appProcess && !appPid) {
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ⚠️ Приложение для сессии ${sessionId} не найдено в активных процессах`
|
||||
);
|
||||
// Всё равно обновляем статус на "ended"
|
||||
await updateSessionStatus(sessionId, "ended");
|
||||
return;
|
||||
}
|
||||
|
||||
processingSessions.add(sessionId);
|
||||
|
||||
try {
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] 🛑 Остановка приложения для сессии ${sessionId} (PID: ${appPid || appProcess?.pid || "неизвестен"})`
|
||||
);
|
||||
|
||||
// Используем PID из базы данных если он есть, иначе из процесса
|
||||
const pidToKill = appPid || appProcess?.pid;
|
||||
|
||||
if (pidToKill) {
|
||||
// Убиваем весь процесс и все его дочерние процессы
|
||||
killProcessTree(pidToKill);
|
||||
} else if (appProcess) {
|
||||
// Если по какой-то причине нет PID, пробуем стандартный способ
|
||||
appProcess.kill("SIGTERM");
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
if (!appProcess.killed) {
|
||||
appProcess.kill("SIGKILL");
|
||||
}
|
||||
}
|
||||
|
||||
activeProcesses.delete(sessionId);
|
||||
|
||||
// Обновить статус на "ended"
|
||||
await updateSessionStatus(sessionId, "ended");
|
||||
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ✅ Приложение и все дочерние процессы остановлены для сессии ${sessionId}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] ❌ Ошибка остановки приложения:`,
|
||||
error instanceof Error ? error.message : error
|
||||
);
|
||||
} finally {
|
||||
processingSessions.delete(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить и обработать сессии
|
||||
*/
|
||||
async function checkSessions(): Promise<void> {
|
||||
if (!SERVER_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const sessions = await fetchSessions();
|
||||
// Получаем текущее время в UTC
|
||||
const now = new Date();
|
||||
|
||||
// Обработать сессии со статусом "starting"
|
||||
// Запускать только если время startAt уже наступило
|
||||
// Примечание: PostgreSQL возвращает timestamp with timezone как ISO 8601 строку,
|
||||
// которая автоматически парсится в UTC при создании Date объекта
|
||||
const allStartingSessions = sessions.filter((s) => s.status === "starting");
|
||||
const startingSessions = allStartingSessions.filter((s) => {
|
||||
const startAt = new Date(s.startAt);
|
||||
return startAt <= now;
|
||||
});
|
||||
|
||||
// Логировать запланированные сессии, которые ещё не пришло время запускать
|
||||
// Логируем не чаще раза в 10 секунд для каждой сессии, чтобы не спамить логами
|
||||
const scheduledSessions = allStartingSessions.filter((s) => {
|
||||
const startAt = new Date(s.startAt);
|
||||
return startAt > now;
|
||||
});
|
||||
|
||||
if (scheduledSessions.length > 0) {
|
||||
for (const session of scheduledSessions) {
|
||||
const lastLogTime = lastScheduledLogTime.get(session.id) || 0;
|
||||
const timeSinceLastLog = now.getTime() - lastLogTime;
|
||||
|
||||
// Логируем только если прошло больше 10 секунд с последнего лога
|
||||
if (timeSinceLastLog > 10000) {
|
||||
const startAt = new Date(session.startAt);
|
||||
const timeUntilStart = Math.round((startAt.getTime() - now.getTime()) / 1000);
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ⏰ Сессия ${session.id} (${session.app.name}) запланирована через ${timeUntilStart} сек`
|
||||
);
|
||||
lastScheduledLogTime.set(session.id, now.getTime());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Очистить карту логирования для сессий, которые больше не запланированы
|
||||
const scheduledSessionIds = new Set(scheduledSessions.map((s) => s.id));
|
||||
for (const sessionId of lastScheduledLogTime.keys()) {
|
||||
if (!scheduledSessionIds.has(sessionId)) {
|
||||
lastScheduledLogTime.delete(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
for (const session of startingSessions) {
|
||||
await startApplication(session);
|
||||
}
|
||||
|
||||
// Обработать сессии со статусом "ending"
|
||||
const endingSessions = sessions.filter((s) => s.status === "ending");
|
||||
for (const session of endingSessions) {
|
||||
await stopApplication(session);
|
||||
}
|
||||
|
||||
// Проверить, что все активные процессы соответствуют активным сессиям
|
||||
// Сессия считается активной, если:
|
||||
// 1. Статус "started"
|
||||
// 2. Статус "starting" И время startAt уже наступило
|
||||
const activeSessions = sessions.filter((s) => {
|
||||
if (s.status === "started") return true;
|
||||
if (s.status === "starting") {
|
||||
const startAt = new Date(s.startAt);
|
||||
return startAt <= now;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const activeSessionIds = new Set(activeSessions.map((s) => s.id));
|
||||
|
||||
// Остановить процессы для сессий, которые больше не активны
|
||||
for (const [sessionId, process] of activeProcesses.entries()) {
|
||||
if (!activeSessionIds.has(sessionId)) {
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ⚠️ Найден процесс для неактивной сессии ${sessionId}, остановка`
|
||||
);
|
||||
process.kill("SIGTERM");
|
||||
activeProcesses.delete(sessionId);
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] ❌ Ошибка проверки сессий:`,
|
||||
error instanceof Error ? error.message : error
|
||||
);
|
||||
}
|
||||
|
||||
// Планируем следующую проверку
|
||||
setTimeout(() => checkSessions(), SESSION_CHECK_INTERVAL_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Основная функция
|
||||
*/
|
||||
async function main() {
|
||||
console.log("=".repeat(60));
|
||||
console.log("🚀 Запуск сессионного сервера");
|
||||
console.log("=".repeat(60));
|
||||
console.log("Конфигурация:");
|
||||
console.log(` API URL: ${API_URL}`);
|
||||
console.log(` Hostname: ${HOSTNAME}`);
|
||||
console.log(` Local IP: ${LOCAL_IP}`);
|
||||
console.log(` Type: ${SERVER_TYPE}`);
|
||||
console.log(` GPU Free MB: ${getGpuFreeMb()} (читается из nvidia-smi)`);
|
||||
if (SERVER_LOCATION) console.log(` Location: ${SERVER_LOCATION}`);
|
||||
if (SERVER_TIER) console.log(` Tier: ${SERVER_TIER}`);
|
||||
if (BRANCH_ID) console.log(` Branch ID: ${BRANCH_ID}`);
|
||||
console.log(` Register Interval: ${REGISTER_INTERVAL_MS}ms`);
|
||||
console.log(` GPU Update Interval: ${GPU_UPDATE_INTERVAL_MS}ms`);
|
||||
console.log(` Session Check Interval: ${SESSION_CHECK_INTERVAL_MS}ms`);
|
||||
console.log("=".repeat(60));
|
||||
|
||||
// Запуск рекурсивной регистрации
|
||||
// Использует setTimeout вместо setInterval, чтобы избежать наложения запросов
|
||||
// После первой успешной регистрации запускается обновление GPU
|
||||
await registerServer();
|
||||
}
|
||||
|
||||
// Запуск
|
||||
main().catch((error: unknown) => {
|
||||
console.error(
|
||||
"Критическая ошибка:",
|
||||
error instanceof Error ? error.message : error
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user