upd
This commit is contained in:
@@ -21,6 +21,9 @@ interface Session {
|
||||
serverId: string | null;
|
||||
appPid: number | null;
|
||||
cirrusPid: number | null;
|
||||
streamerPort: number | null;
|
||||
playerPort: number | null;
|
||||
sfuPort: number | null;
|
||||
startAt: string;
|
||||
endAt: string | null;
|
||||
createdAt: string;
|
||||
@@ -44,6 +47,7 @@ interface Session {
|
||||
id: string;
|
||||
email: string;
|
||||
role: string;
|
||||
displayName: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
@@ -146,11 +150,11 @@ function SessionPage() {
|
||||
</div>
|
||||
|
||||
{/* Pixel Streaming Player - показывается когда сессия запущена */}
|
||||
{session.status === "started" && (
|
||||
{session.status === "started" && session.playerPort && (
|
||||
<div className="mb-6 aspect-video">
|
||||
<PixelStreamingWrapper
|
||||
initialSettings={{
|
||||
ss: "ws://127.0.0.1:8080",
|
||||
ss: `ws://127.0.0.1:${session.playerPort}`,
|
||||
AutoPlayVideo: true,
|
||||
AutoConnect: true,
|
||||
StartVideoMuted: true,
|
||||
@@ -190,6 +194,15 @@ function SessionPage() {
|
||||
{session.cirrusPid && (
|
||||
<InfoRow label="PID Cirrus" value={session.cirrusPid} />
|
||||
)}
|
||||
{session.streamerPort && (
|
||||
<InfoRow label="Streamer Port" value={session.streamerPort} />
|
||||
)}
|
||||
{session.playerPort && (
|
||||
<InfoRow label="Player Port" value={session.playerPort} />
|
||||
)}
|
||||
{session.sfuPort && (
|
||||
<InfoRow label="SFU Port" value={session.sfuPort} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@ interface Session {
|
||||
serverId: string | null;
|
||||
appPid: number | null;
|
||||
cirrusPid: number | null;
|
||||
streamerPort: number | null;
|
||||
playerPort: number | null;
|
||||
sfuPort: number | null;
|
||||
startAt: string;
|
||||
endAt: string | null;
|
||||
}
|
||||
@@ -29,7 +32,7 @@ function TestPage() {
|
||||
const response = await api
|
||||
.post("sessions", {
|
||||
json: {
|
||||
appId: "2914d736-b928-461c-b58f-e5d35d8b605d",
|
||||
appId: "c1a06420-ca72-4a89-893e-3fe669bbbb99",
|
||||
mode: "stream",
|
||||
tier: "demo",
|
||||
},
|
||||
|
||||
@@ -17,10 +17,16 @@ export const sessionController = new Elysia({ prefix: "/sessions" })
|
||||
status: sessionStatus,
|
||||
appPid,
|
||||
cirrusPid,
|
||||
streamerPort,
|
||||
playerPort,
|
||||
sfuPort,
|
||||
} = body as {
|
||||
status?: "starting" | "started" | "ending" | "ended";
|
||||
appPid?: number;
|
||||
cirrusPid?: number;
|
||||
streamerPort?: number;
|
||||
playerPort?: number;
|
||||
sfuPort?: number;
|
||||
};
|
||||
|
||||
// Проверить, что сессия существует
|
||||
@@ -35,6 +41,9 @@ export const sessionController = new Elysia({ prefix: "/sessions" })
|
||||
status: sessionStatus,
|
||||
appPid,
|
||||
cirrusPid,
|
||||
streamerPort,
|
||||
playerPort,
|
||||
sfuPort,
|
||||
});
|
||||
|
||||
return { session: updatedSession };
|
||||
@@ -51,6 +60,9 @@ export const sessionController = new Elysia({ prefix: "/sessions" })
|
||||
),
|
||||
appPid: t.Optional(t.Number()),
|
||||
cirrusPid: t.Optional(t.Number()),
|
||||
streamerPort: t.Optional(t.Number()),
|
||||
playerPort: t.Optional(t.Number()),
|
||||
sfuPort: t.Optional(t.Number()),
|
||||
}),
|
||||
}
|
||||
)
|
||||
@@ -253,11 +265,17 @@ export const sessionController = new Elysia({ prefix: "/sessions" })
|
||||
status: sessionStatus,
|
||||
appPid,
|
||||
cirrusPid,
|
||||
streamerPort,
|
||||
playerPort,
|
||||
sfuPort,
|
||||
endAt,
|
||||
} = body as {
|
||||
status?: "starting" | "started" | "ending" | "ended";
|
||||
appPid?: number;
|
||||
cirrusPid?: number;
|
||||
streamerPort?: number;
|
||||
playerPort?: number;
|
||||
sfuPort?: number;
|
||||
endAt?: string;
|
||||
};
|
||||
|
||||
@@ -276,6 +294,9 @@ export const sessionController = new Elysia({ prefix: "/sessions" })
|
||||
status: sessionStatus,
|
||||
appPid,
|
||||
cirrusPid,
|
||||
streamerPort,
|
||||
playerPort,
|
||||
sfuPort,
|
||||
endAt: endAt ? new Date(endAt) : undefined,
|
||||
});
|
||||
|
||||
@@ -293,6 +314,9 @@ export const sessionController = new Elysia({ prefix: "/sessions" })
|
||||
),
|
||||
appPid: t.Optional(t.Number()),
|
||||
cirrusPid: t.Optional(t.Number()),
|
||||
streamerPort: t.Optional(t.Number()),
|
||||
playerPort: t.Optional(t.Number()),
|
||||
sfuPort: t.Optional(t.Number()),
|
||||
endAt: t.Optional(t.String({ format: "date-time" })),
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ export const serverSessions = pgTable("server_sessions", {
|
||||
endAt: timestamp("end_at", { withTimezone: true }), // Default 30 minutes from start_at
|
||||
appPid: integer("app_pid"),
|
||||
cirrusPid: integer("cirrus_pid"),
|
||||
streamerPort: integer("streamer_port"), // Порт Cirrus для UE приложения (streamer)
|
||||
playerPort: integer("player_port"), // Порт Cirrus для клиента (player/браузер)
|
||||
sfuPort: integer("sfu_port"), // Порт Cirrus для SFU (Selective Forwarding Unit)
|
||||
mode: sessionModeEnum("mode").notNull(), // stream, local
|
||||
tier: serverTierEnum("tier"), // demo, prod (только для stream, nullable)
|
||||
status: sessionStatusEnum("status").notNull(), // starting, started, ending, ended
|
||||
|
||||
@@ -19,6 +19,9 @@ export interface UpdateSessionParams {
|
||||
status?: SessionStatus;
|
||||
appPid?: number;
|
||||
cirrusPid?: number;
|
||||
streamerPort?: number;
|
||||
playerPort?: number;
|
||||
sfuPort?: number;
|
||||
endAt?: Date;
|
||||
}
|
||||
|
||||
@@ -103,6 +106,9 @@ export const serverSessionService = {
|
||||
endAt: serverSessions.endAt,
|
||||
appPid: serverSessions.appPid,
|
||||
cirrusPid: serverSessions.cirrusPid,
|
||||
streamerPort: serverSessions.streamerPort,
|
||||
playerPort: serverSessions.playerPort,
|
||||
sfuPort: serverSessions.sfuPort,
|
||||
mode: serverSessions.mode,
|
||||
status: serverSessions.status,
|
||||
createdAt: serverSessions.createdAt,
|
||||
@@ -292,6 +298,18 @@ export const serverSessionService = {
|
||||
updateData.cirrusPid = params.cirrusPid;
|
||||
}
|
||||
|
||||
if (params.streamerPort !== undefined) {
|
||||
updateData.streamerPort = params.streamerPort;
|
||||
}
|
||||
|
||||
if (params.playerPort !== undefined) {
|
||||
updateData.playerPort = params.playerPort;
|
||||
}
|
||||
|
||||
if (params.sfuPort !== undefined) {
|
||||
updateData.sfuPort = params.sfuPort;
|
||||
}
|
||||
|
||||
if (params.endAt) {
|
||||
updateData.endAt = params.endAt;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
# production
|
||||
/build
|
||||
/dist
|
||||
/SignallingWebServer
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"keep": {
|
||||
"days": true,
|
||||
"amount": 14
|
||||
},
|
||||
"auditLog": "logs\\.6c25b665cc60dab90c3b387b2a7b4da90595b19b-audit.json",
|
||||
"files": [
|
||||
{
|
||||
"date": 1760357222809,
|
||||
"name": "logs\\server-2025-10-13.log",
|
||||
"hash": "d8c9aa8fe14287c28a5e6d97abfc99880c7be890d8f58eb2229e747850d1ac12"
|
||||
}
|
||||
],
|
||||
"hashType": "sha256"
|
||||
}
|
||||
+331
-46
@@ -2,6 +2,29 @@ import got, { RequestError } from "got";
|
||||
import os from "os";
|
||||
import { execSync, spawn, ChildProcess } from "child_process";
|
||||
import { existsSync } from "fs";
|
||||
import { createServer } from "net";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, join } from "path";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
/**
|
||||
* Сессионный сервер для запуска UE приложений
|
||||
*
|
||||
* Поддерживает два режима работы:
|
||||
*
|
||||
* 1. STREAM режим:
|
||||
* - Запускает Cirrus SignallingWebServer на случайном свободном порту (8080-9000)
|
||||
* - Запускает UE приложение с Pixel Streaming, подключенным к Cirrus
|
||||
* - Сохраняет в БД: appPid, cirrusPid, cirrusPort
|
||||
* - Используется для удаленного доступа через браузер
|
||||
*
|
||||
* 2. LOCAL режим:
|
||||
* - Запускает только UE приложение без Pixel Streaming
|
||||
* - Сохраняет в БД: только appPid
|
||||
* - Используется для локального запуска приложения
|
||||
*/
|
||||
|
||||
// Конфигурация
|
||||
const API_URL = process.env.API_URL || "http://localhost:3000";
|
||||
@@ -29,9 +52,12 @@ const SESSION_CHECK_INTERVAL_MS = parseInt(
|
||||
// ID зарегистрированного сервера (заполняется после регистрации)
|
||||
let SERVER_ID: string | null = null;
|
||||
|
||||
// Карта активных процессов: sessionId -> процесс приложения
|
||||
// Карта активных процессов приложений: sessionId -> процесс приложения (UE)
|
||||
const activeProcesses = new Map<string, ChildProcess>();
|
||||
|
||||
// Карта активных Cirrus процессов: sessionId -> процесс Cirrus
|
||||
const activeCirrusProcesses = new Map<string, ChildProcess>();
|
||||
|
||||
// Карта сессий в процессе запуска/остановки для предотвращения дублирования
|
||||
const processingSessions = new Set<string>();
|
||||
|
||||
@@ -73,6 +99,9 @@ interface SessionData {
|
||||
endAt: string | null;
|
||||
appPid: number | null;
|
||||
cirrusPid: number | null;
|
||||
streamerPort: number | null; // Порт Cirrus для UE приложения (streamer)
|
||||
playerPort: number | null; // Порт Cirrus для клиента (player/браузер)
|
||||
sfuPort: number | null; // Порт Cirrus для SFU (Selective Forwarding Unit)
|
||||
mode: "stream" | "local";
|
||||
status: "starting" | "started" | "ending" | "ended";
|
||||
createdAt: string;
|
||||
@@ -112,6 +141,42 @@ function getLocalIp(): string {
|
||||
return "127.0.0.1";
|
||||
}
|
||||
|
||||
/**
|
||||
* Найти свободный порт в указанном диапазоне
|
||||
*/
|
||||
function findFreePort(
|
||||
startPort: number = 8080,
|
||||
endPort: number = 9000
|
||||
): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tryPort = (port: number) => {
|
||||
if (port > endPort) {
|
||||
reject(
|
||||
new Error(
|
||||
`Не найден свободный порт в диапазоне ${startPort}-${endPort}`
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const server = createServer();
|
||||
|
||||
server.listen(port, () => {
|
||||
server.once("close", () => {
|
||||
resolve(port);
|
||||
});
|
||||
server.close();
|
||||
});
|
||||
|
||||
server.on("error", () => {
|
||||
tryPort(port + 1);
|
||||
});
|
||||
};
|
||||
|
||||
tryPort(startPort);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить свободную память GPU через nvidia-smi
|
||||
* Возвращает количество свободной памяти в МБ
|
||||
@@ -314,7 +379,10 @@ async function updateSessionStatus(
|
||||
sessionId: string,
|
||||
status: "starting" | "started" | "ending" | "ended",
|
||||
appPid?: number,
|
||||
cirrusPid?: number
|
||||
cirrusPid?: number,
|
||||
streamerPort?: number,
|
||||
playerPort?: number,
|
||||
sfuPort?: number
|
||||
): Promise<void> {
|
||||
try {
|
||||
await got.patch(`${API_URL}/sessions/${sessionId}/status`, {
|
||||
@@ -322,6 +390,9 @@ async function updateSessionStatus(
|
||||
status,
|
||||
appPid,
|
||||
cirrusPid,
|
||||
streamerPort,
|
||||
playerPort,
|
||||
sfuPort,
|
||||
},
|
||||
timeout: {
|
||||
request: 5000,
|
||||
@@ -343,7 +414,7 @@ async function updateSessionStatus(
|
||||
* Запустить приложение для сессии
|
||||
*/
|
||||
async function startApplication(session: SessionData): Promise<void> {
|
||||
const { id: sessionId, app, serverId } = session;
|
||||
const { id: sessionId, app, serverId, mode } = session;
|
||||
|
||||
// Проверить, не обрабатывается ли уже эта сессия
|
||||
if (processingSessions.has(sessionId)) {
|
||||
@@ -379,9 +450,16 @@ async function startApplication(session: SessionData): Promise<void> {
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] 🚀 Запуск приложения "${
|
||||
app.name
|
||||
}" для сессии ${sessionId} (активных процессов: ${activeProcesses.size})`
|
||||
}" для сессии ${sessionId} в режиме ${mode} (активных процессов: ${
|
||||
activeProcesses.size
|
||||
})`
|
||||
);
|
||||
|
||||
let streamerPort: number | undefined = undefined;
|
||||
let playerPort: number | undefined = undefined;
|
||||
let sfuPort: number | undefined = undefined;
|
||||
let cirrusPid: number | undefined = undefined;
|
||||
|
||||
// Формируем путь к exe файлу приложения
|
||||
// Путь: C:\apps\{appName}\{appName}.exe
|
||||
const appPath = `C:\\apps\\${app.name}\\${app.name}.exe`;
|
||||
@@ -397,30 +475,160 @@ async function startApplication(session: SessionData): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
// Запускаем exe приложение
|
||||
// Используем 'pipe' для stderr чтобы видеть ошибки, но 'ignore' для stdin/stdout
|
||||
const appProcess = spawn(
|
||||
appPath,
|
||||
[
|
||||
"-PixelStreamingURL=ws://127.0.0.1:8888",
|
||||
"-ForceRes",
|
||||
"-ResX=1920",
|
||||
"-ResY=1080",
|
||||
"-Unattended",
|
||||
"-RenderOffScreen",
|
||||
],
|
||||
{
|
||||
detached: false,
|
||||
stdio: ["ignore", "ignore", "pipe"], // stdin: ignore, stdout: ignore, stderr: pipe
|
||||
windowsHide: true, // Скрывать окно консоли на Windows
|
||||
cwd: `C:\\apps\\${app.name}`, // Устанавливаем рабочую директорию приложения
|
||||
// Для stream-режима запускаем Cirrus с тремя разными портами
|
||||
if (mode === "stream") {
|
||||
// Найти три свободных порта для Cirrus (смещая старт поиска, чтобы избежать зацикливания)
|
||||
streamerPort = await findFreePort(8080, 9000);
|
||||
playerPort = await findFreePort(Math.min(streamerPort + 1, 9000), 9000);
|
||||
sfuPort = await findFreePort(Math.min(playerPort + 1, 9000), 9000);
|
||||
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] 🔌 Найдены свободные порты - Streamer: ${streamerPort}, Player: ${playerPort}, SFU: ${sfuPort}`
|
||||
);
|
||||
|
||||
// Запускаем Cirrus SignallingWebServer
|
||||
// Путь к Cirrus: session-server/SignallingWebServer/dist/index.js
|
||||
// __dirname указывает на session-server/src или session-server/dist (в зависимости от того, запущен ли скомпилированный код)
|
||||
const cirrusPath = join(
|
||||
__dirname,
|
||||
"..",
|
||||
"SignallingWebServer",
|
||||
"dist",
|
||||
"index.js"
|
||||
);
|
||||
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] 🌐 Запуск Cirrus SignallingWebServer (Streamer: ${streamerPort}, Player: ${playerPort}, SFU: ${sfuPort})`
|
||||
);
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] 📂 Путь к Cirrus: ${cirrusPath}`
|
||||
);
|
||||
|
||||
const cirrusProcess = spawn(
|
||||
"node",
|
||||
[
|
||||
cirrusPath,
|
||||
"--streamer_port",
|
||||
streamerPort.toString(),
|
||||
"--player_port",
|
||||
playerPort.toString(),
|
||||
"--sfu_port",
|
||||
sfuPort.toString(),
|
||||
"--max_players",
|
||||
"0",
|
||||
],
|
||||
{
|
||||
detached: false,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
windowsHide: true,
|
||||
}
|
||||
);
|
||||
|
||||
cirrusPid = cirrusProcess.pid;
|
||||
|
||||
if (!cirrusPid) {
|
||||
throw new Error("Не удалось получить PID процесса Cirrus");
|
||||
}
|
||||
);
|
||||
|
||||
// Сохранить Cirrus процесс
|
||||
activeCirrusProcesses.set(sessionId, cirrusProcess);
|
||||
|
||||
// Логирование stdout и stderr для Cirrus
|
||||
if (cirrusProcess.stdout) {
|
||||
cirrusProcess.stdout.on("data", (data) => {
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] 🌐 Cirrus [${sessionId}]: ${data
|
||||
.toString()
|
||||
.trim()}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (cirrusProcess.stderr) {
|
||||
cirrusProcess.stderr.on("data", (data) => {
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] 🔴 Cirrus STDERR [${sessionId}]: ${data
|
||||
.toString()
|
||||
.trim()}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Обработка завершения Cirrus процесса
|
||||
cirrusProcess.on("exit", async (code, signal) => {
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] 🛑 Cirrus для сессии ${sessionId} завершился с кодом ${code}${
|
||||
signal ? ` (сигнал: ${signal})` : ""
|
||||
}`
|
||||
);
|
||||
activeCirrusProcesses.delete(sessionId);
|
||||
});
|
||||
|
||||
cirrusProcess.on("error", async (error) => {
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] ❌ Ошибка процесса Cirrus для сессии ${sessionId}:`,
|
||||
error
|
||||
);
|
||||
activeCirrusProcesses.delete(sessionId);
|
||||
});
|
||||
|
||||
// Ждём немного, чтобы Cirrus успел запуститься
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
}
|
||||
|
||||
// Запускаем UE приложение
|
||||
let appProcess: ChildProcess;
|
||||
|
||||
if (mode === "stream") {
|
||||
// Stream-режим: запускаем с Pixel Streaming
|
||||
appProcess = spawn(
|
||||
appPath,
|
||||
[
|
||||
`-PixelStreamingURL=ws://127.0.0.1:${streamerPort}`,
|
||||
"-ForceRes",
|
||||
"-ResX=1920",
|
||||
"-ResY=1080",
|
||||
"-Unattended",
|
||||
"-RenderOffScreen",
|
||||
],
|
||||
{
|
||||
detached: false,
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
windowsHide: true,
|
||||
cwd: `C:\\apps\\${app.name}`,
|
||||
}
|
||||
);
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] 🌐 Stream-режим: приложение подключается к Cirrus streamer_port ${streamerPort}`
|
||||
);
|
||||
} else {
|
||||
// Local-режим: запускаем без Pixel Streaming
|
||||
appProcess = spawn(
|
||||
appPath,
|
||||
[
|
||||
"-ForceRes",
|
||||
"-ResX=1920",
|
||||
"-ResY=1080",
|
||||
"-Unattended",
|
||||
"-RenderOffScreen",
|
||||
],
|
||||
{
|
||||
detached: false,
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
windowsHide: true,
|
||||
cwd: `C:\\apps\\${app.name}`,
|
||||
}
|
||||
);
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] 💻 Local-режим: приложение запускается без Pixel Streaming`
|
||||
);
|
||||
}
|
||||
|
||||
const appPid = appProcess.pid;
|
||||
|
||||
if (!appPid) {
|
||||
throw new Error("Не удалось получить PID процесса");
|
||||
throw new Error("Не удалось получить PID процесса приложения");
|
||||
}
|
||||
|
||||
// Сохранить процесс в карте активных процессов
|
||||
@@ -430,14 +638,14 @@ async function startApplication(session: SessionData): Promise<void> {
|
||||
if (appProcess.stderr) {
|
||||
appProcess.stderr.on("data", (data) => {
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] 🔴 STDERR [${sessionId}]: ${data
|
||||
`[${new Date().toISOString()}] 🔴 App STDERR [${sessionId}]: ${data
|
||||
.toString()
|
||||
.trim()}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Обработка завершения процесса
|
||||
// Обработка завершения процесса приложения
|
||||
appProcess.on("exit", async (code, signal) => {
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] 🛑 Приложение для сессии ${sessionId} завершилось с кодом ${code}${
|
||||
@@ -453,34 +661,82 @@ async function startApplication(session: SessionData): Promise<void> {
|
||||
|
||||
activeProcesses.delete(sessionId);
|
||||
|
||||
// Для stream-режима также останавливаем Cirrus если он ещё работает
|
||||
if (mode === "stream") {
|
||||
const cirrus = activeCirrusProcesses.get(sessionId);
|
||||
if (cirrus) {
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] 🛑 Остановка Cirrus для завершённой сессии ${sessionId}`
|
||||
);
|
||||
cirrus.kill("SIGTERM");
|
||||
activeCirrusProcesses.delete(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Обновить статус на "ended"
|
||||
await updateSessionStatus(sessionId, "ended");
|
||||
});
|
||||
|
||||
appProcess.on("error", async (error) => {
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] ❌ Ошибка процесса для сессии ${sessionId}:`,
|
||||
`[${new Date().toISOString()}] ❌ Ошибка процесса приложения для сессии ${sessionId}:`,
|
||||
error
|
||||
);
|
||||
activeProcesses.delete(sessionId);
|
||||
|
||||
// Для stream-режима также останавливаем Cirrus
|
||||
if (mode === "stream") {
|
||||
const cirrus = activeCirrusProcesses.get(sessionId);
|
||||
if (cirrus) {
|
||||
cirrus.kill("SIGTERM");
|
||||
activeCirrusProcesses.delete(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Обновить статус на "ended" в случае ошибки
|
||||
await updateSessionStatus(sessionId, "ended");
|
||||
});
|
||||
|
||||
// Обновить статус на "started" с PID
|
||||
await updateSessionStatus(sessionId, "started", appPid);
|
||||
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ✅ Приложение запущено с PID ${appPid} (всего активных: ${
|
||||
activeProcesses.size
|
||||
})`
|
||||
);
|
||||
// Обновить статус на "started" с PID приложения
|
||||
// Для stream-режима также сохраняем Cirrus PID и порты
|
||||
if (mode === "stream" && cirrusPid && streamerPort && playerPort && sfuPort) {
|
||||
await updateSessionStatus(
|
||||
sessionId,
|
||||
"started",
|
||||
appPid,
|
||||
cirrusPid,
|
||||
streamerPort,
|
||||
playerPort,
|
||||
sfuPort
|
||||
);
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ✅ Приложение запущено с PID ${appPid}, Cirrus с PID ${cirrusPid} (Streamer: ${streamerPort}, Player: ${playerPort}, SFU: ${sfuPort}) (всего активных: ${
|
||||
activeProcesses.size
|
||||
})`
|
||||
);
|
||||
} else {
|
||||
await updateSessionStatus(sessionId, "started", appPid);
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ✅ Приложение запущено с PID ${appPid} в режиме local (всего активных: ${
|
||||
activeProcesses.size
|
||||
})`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] ❌ Ошибка запуска приложения:`,
|
||||
error instanceof Error ? error.message : error
|
||||
);
|
||||
|
||||
// Очистить Cirrus процесс если он был запущен (только для stream-режима)
|
||||
if (mode === "stream") {
|
||||
const cirrus = activeCirrusProcesses.get(sessionId);
|
||||
if (cirrus) {
|
||||
cirrus.kill("SIGTERM");
|
||||
activeCirrusProcesses.delete(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Обновить статус на "ended" в случае ошибки
|
||||
await updateSessionStatus(sessionId, "ended");
|
||||
} finally {
|
||||
@@ -515,7 +771,7 @@ function killProcessTree(pid: number): void {
|
||||
* Остановить приложение для сессии
|
||||
*/
|
||||
async function stopApplication(session: SessionData): Promise<void> {
|
||||
const { id: sessionId, appPid } = session;
|
||||
const { id: sessionId, appPid, cirrusPid, mode } = session;
|
||||
|
||||
// Проверить, не обрабатывается ли уже эта сессия
|
||||
if (processingSessions.has(sessionId)) {
|
||||
@@ -523,8 +779,9 @@ async function stopApplication(session: SessionData): Promise<void> {
|
||||
}
|
||||
|
||||
const appProcess = activeProcesses.get(sessionId);
|
||||
const cirrusProcess = activeCirrusProcesses.get(sessionId);
|
||||
|
||||
if (!appProcess && !appPid) {
|
||||
if (!appProcess && !appPid && !cirrusProcess && !cirrusPid) {
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ⚠️ Приложение для сессии ${sessionId} не найдено в активных процессах`
|
||||
);
|
||||
@@ -536,15 +793,22 @@ async function stopApplication(session: SessionData): Promise<void> {
|
||||
processingSessions.add(sessionId);
|
||||
|
||||
try {
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] 🛑 Остановка приложения для сессии ${sessionId} (PID: ${
|
||||
appPid || appProcess?.pid || "неизвестен"
|
||||
})`
|
||||
);
|
||||
if (mode === "stream") {
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] 🛑 Остановка приложения для сессии ${sessionId} (App PID: ${
|
||||
appPid || appProcess?.pid || "неизвестен"
|
||||
}, Cirrus PID: ${cirrusPid || cirrusProcess?.pid || "неизвестен"})`
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] 🛑 Остановка приложения для сессии ${sessionId} (App PID: ${
|
||||
appPid || appProcess?.pid || "неизвестен"
|
||||
}) в режиме local`
|
||||
);
|
||||
}
|
||||
|
||||
// Используем PID из базы данных если он есть, иначе из процесса
|
||||
// Останавливаем UE приложение
|
||||
const pidToKill = appPid || appProcess?.pid;
|
||||
|
||||
if (pidToKill) {
|
||||
// Убиваем весь процесс и все его дочерние процессы
|
||||
killProcessTree(pidToKill);
|
||||
@@ -557,14 +821,35 @@ async function stopApplication(session: SessionData): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// Останавливаем 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");
|
||||
}
|
||||
}
|
||||
activeCirrusProcesses.delete(sessionId);
|
||||
}
|
||||
|
||||
activeProcesses.delete(sessionId);
|
||||
|
||||
// Обновить статус на "ended"
|
||||
await updateSessionStatus(sessionId, "ended");
|
||||
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ✅ Приложение и все дочерние процессы остановлены для сессии ${sessionId}`
|
||||
);
|
||||
if (mode === "stream") {
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ✅ Приложение и Cirrus остановлены для сессии ${sessionId}`
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ✅ Приложение остановлено для сессии ${sessionId}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] ❌ Ошибка остановки приложения:`,
|
||||
|
||||
Reference in New Issue
Block a user