Add SessionInfoFloat component to SessionPage and enhance session management in server

- Introduced SessionInfoFloat component to display session details when a session is active.
- Updated session server logic to automatically transition sessions to "ending" status when their end time is reached.
- Improved process termination handling with enhanced error checking and fallback mechanisms.
- Refactored application stop logic to handle multiple processes more efficiently and added timeout management for process termination.
This commit is contained in:
2025-12-10 20:12:32 +05:00
parent e8b8eca0d6
commit a80544c936
6 changed files with 330 additions and 28 deletions
+14 -3
View File
@@ -92,7 +92,12 @@ Session Server автоматически управляет игровыми с
### Как это работает
1. **Проверка сессий**: Каждую секунду (или согласно `SESSION_CHECK_INTERVAL_MS`) сервер запрашивает у основного API список сессий для этого сервера
2. **Запуск приложений**: Для сессий со статусом `starting`:
2. **Проверка времени окончания**: Для активных сессий (`started` или `starting`):
- ⏰ Проверяется время `endAt` - если оно наступило, сессия автоматически переводится в статус `ending`
- **Timezone**: Все время хранится в UTC, сравнение корректно работает независимо от часового пояса сервера
- По умолчанию сессии создаются с временем окончания +30 минут от момента создания
- Автоматическое завершение гарантирует, что сессии не будут работать бесконечно
3. **Запуск приложений**: Для сессий со статусом `starting`:
- ⏰ Проверяется время `startAt` - приложение запускается только если это время уже наступило
- **Timezone**: Все время хранится в UTC, сравнение корректно работает независимо от часового пояса сервера
- Запланированные сессии (с будущим `startAt`) логируются с информацией о времени до запуска
@@ -104,13 +109,13 @@ Session Server автоматически управляет игровыми с
- Запускается соответствующее приложение
- Отслеживается PID процесса
- Статус сессии обновляется на `started` на главном сервере
3. **Остановка приложений**: Для сессий со статусом `ending`:
4. **Остановка приложений**: Для сессий со статусом `ending`:
- Используется `taskkill /pid {PID} /T /F` для завершения всего дерева процессов
- `/T` - завершает указанный процесс и ВСЕ дочерние процессы
- `/F` - принудительное завершение
- Решает проблему с UE5 и другими приложениями, создающими дочерние процессы
- Статус сессии обновляется на `ended` на главном сервере
4. **Автоматическая очистка**: Процессы для неактивных сессий автоматически останавливаются
5. **Автоматическая очистка**: Процессы для неактивных сессий автоматически останавливаются
### API endpoints для управления сессиями
@@ -182,6 +187,12 @@ C:\apps\
[2025-10-06T10:02:01.100Z] ✅ Статус сессии 123e4567-e89b-12d3-a456-426614174000 обновлен на "started"
```
Автоматическое завершение по истечении времени:
```
[2025-10-06T10:32:00.000Z] ⏰ Время сессии 123e4567-e89b-12d3-a456-426614174000 (minecraft) истекло, завершение...
[2025-10-06T10:32:00.100Z] ✅ Статус сессии 123e4567-e89b-12d3-a456-426614174000 обновлен на "ending"
```
Остановка сессии:
```
[2025-10-06T10:32:00.000Z] 🛑 Остановка приложения для сессии 123e4567-e89b-12d3-a456-426614174000 (PID: 12345)
Binary file not shown.
+134 -25
View File
@@ -584,11 +584,11 @@ async function startApplication(session: SessionData): Promise<void> {
// Для stream-режима также останавливаем Cirrus если он ещё работает
if (mode === "stream") {
const cirrus = activeCirrusProcesses.get(sessionId);
if (cirrus) {
if (cirrus && cirrus.pid) {
console.log(
`[${new Date().toISOString()}] 🛑 Остановка Cirrus для завершённой сессии ${sessionId}`
);
cirrus.kill("SIGTERM");
killProcessTree(cirrus.pid);
activeCirrusProcesses.delete(sessionId);
}
}
@@ -614,8 +614,8 @@ async function startApplication(session: SessionData): Promise<void> {
// Для stream-режима также останавливаем Cirrus
if (mode === "stream") {
const cirrus = activeCirrusProcesses.get(sessionId);
if (cirrus) {
cirrus.kill("SIGTERM");
if (cirrus && cirrus.pid) {
killProcessTree(cirrus.pid);
activeCirrusProcesses.delete(sessionId);
}
}
@@ -666,8 +666,8 @@ async function startApplication(session: SessionData): Promise<void> {
// Очистить Cirrus процесс если он был запущен (только для stream-режима)
if (mode === "stream") {
const cirrus = activeCirrusProcesses.get(sessionId);
if (cirrus) {
cirrus.kill("SIGTERM");
if (cirrus && cirrus.pid) {
killProcessTree(cirrus.pid);
activeCirrusProcesses.delete(sessionId);
}
}
@@ -689,17 +689,67 @@ function killProcessTree(pid: number): void {
// /T - завершить указанный процесс и все дочерние процессы
execSync(`taskkill /pid ${pid} /T /F`, {
stdio: "ignore",
timeout: 10000,
timeout: 30000, // Увеличен таймаут до 30 секунд
windowsHide: true, // Скрыть окно консоли на Windows
killSignal: "SIGKILL", // Принудительное завершение при таймауте
});
console.log(
`[${new Date().toISOString()}] ✅ Дерево процессов для PID ${pid} успешно завершено`
);
} catch (error) {
// Проверяем, действительно ли процесс всё ещё существует
// Если процесс не существует, это не ошибка
const processExists = checkProcessExists(pid);
if (!processExists) {
console.log(
`[${new Date().toISOString()}] ✅ Процесс PID ${pid} уже завершён`
);
return;
}
console.error(
`[${new Date().toISOString()}] ⚠️ Ошибка при завершении дерева процессов PID ${pid}:`,
error instanceof Error ? error.message : error
);
// Попытка принудительного завершения через PowerShell как запасной вариант
try {
console.log(
`[${new Date().toISOString()}] 🔄 Попытка принудительного завершения через PowerShell для PID ${pid}`
);
execSync(
`powershell -Command "Stop-Process -Id ${pid} -Force -ErrorAction SilentlyContinue"`,
{
stdio: "ignore",
timeout: 15000,
windowsHide: true,
}
);
console.log(
`[${new Date().toISOString()}] ✅ Процесс PID ${pid} завершён через PowerShell`
);
} catch (psError) {
console.error(
`[${new Date().toISOString()}] ❌ Не удалось завершить процесс PID ${pid} даже через PowerShell:`,
psError instanceof Error ? psError.message : psError
);
}
}
}
/**
* Проверить, существует ли процесс с указанным PID
*/
function checkProcessExists(pid: number): boolean {
try {
execSync(`tasklist /FI "PID eq ${pid}" | find "${pid}"`, {
stdio: "ignore",
timeout: 5000,
windowsHide: true,
});
return true;
} catch {
return false;
}
}
@@ -748,35 +798,54 @@ async function stopApplication(session: SessionData): Promise<void> {
);
}
// Создаём массив промисов для параллельной остановки процессов
const killPromises: Promise<void>[] = [];
// Останавливаем UE приложение
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");
}
killPromises.push(
new Promise<void>((resolve) => {
// Запускаем killProcessTree в отдельном потоке через setTimeout
// чтобы не блокировать основной поток
setTimeout(() => {
killProcessTree(pidToKill);
resolve();
}, 0);
})
);
} else {
console.warn(
`[${new Date().toISOString()}] ⚠️ Не удалось получить PID приложения для сессии ${sessionId}`
);
}
// Останавливаем Cirrus процесс (только для stream-режима)
if (mode === "stream") {
const cirrusPidToKill = cirrusPid || cirrusProcess?.pid;
if (cirrusPidToKill) {
killProcessTree(cirrusPidToKill);
} else if (cirrusProcess) {
cirrusProcess.kill("SIGTERM");
await new Promise((resolve) => setTimeout(resolve, 2000));
if (!cirrusProcess.killed) {
cirrusProcess.kill("SIGKILL");
}
killPromises.push(
new Promise<void>((resolve) => {
setTimeout(() => {
killProcessTree(cirrusPidToKill);
resolve();
}, 0);
})
);
} else {
console.warn(
`[${new Date().toISOString()}] ⚠️ Не удалось получить PID Cirrus для сессии ${sessionId}`
);
}
activeCirrusProcesses.delete(sessionId);
}
// Ждём завершения всех операций остановки с таймаутом
await Promise.race([
Promise.all(killPromises),
new Promise<void>((resolve) => setTimeout(resolve, 35000)), // Таймаут 35 секунд
]);
activeProcesses.delete(sessionId);
// Обновить статус на "ended"
@@ -860,9 +929,40 @@ async function checkSessions(): Promise<void> {
}
}
// Проверить сессии, у которых истекло время endAt
// Автоматически переводим их в статус "ending"
const expiredSessions = sessions.filter((s) => {
// Проверяем только активные сессии (started или starting)
// Для starting проверяем, что время startAt уже наступило
if (s.status === "starting") {
const startAt = new Date(s.startAt);
if (startAt > now) return false; // Ещё не началась
} else if (s.status !== "started") {
return false; // Не активная сессия
}
if (!s.endAt) return false;
const endAt = new Date(s.endAt);
return endAt <= now;
});
// Переводим истекшие сессии в статус "ending"
for (const session of expiredSessions) {
console.log(
`[${new Date().toISOString()}] ⏰ Время сессии ${session.id} (${
session.app.name
}) истекло, завершение...`
);
await updateSessionStatus(session.id, "ending");
}
// Обработать сессии со статусом "ending" ПЕРЕД запуском новых сессий
// Это гарантирует, что процессы будут остановлены до запуска новых
const endingSessions = sessions.filter((s) => s.status === "ending");
// Включаем как сессии со статусом "ending", так и только что истекшие
const endingSessions = sessions.filter(
(s) => s.status === "ending" || expiredSessions.some((es) => es.id === s.id)
);
for (const session of endingSessions) {
await stopApplication(session);
}
@@ -892,8 +992,17 @@ async function checkSessions(): Promise<void> {
console.log(
`[${new Date().toISOString()}] ⚠️ Найден процесс для неактивной сессии ${sessionId}, остановка`
);
process.kill("SIGTERM");
if (process.pid) {
killProcessTree(process.pid);
}
activeProcesses.delete(sessionId);
// Также останавливаем Cirrus если есть
const cirrusProcess = activeCirrusProcesses.get(sessionId);
if (cirrusProcess && cirrusProcess.pid) {
killProcessTree(cirrusProcess.pid);
activeCirrusProcesses.delete(sessionId);
}
}
}
} catch (error: unknown) {