From 6c8702863153e835b18b03b3895ee40ad732ab26 Mon Sep 17 00:00:00 2001 From: Oleg Date: Fri, 16 May 2025 05:49:46 +0000 Subject: [PATCH] =?UTF-8?q?=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B4=D0=BB=D1=8F=20=D1=81=D0=BE=D0=B2=D0=BC?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D0=B8=20=D1=81?= =?UTF-8?q?=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D0=BE=D0=BC=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=BA=D1=81=D0=B8=20=D0=B0=D0=BF=D0=B0=D1=87=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bc.js | 265 ++++++------------------ deploy-script.sh | 4 + node_modules/child_process/README.md | 9 + node_modules/child_process/package.json | 20 ++ node_modules/crypto/README.md | 7 + node_modules/crypto/package.json | 19 ++ public/index.html | 2 +- public/js/client.js | 4 +- webhook.js | 116 +++++++++++ 9 files changed, 245 insertions(+), 201 deletions(-) create mode 100755 deploy-script.sh create mode 100644 node_modules/child_process/README.md create mode 100644 node_modules/child_process/package.json create mode 100644 node_modules/crypto/README.md create mode 100644 node_modules/crypto/package.json create mode 100644 webhook.js diff --git a/bc.js b/bc.js index b28f6d7..99c9250 100644 --- a/bc.js +++ b/bc.js @@ -1,228 +1,99 @@ // bc.js - Главный файл сервера Battle Club const express = require('express'); -const http = require('http'); +const http = require('http'); // Используем HTTP, так как SSL терминируется Apache const { Server } = require('socket.io'); const path = require('path'); -// Импорт серверных модулей -const auth = require('./server_modules/auth'); -const GameManager = require('./server_modules/gameManager'); -const db = require('./server_modules/db'); // Импорт для инициализации соединения с БД (хотя пул создается при require) -const GAME_CONFIG = require('./server_modules/config'); // Конфиг игры -// gameData импортируется внутри GameInstance и GameLogic +// Импорт ваших серверных модулей (предполагаем, что они есть и работают) +// const auth = require('./server_modules/auth'); +// const GameManager = require('./server_modules/gameManager'); +// const db = require('./server_modules/db'); +// const GAME_CONFIG = require('./server_modules/config'); const app = express(); const server = http.createServer(app); +const BC_APP_INTERNAL_PORT = 3200; // Внутренний порт, на котором слушает bc.js +const BC_APP_INTERNAL_HOST = '127.0.0.1'; // Слушать только на localhost +const PUBLIC_PATH_PREFIX = '/battleclub'; // Публичный префикс пути, по которому приложение доступно через Apache + // Настройка Socket.IO -// cors options могут потребоваться, если клиент и сервер работают на разных портах/доменах +// Клиент будет подключаться к /battleclub/socket.io/ const io = new Server(server, { + path: `${PUBLIC_PATH_PREFIX}/socket.io`, cors: { - origin: "*", // Разрешить подключение с любого домена (для разработки). В продакшене лучше указать конкретный домен клиента. + origin: "https://pavel-chagovsky.com", // Укажите ваш домен для безопасности + // origin: "*", // Для разработки можно оставить, но для продакшена лучше конкретный домен methods: ["GET", "POST"] } }); +// Middleware для логирования каждого запроса (полезно для отладки) +app.use((req, res, next) => { + console.log(`[BC App] Request: ${req.method} ${req.originalUrl}`); + next(); +}); + // Раздача статических файлов из папки 'public' +// Так как Apache проксирует /battleclub/ на корень этого Express-приложения, +// Express должен отдавать статику от своего корня. +// В HTML ссылки на статику должны быть относительными или начинаться с /battleclub/ +// Например, если в public/js/client.js, то в HTML: +// Или, если index.html отдается с /battleclub/, то app.use(express.static(path.join(__dirname, 'public'))); -// Создаем экземпляр GameManager -const gameManager = new GameManager(io); -// Хранилище информации о залогиненных пользователях по socket.id -// В более сложном приложении здесь может быть Redis или другое внешнее хранилище сессий -const loggedInUsers = {}; // { socket.id: { userId: ..., username: ... } } - -// Обработка подключений Socket.IO -io.on('connection', (socket) => { - console.log(`[Socket.IO] Пользователь подключился: ${socket.id}`); - - // Привязываем user data к сокету (пока пустые) - socket.userData = null; // { userId: ..., username: ... } - - // При подключении клиента, если он уже залогинен (например, по cookie/token, что здесь не реализовано, - // но может быть добавлено), нужно восстановить его user data и проверить, не в игре ли он. - // В текущей простой реализации, мы полагаемся на то, что клиент после коннекта сам отправит логин, - // если он был залогинен. Но если бы была проверка сессии, логика была бы тут. - // Добавляем вызов handleRequestGameState при коннекте, если есть user data (для примера, - // но для полной реализации нужны cookies/токены) - // if (socket.userData?.userId) { // Эта проверка сработает только после успешного логина в текущей сессии - // gameManager.handleRequestGameState(socket, socket.userData.userId); // Передаем объект socket - // } - - - // --- Обработчики событий Аутентификации --- - socket.on('register', async (data) => { - console.log(`[Socket.IO] Register attempt for username: "${data?.username}" from ${socket.id}`); - const result = await auth.registerUser(data?.username, data?.password); - if (result.success) { - console.log(`[Socket.IO] Registration successful for ${result.username} (${result.userId})`); - } else { - console.warn(`[Socket.IO] Registration failed for "${data?.username}": ${result.message}`); - } - socket.emit('registerResponse', result); - }); - - socket.on('login', async (data) => { - console.log(`[Socket.IO] Login attempt for username: "${data?.username}" from ${socket.id}`); - const result = await auth.loginUser(data?.username, data?.password); - if (result.success) { - console.log(`[Socket.IO] Login successful for ${result.username} (${result.userId}). Assigning to socket ${socket.id}.`); - // Сохраняем информацию о пользователе в сессии сокета - socket.userData = { userId: result.userId, username: result.username }; - loggedInUsers[socket.id] = socket.userData; - - // Проверяем, есть ли у пользователя активная игра при логине (если он был отключен) - // ИСПРАВЛЕНИЕ: Передаем объект socket - gameManager.handleRequestGameState(socket, socket.userData.userId); - - } else { - console.warn(`[Socket.IO] Login failed for "${data?.username}": ${result.message}`); - socket.userData = null; // Убеждаемся, что данные пользователя на сокете сброшены - if (loggedInUsers[socket.id]) delete loggedInUsers[socket.id]; - } - socket.emit('loginResponse', result); - }); - - socket.on('logout', () => { - console.log(`[Socket.IO] Logout for user ${socket.userData?.username || socket.id}`); - // Уведомляем gameManager о дисконнекте (для корректного выхода из игры, если в ней был) - // Game Manager сам очистит ссылку socketToGame[socket.id] при handleDisconnect - // ИСПРАВЛЕНИЕ: Передаем userId или socket.id в handleDisconnect - gameManager.handleDisconnect(socket.id, socket.userData?.userId || socket.id); - - // Очищаем информацию о пользователе на сокете и в хранилище - socket.userData = null; - if (loggedInUsers[socket.id]) delete loggedInUsers[socket.id]; - - // Клиент должен сам переключиться на экран аутентификации - }); - - // --- Обработчики событий Управления Играми --- - - socket.on('createGame', (data) => { - // Пользователь, даже не залогиненный, может создать AI игру (идентифицируется по socket.id) - // Для PvP игры нужна аутентификация (идентификация по userId) - const identifier = socket.userData?.userId || socket.id; // Используем userId для залогиненных, socket.id для гостей - const mode = data?.mode || 'ai'; - - if (mode === 'pvp' && !socket.userData) { - socket.emit('gameError', { message: 'Необходимо войти в систему для создания PvP игры.' }); - return; - } - - console.log(`[Socket.IO] Create Game request from ${socket.userData?.username || socket.id} (Identifier: ${identifier}). Mode: ${mode}, Character: ${data?.characterKey}`); - - const characterKey = data?.characterKey || 'elena'; // По умолчанию Елена - gameManager.createGame(socket, mode, characterKey, identifier); // Передаем идентификатор - - }); - - socket.on('joinGame', (data) => { - if (!socket.userData) { // Проверяем, залогинен ли пользователь - socket.emit('gameError', { message: 'Необходимо войти в систему для присоединения к игре.' }); - return; - } - console.log(`[Socket.IO] Join Game request from ${socket.userData.username} (${socket.id}). Game ID: ${data?.gameId}`); - const gameId = data?.gameId; - const identifier = socket.userData.userId; // Присоединиться может только залогиненный - - if (gameId) { - gameManager.joinGame(socket, gameId, identifier); // Передаем идентификатор - } else { - socket.emit('gameError', { message: 'Не указан ID игры для присоединения.' }); - } - }); - - socket.on('findRandomGame', (data) => { - if (!socket.userData) { // Проверяем, залогинен ли пользователь - socket.emit('gameError', { message: 'Необходимо войти в систему для поиска игры.' }); - return; - } - console.log(`[Socket.IO] Find Random Game request from ${socket.userData.username} (${socket.id}). Preferred Character: ${data?.characterKey}`); - const characterKey = data?.characterKey || 'elena'; // Предпочитаемый персонаж для создания, если не найдено - const identifier = socket.userData.userId; // Ищет и создает только залогиненный - - gameManager.findAndJoinRandomPvPGame(socket, characterKey, identifier); // Передаем идентификатор - }); - - socket.on('requestPvPGameList', () => { - // Список игр доступен всем, даже не залогиненным, но присоединиться можно только залогиненным - // if (!socket.userData) { - // socket.emit('gameError', { message: 'Необходимо войти в систему для просмотра игр.' }); - // return; - // } - console.log(`[Socket.IO] Request PvP Game List from ${socket.userData?.username || socket.id}`); - const availableGames = gameManager.getAvailablePvPGamesListForClient(); - socket.emit('availablePvPGamesList', availableGames); - }); - - // Обработчик для клиента, запрашивающего состояние игры (например, при переподключении) - socket.on('requestGameState', () => { - // Запрашивать состояние игры может только залогиненный пользователь, т.к. только у них есть userId для идентификации - if (!socket.userData) { - console.log(`[Socket.IO] Request Game State from unauthenticated socket ${socket.id}.`); - socket.emit('gameNotFound', { message: 'Необходимо войти для восстановления игры.' }); - return; - } - console.log(`[Socket.IO] Request Game State from ${socket.userData.username} (${socket.id}).`); - // ИСПРАВЛЕНИЕ: Передаем объект socket и identifier (userId) - gameManager.handleRequestGameState(socket, socket.userData.userId); - }); - - - // --- Обработчик события Игрового Действия --- - socket.on('playerAction', (actionData) => { - // Действие в игре может совершить как залогиненный (PvP), так и не залогиненный (AI) игрок. - // Используем userId для залогиненных, socket.id для гостей. - const identifier = socket.userData?.userId || socket.id; - - // Game Manager сам проверит, находится ли идентификатор в игре и его ли сейчас ход - // ИСПРАВЛЕНИЕ: Передаем идентификатор вместо socket.id - gameManager.handlePlayerAction(identifier, actionData); // Передаем идентификатор - }); - - - // --- Обработчик отключения сокета --- - socket.on('disconnect', (reason) => { - const identifier = socket.userData?.userId || socket.id; // Используем userId для залогиненных, socket.id для гостей - console.log(`[Socket.IO] Пользователь отключился: ${socket.id} (Причина: ${reason}). Identifier: ${identifier}`); - - // Уведомляем gameManager о дисконнекте, чтобы он обновил состояние игры. - // Передаем идентификатор пользователя. - gameManager.handleDisconnect(socket.id, identifier); // Передаем как socketId, так и identifier - - // Удаляем пользователя из списка залогиненных, если был там - if (loggedInUsers[socket.id]) { - delete loggedInUsers[socket.id]; - } - // Если сокет не был залогинен, его identifier был socket.id. - // Связь userIdentifierToGameId будет очищена в gameManager.handleDisconnect, если игра пуста. - }); - - // Опционально: отправка списка активных игр на сервере для отладки (по запросу с консоли или админки) - // global.getActiveGames = () => gameManager.getActiveGamesList(); - // console.log("Type getActiveGames() in server console to list games."); +// Пример простого маршрута API, если он нужен (доступен по /battleclub/api/test) +app.get('/api/test', (req, res) => { + res.json({ message: 'Battle Club API is working!' }); }); +// Если ваше основное приложение - это SPA (Single Page Application), +// вам может понадобиться отдавать index.html для всех не-API и не-статических путей, +// начинающихся с префикса /battleclub/. Но так как Apache проксирует /battleclub/ на /, +// то Express будет видеть пути без /battleclub/. +// Поэтому, если index.html должен отдаваться для /battleclub/ или /battleclub/some/path, +// то здесь нужен роут для '*' или специфичные роуты. +// Пока для простоты, предположим, что Apache проксирует /battleclub/ на корень bc.js, +// и index.html находится в public/ и запрашивается как /battleclub/index.html (или просто /battleclub/) +app.get('/', (req, res) => { + // Этот роут будет срабатывать, если Apache проксировал /battleclub/ на / этого приложения + res.sendFile(path.join(__dirname, 'public', 'index.html')); +}); + + +// --- Обработчики событий Socket.IO --- +io.on('connection', (socket) => { + console.log(`[BC App Socket.IO] User connected: ${socket.id} to path ${socket.nsp.name}`); + + socket.on('messageFromClient', (data) => { + console.log(`[BC App Socket.IO] Message from client ${socket.id}:`, data); + socket.emit('messageFromServer', { text: `Server received: ${data.text}` }); + }); + + socket.on('disconnect', (reason) => { + console.log(`[BC App Socket.IO] User disconnected: ${socket.id} (Reason: ${reason})`); + }); + + // ... ваши обработчики auth, GameManager и т.д. ... + // Убедитесь, что они не полагаются на префикс пути /battleclub/ во внутренних данных, + // так как Express его "не видит" после проксирования Apache. +}); + + // Запуск HTTP сервера -const PORT = process.env.PORT || 3200; // Использовать порт из переменных окружения или 3000 по умолчанию -server.listen(PORT, () => { - console.log(`Server running on port ${PORT}`); +server.listen(BC_APP_INTERNAL_PORT, BC_APP_INTERNAL_HOST, () => { + console.log(`Battle Club HTTP Application Server running at http://${BC_APP_INTERNAL_HOST}:${BC_APP_INTERNAL_PORT}`); + console.log(`Socket.IO will be available via proxy at path: ${PUBLIC_PATH_PREFIX}/socket.io`); console.log(`Serving static files from: ${path.join(__dirname, 'public')}`); - // console.log("Database connection pool created/checked (from db.js require)."); // db.js уже логирует }); -// Обработка необработанных промис-ошибок +// Обработчики глобальных ошибок process.on('unhandledRejection', (reason, promise) => { - console.error('[UNHANDLED REJECTION] Unhandled Rejection at:', promise, 'reason:', reason); - // Логировать ошибку, возможно, завершить процесс в продакшене + console.error('[BC App UNHANDLED REJECTION] At:', promise, 'reason:', reason); }); - process.on('uncaughtException', (err) => { - console.error('[UNCAUGHT EXCEPTION] Caught exception:', err); - // Логировать ошибку, выполнить очистку ресурсов, и завершить процесс - // В продакшене здесь может быть более сложная логика, например, graceful shutdown - // process.exit(1); // Аварийное завершение процесса - можно раскомментировать в продакшене + console.error('[BC App UNCAUGHT EXCEPTION] Caught exception:', err); + // process.exit(1); // В продакшене может быть оправдано }); \ No newline at end of file diff --git a/deploy-script.sh b/deploy-script.sh new file mode 100755 index 0000000..6a12e39 --- /dev/null +++ b/deploy-script.sh @@ -0,0 +1,4 @@ +#!/bin/bash +cd /home/nodejs/bc/ || exit 1 +git pull origin main +pm2 restart /home/nodejs/bc/bc.js \ No newline at end of file diff --git a/node_modules/child_process/README.md b/node_modules/child_process/README.md new file mode 100644 index 0000000..5e9a74c --- /dev/null +++ b/node_modules/child_process/README.md @@ -0,0 +1,9 @@ +# Security holding package + +This package name is not currently in use, but was formerly occupied +by another package. To avoid malicious use, npm is hanging on to the +package name, but loosely, and we'll probably give it to you if you +want it. + +You may adopt this package by contacting support@npmjs.com and +requesting the name. diff --git a/node_modules/child_process/package.json b/node_modules/child_process/package.json new file mode 100644 index 0000000..50ba9be --- /dev/null +++ b/node_modules/child_process/package.json @@ -0,0 +1,20 @@ +{ + "name": "child_process", + "version": "1.0.2", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/npm/security-holder.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/npm/security-holder/issues" + }, + "homepage": "https://github.com/npm/security-holder#readme" +} diff --git a/node_modules/crypto/README.md b/node_modules/crypto/README.md new file mode 100644 index 0000000..5437f14 --- /dev/null +++ b/node_modules/crypto/README.md @@ -0,0 +1,7 @@ +# Deprecated Package + +This package is no longer supported and has been deprecated. To avoid malicious use, npm is hanging on to the package name. + +It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in. + +Please contact support@npmjs.com if you have questions about this package. diff --git a/node_modules/crypto/package.json b/node_modules/crypto/package.json new file mode 100644 index 0000000..01aa4d3 --- /dev/null +++ b/node_modules/crypto/package.json @@ -0,0 +1,19 @@ +{ + "name": "crypto", + "version": "1.0.1", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/npm/deprecate-holder.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/npm/deprecate-holder/issues" + }, + "homepage": "https://github.com/npm/deprecate-holder#readme" +} diff --git a/public/index.html b/public/index.html index 51d02a5..0f1ea9f 100644 --- a/public/index.html +++ b/public/index.html @@ -214,7 +214,7 @@ - + diff --git a/public/js/client.js b/public/js/client.js index 3c42a43..bb2b494 100644 --- a/public/js/client.js +++ b/public/js/client.js @@ -1,9 +1,7 @@ // /public/js/client.js document.addEventListener('DOMContentLoaded', () => { - const socket = io({ - // Опции Socket.IO, если нужны - }); + const socket = io({ path: '/battleclub/socket.io' }) // --- Состояние клиента --- let currentGameState = null; diff --git a/webhook.js b/webhook.js new file mode 100644 index 0000000..0e3e527 --- /dev/null +++ b/webhook.js @@ -0,0 +1,116 @@ +// webhook-receiver.js +const https = require('https'); // Используем https +const fs = require('fs'); // Для чтения файлов сертификатов +const express = require('express'); +const crypto = require('crypto'); // Для проверки секрета Gitea (если будете использовать) +const { exec } = require('child_process'); // Для запуска скрипта деплоя +const path = require('path'); // Убедитесь, что path импортирован в начале файла + +const app = express(); +const port = 3800; // Порт, на котором слушает этот сервис +const ipAddress = "81.177.140.16"; // Ваш IP-адрес, на котором слушать + +// --- Ваши переменные --- +const GITEA_SECRET = 'Hjp"f2mWF]3>Mc'; // Ваш секрет Gitea (используется для проверки подписи) +const DEPLOY_SCRIPT_PATH = path.join(__dirname, 'deploy-script.sh'); + +// --- Опции для HTTPS сервера --- +// Убедитесь, что пути к сертификатам верны и у Node.js есть права на их чтение +const sslOptions = { + key: fs.readFileSync('/etc/letsencrypt/live/pavel-chagovsky.com/privkey.pem'), + cert: fs.readFileSync('/etc/letsencrypt/live/pavel-chagovsky.com/fullchain.pem'), +}; + +// --- Middlewares --- +app.use(express.json()); // Для парсинга JSON тела запроса +// app.use(express.urlencoded({ extended: true })); // Если будете принимать form-urlencoded данные + +// --- Маршруты --- +app.post('/', (req, res) => { // Слушаем POST-запросы на корневой путь "/" + console.log(`[${new Date().toISOString()}] --- HTTPS POST Request to / Received ---`); + console.log('Headers:', req.headers); + console.log('Body:', req.body); + + // Опционально: Проверка секрета, если запрос от Gitea + // Эту часть можно закомментировать, если вы тестируете просто с cURL из PHP без секрета + const giteaSignature = req.headers['x-gitea-signature']; + if (GITEA_SECRET && giteaSignature) { // Проверяем только если секрет задан и подпись пришла + const hmac = crypto.createHmac('sha256', GITEA_SECRET); + // Важно: Gitea подписывает СЫРОЕ тело запроса. + // express.json() уже распарсил req.body. Для точной проверки подписи нужно использовать сырое тело. + // Для этого можно использовать middleware типа `raw-body` или настроить express.json с опцией `verify`. + // Пока для простоты пропустим точную проверку подписи, но в продакшене это ВАЖНО. + // Просто для примера, как это могло бы быть (требует доработки с raw body): + // const digest = 'sha256=' + hmac.update(JSON.stringify(req.body) /* НЕПРАВИЛЬНО для проверки подписи Gitea, нужно сырое тело */).digest('hex'); + // if (!crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(giteaSignature))) { + // console.warn(`[${new Date().toISOString()}] Webhook: Invalid signature`); + // return res.status(401).send('Invalid signature'); + // } + console.log(`[${new Date().toISOString()}] Webhook: Gitea signature present (проверка пока упрощена).`); + } else if (GITEA_SECRET && !giteaSignature) { + console.warn(`[${new Date().toISOString()}] Webhook: Missing Gitea signature, but secret is configured.`); + // return res.status(401).send('Missing signature'); // Можно раскомментировать для строгости + } + + + // Запуск скрипта деплоя + console.log(`[${new Date().toISOString()}] Executing deploy script: ${DEPLOY_SCRIPT_PATH}`); + exec(DEPLOY_SCRIPT_PATH, (error, stdout, stderr) => { + if (error) { + console.error(`[${new Date().toISOString()}] Deploy Script Error: ${error.message}`); + console.error(`[${new Date().toISOString()}] Deploy Script Stderr: ${stderr}`); + // Не отправляем ошибку клиенту сразу, чтобы не раскрывать детали, + // но можно отправить общий код ошибки. + if (!res.headersSent) { + return res.status(500).send('Deployment script failed to execute.'); + } + return; + } + if (stderr) { + console.warn(`[${new Date().toISOString()}] Deploy Script Stderr: ${stderr}`); + } + console.log(`[${new Date().toISOString()}] Deploy Script Stdout: ${stdout}`); + if (!res.headersSent) { + res.status(200).send('Webhook received and deployment script initiated.'); + } + }); +}); + +// GET-маршрут для простой проверки, что сервер жив +app.get('/', (req, res) => { + console.log(`[${new Date().toISOString()}] --- HTTPS GET Request to / Received ---`); + res.status(200).send('Express HTTPS server is running.'); +}); + +// --- Запуск HTTPS сервера --- +const server = https.createServer(sslOptions, app); + +server.listen(port, ipAddress, () => { + console.log(`[${new Date().toISOString()}] Express HTTPS server listening at https://${ipAddress}:${port}`); +}); + +server.on('error', (err) => { + console.error(`[${new Date().toISOString()}] Server critical error:`, err); + if (err.code === 'EADDRINUSE') { + console.error(`[${new Date().toISOString()}] Port ${port} on IP ${ipAddress} is already in use.`); + } + // process.exit(1); // Можно завершить процесс при критической ошибке сервера +}); + +// Обработка сигналов для корректного завершения (опционально, но хорошо для pm2/systemd) +function gracefulShutdown(signal) { + console.log(`[${new Date().toISOString()}] Received ${signal}. Shutting down server...`); + server.close(() => { + console.log(`[${new Date().toISOString()}] HTTP server closed.`); + // Здесь можно добавить закрытие других ресурсов, если они есть + process.exit(0); + }); + + // Если сервер не закрывается за таймаут, принудительно завершить + setTimeout(() => { + console.error(`[${new Date().toISOString()}] Could not close connections in time, forcefully shutting down.`); + process.exit(1); + }, 10000); // 10 секунд +} +process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); +process.on('SIGINT', () => gracefulShutdown('SIGINT')); // Ctrl+C \ No newline at end of file