diff --git a/client/src/pages/SessionPage.tsx b/client/src/pages/SessionPage.tsx
index 9432933..31710b8 100644
--- a/client/src/pages/SessionPage.tsx
+++ b/client/src/pages/SessionPage.tsx
@@ -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() {
{/* Pixel Streaming Player - показывается когда сессия запущена */}
- {session.status === "started" && (
+ {session.status === "started" && session.playerPort && (
)}
+ {session.streamerPort && (
+
+ )}
+ {session.playerPort && (
+
+ )}
+ {session.sfuPort && (
+
+ )}
diff --git a/client/src/pages/TestPage.tsx b/client/src/pages/TestPage.tsx
index f6b9bdb..2c7d5a6 100644
--- a/client/src/pages/TestPage.tsx
+++ b/client/src/pages/TestPage.tsx
@@ -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",
},
diff --git a/server/src/controllers/session.ts b/server/src/controllers/session.ts
index a592f03..40cd612 100644
--- a/server/src/controllers/session.ts
+++ b/server/src/controllers/session.ts
@@ -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" })),
}),
}
diff --git a/server/src/db/schema/serverSessions.ts b/server/src/db/schema/serverSessions.ts
index 3d4d02d..eca1c3d 100644
--- a/server/src/db/schema/serverSessions.ts
+++ b/server/src/db/schema/serverSessions.ts
@@ -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
diff --git a/server/src/services/serverSession/index.ts b/server/src/services/serverSession/index.ts
index 55e1bee..e0617c7 100644
--- a/server/src/services/serverSession/index.ts
+++ b/server/src/services/serverSession/index.ts
@@ -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;
}
diff --git a/session-server/.gitignore b/session-server/.gitignore
index a68d131..3140421 100644
--- a/session-server/.gitignore
+++ b/session-server/.gitignore
@@ -15,6 +15,7 @@
# production
/build
/dist
+/SignallingWebServer
# misc
.DS_Store
diff --git a/session-server/logs/.6c25b665cc60dab90c3b387b2a7b4da90595b19b-audit.json b/session-server/logs/.6c25b665cc60dab90c3b387b2a7b4da90595b19b-audit.json
new file mode 100644
index 0000000..a79fc95
--- /dev/null
+++ b/session-server/logs/.6c25b665cc60dab90c3b387b2a7b4da90595b19b-audit.json
@@ -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"
+}
\ No newline at end of file
diff --git a/session-server/src/index.ts b/session-server/src/index.ts
index 413c9b5..963a760 100644
--- a/session-server/src/index.ts
+++ b/session-server/src/index.ts
@@ -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();
+// Карта активных Cirrus процессов: sessionId -> процесс Cirrus
+const activeCirrusProcesses = new Map();
+
// Карта сессий в процессе запуска/остановки для предотвращения дублирования
const processingSessions = new Set();
@@ -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 {
+ 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 {
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 {
- 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 {
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 {
);
}
- // Запускаем 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 {
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 {
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 {
- 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 {
}
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 {
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 {
}
}
+ // Останавливаем 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()}] ❌ Ошибка остановки приложения:`,