Загрузка данных


Проект game-server (Node/TypeScript, Express). Файлы: src/controllers/index.ts и src/server.ts.
Git-репозитория нет. Перед правками выполни: cp -r src ../baseline_src (или используй существующую ../baseline/src). После правок выдай diff -u для каждого изменённого файла относительно baseline.

Симптомы:
- Сайт получает "KillGameServer timed out after 15 seconds" и "Failed to start game server ... timeout 100 seconds". game-server принимает запрос и не отвечает.
- В логе "port is already in use 6001" повторяется до 60 секунд, потому что порт остаётся в reservedPorts от мёртвого RCC.
- Клиент получает "Timedout" на PlaceLauncher.ashx примерно через 68 секунд: сервер не отвечает на запрос.

Не меняй формат ответов API, логику выбора портов, логику игры, конфиг.

Что сделать:

1. startRcc (строки ~171–326):
   a) Цикл while (this.reservedPorts.includes(portToRunOn)) (строки ~189–204) ограничь: если порт свободен по isPortAvailable/getFreeRccPort, но числится в reservedPorts, и связанный процесс RCC мёртв (rcc.exitCode !== null или rcc.killed), сними резерв сразу, не ждя 60 секунд. Общий лимит ожидания 5 секунд, потом освобождай резерв.
   b) Обработчики 'error' (строка ~230) и 'exit' (строка ~255) уже снимают порт; проверь, что при неудачном spawn (catch, строка ~218) порт тоже снимается (уже есть), и что при ошибке waitForRcc (строки ~316–324) порт снимается и rcc убивается через rcc.kill().

2. waitForRcc (строки ~346–374): ожидание уже ограничено 10 секундами, но каждый axios-запрос имеет timeout 1000 мс, это нормально. Добавь в catch при throw: снять порт, вызвать game.rccReference?.close(), выставить rccClosed = true.

3. openJob (строка ~452): таймер rej уже есть. Добавь, что по таймауту вызывается kill процесса RCC, если он передан или доступен через game.rccReference.close(), и удаляется задание из executorStatus.games. Не оставляй Promise висеть.

4. closeGame (строки ~656–705): сейчас есть два вызова без общего таймаута:
   - await axiosClient.post(conf.baseUrl + '/gs/delete', ...) (строка ~673) — у него нет timeout. Добавь timeout: 5000.
   - await this.openJob(game, 10 * 1000, ...) (строка ~682) — уменьши до 5000 мс.
   После этого: если openJob упал или не ответил, вызови rcc.kill() (через game.rccReference.close()), обязательно убери задание из executorStatus.games (строка ~688) даже при ошибках, и сними порт из reservedPorts.
   Оберни всё тело в try/finally, чтобы задание удалялось всегда.

5. startGame (строки ~558–654): перед push новой записи в executorStatus.games (строка ~620) проверь, нет ли уже задания с тем же serverId или портом. Если есть, сначала await this.closeGame(oldId) с таймаутом 5 секунд, потом запускай новое.

6. shutdown (строка ~722): оберни await this.closeGame в Promise.race с таймаутом 10 секунд. По таймауту: вызови rcc.kill() у найденного задания, удали задание из executorStatus.games, сними порт, и верни {} (не выбрасывай ошибку, если задание уже удалено).

7. server.ts (строки ~60–95): /start-game-server и /kill-game-server уже используют try/catch с res.status().json(). Добавь общий таймаут на обработчик: Promise.race между handle.startGameServer / handle.shutdown и таймером 30 секунд (для start) и 15 секунд (для kill). По таймауту верни res.status(504).json({ success: false, message: 'Timeout.' }).end(), только если ответ ещё не отправлен (res.headersSent).

8. Логи: в каждом этапе (startRcc, waitForRcc, openJob, closeGame, shutdown) добавь console.log с префиксом [timing] и Date.now() - start в мс. Существующие console.log не удаляй.

После правок: покажи diff -u по каждому файлу, перечисли изменённые функции и объясни, что защищает каждый таймаут.