/
kochkatech
/
CorpGame
Обзор
Документация
Войти
/
kochkatech
/
CorpGame
Код
Запросы
1
Задачи
Вики
Пакеты
0
Релизы
1
CI/CD
Аналитика
master
scripts/start.ps1
271 строка
14 KB
kochkareal
feat(devops): свод к одному процессу Node на SQLite, портативная сборка без Docker; аудит безопасности
06 авг 2026, 15:06
06 авг 2026, 15:06
3266bcd
Код
Авторство
О чём код?
# Единый запуск игры: без Docker, без Postgres, без Redis — один процесс Node на файле SQLite. # Вызывается из start.bat (тонкая ASCII-обёртка, чтобы двойной клик работал как раньше). # Вся логика и русский текст — здесь, в PowerShell: он печатает кириллицу в консоли Windows # надёжно, в отличие от сырого cmd.exe, где строки могли разваливаться на куски и # восприниматься как отдельные (несуществующие) команды из-за путаницы с кодовой страницей. # # Node.js в поставку НЕ входит — единственное системное требование к машине ведущего, # как раньше был Docker Desktop, только на порядок легче (обычный установщик ~30 МБ, # без прав администратора и без виртуализации). Node ищется в PATH. # # Этот файл (и его соседи ensure-env.ps1/stop.ps1/fix-firewall.ps1) лежит в scripts/ — на # уровень ниже корня игры, где живут .bat-обёртки, data/, logs/ и .env. build-portable.ps1 # копирует всю папку scripts/ в портативную сборку как есть, без изменений: раскладка # определяется автоматически (см. Resolve-Layout) — по наличию app\server\main.js рядом с # корнем игры. Если его нет, считаем, что это разработка прямо из исходников репозитория, # и берём сервер из backend\dist. $ErrorActionPreference = 'Stop' [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8 function Stop-WithPause([int]$code) { Write-Host "" Read-Host "Нажмите Enter, чтобы закрыть окно" exit $code } function Resolve-Layout { # scripts/ всегда лежит прямо в корне игры — и в портативной сборке, и при запуске из # исходников: так start.bat может оставаться дословно тем же простым # `-File "%~dp0scripts\start.ps1"` в обоих случаях, без развилок. # Раскладка внутри отличается только тем, где лежит собранный сервер: # портативная: <корень>\app\server\main.js # из исходников: <корень>\backend\dist\main.js $gameRoot = Split-Path -Parent $PSScriptRoot $portableEntry = Join-Path $gameRoot 'app\server\main.js' if (Test-Path $portableEntry) { return [PSCustomObject]@{ GameRoot = $gameRoot; ServerEntry = $portableEntry; Portable = $true } } return [PSCustomObject]@{ GameRoot = $gameRoot ServerEntry = Join-Path $gameRoot 'backend\dist\main.js' Portable = $false } } function Test-PortFree([int]$port) { $listener = $null try { $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Any, $port) $listener.Start() return $true } catch { return $false } finally { if ($listener) { $listener.Stop() } } } function Select-Port { foreach ($candidate in 80, 8080, 3000, 3001) { if (Test-PortFree $candidate) { return $candidate } } # Все четыре заняты — не блокируем игру, отдаём выбор системе (0 = любой свободный), # затем узнаём, что она выбрала, точно так же, как проверяли остальные варианты. $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Any, 0) $listener.Start() $port = $listener.LocalEndpoint.Port $listener.Stop() return $port } function Get-LanIPs { # Реальные адреса в локальной сети: без loopback, без виртуальных адаптеров Docker/Hyper-V/ # WSL/VPN — на них есть IP, но игроки с телефонов до них не достучатся, а ведущему это # ничего не скажет, кроме путаницы. try { Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop | Where-Object { $_.IPAddress -notlike '169.254.*' -and $_.InterfaceAlias -notmatch 'Loopback|vEthernet|WSL|Virtual|Docker' } | Select-Object -ExpandProperty IPAddress } catch { @() } } function Test-FirewallPreflight([string]$nodeExe, [int]$port) { # Лучшее из возможного без прав администратора: предупредить о том, что уже настроено # неправильно. Ничего не чиним здесь — только диагностика, чинит fix-firewall.bat. try { $nodeExeFull = (Resolve-Path $nodeExe -ErrorAction Stop).Path $blocking = Get-NetFirewallRule -Direction Inbound -Action Block -Enabled True -ErrorAction Stop | Where-Object { $filter = $_ | Get-NetFirewallApplicationFilter -ErrorAction SilentlyContinue $filter -and $filter.Program -eq $nodeExeFull } if ($blocking) { Write-Host "[внимание] В брандмауэре Windows есть блокирующее правило для node.exe." Write-Host " Игроки не смогут подключиться по сети. Запустите fix-firewall.bat" Write-Host " (потребуются права администратора) и попробуйте снова." Write-Host "" } } catch { # Модуль NetSecurity недоступен или нет прав на чтение правил — не блокируем запуск, # просто не можем предупредить заранее. } try { $publicActive = Get-NetConnectionProfile -ErrorAction Stop | Where-Object { $_.NetworkCategory -eq 'Public' } if ($publicActive) { Write-Host "[внимание] Текущая Wi-Fi сеть помечена в Windows как «Общедоступная»." Write-Host " Даже если вы разрешите доступ в диалоге брандмауэра, для" Write-Host " общедоступных сетей он может остаться заблокирован. Если" Write-Host " телефоны не смогут зайти в игру — запустите fix-firewall.bat." Write-Host "" } } catch { # То же самое — необязательная диагностика. } } $root = $PSScriptRoot Write-Host "==================================================" Write-Host " БИТВА КОРПОРАЦИЙ — запуск" Write-Host "==================================================" Write-Host "" $layout = Resolve-Layout $gameRoot = $layout.GameRoot # file:-URL для DATABASE_URL ломается на путях с # ? % (это управляющие символы в URL) — # без этой проверки игра стартовала бы, но с непредсказуемо усечённым путём к базе. if ($gameRoot -match '[#?%]') { Write-Host "[ошибка] Путь к папке игры содержит символ # ? или % :" Write-Host " $gameRoot" Write-Host "Переместите папку игры в путь без таких символов (например, C:\CorpGame) и запустите снова." Stop-WithPause 1 } if ($gameRoot.Length -gt 100) { Write-Host "[внимание] Путь к папке игры очень длинный:" Write-Host " $gameRoot" Write-Host "Если что-то пойдёт не так на старте — переместите папку ближе к корню диска" Write-Host "(например, C:\CorpGame) и запустите снова." Write-Host "" } if (-not (Get-Command node -ErrorAction SilentlyContinue)) { Write-Host "[ошибка] Node.js не найден." Write-Host "Установите Node.js 20 LTS (обычный установщик, ~30 МБ, права администратора" Write-Host "не нужны): https://nodejs.org/" Write-Host "После установки запустите start.bat ещё раз." Stop-WithPause 1 } if (-not (Test-Path $layout.ServerEntry)) { if ($layout.Portable) { Write-Host "[ошибка] Повреждённая поставка: не найден $($layout.ServerEntry)" Write-Host "Скачайте игру заново." } else { Write-Host "[ошибка] Не найдена сборка сервера: $($layout.ServerEntry)" Write-Host "Соберите backend перед запуском:" Write-Host " cd backend; npm ci; npm run build" } Stop-WithPause 1 } $dataDir = Join-Path $gameRoot 'data' $logsDir = Join-Path $gameRoot 'logs' New-Item -ItemType Directory -Force -Path $dataDir | Out-Null New-Item -ItemType Directory -Force -Path $logsDir | Out-Null $dbPath = Join-Path $dataDir 'corpgame.db' $logPath = Join-Path $logsDir 'server.log' $pidPath = Join-Path $dataDir 'server.pid' Write-Host "Проверяю .env и секреты..." & (Join-Path $root 'ensure-env.ps1') -EnvPath (Join-Path $gameRoot '.env') -ExamplePath (Join-Path $gameRoot '.env.example') -DbPath $dbPath if ($LASTEXITCODE -ne 0) { Write-Host "[ошибка] Не удалось подготовить .env. Смотрите сообщение выше." Stop-WithPause 1 } $port = Select-Port Write-Host "Порт: $port" $nodeExe = (Get-Command node).Source Test-FirewallPreflight $nodeExe $port Write-Host "" Write-Host "Сейчас Windows может спросить про доступ к сети для node.exe." Write-Host "Нажмите «Разрешить доступ» и поставьте ОБЕ галочки (частные и общественные сети)." Write-Host "Если нажать «Отмена», телефоны не смогут подключиться к игре." Write-Host "" Write-Host "Запускаю сервер..." if (Test-Path $pidPath) { # Осиротевший процесс с прошлого раза (окно закрыли необычным способом) — не мешаем # новому занять порт. $oldPid = Get-Content $pidPath -ErrorAction SilentlyContinue if ($oldPid) { $oldProc = Get-Process -Id $oldPid -ErrorAction SilentlyContinue if ($oldProc -and $oldProc.ProcessName -eq 'node') { Stop-Process -Id $oldPid -Force -ErrorAction SilentlyContinue } } } $env:PORT = "$port" $env:NODE_ENV = 'production' $proc = Start-Process -FilePath $nodeExe -ArgumentList "`"$($layout.ServerEntry)`"" ` -WorkingDirectory (Split-Path -Parent $layout.ServerEntry) ` -NoNewWindow -PassThru ` -RedirectStandardOutput $logPath -RedirectStandardError "$logPath.err" Set-Content -Path $pidPath -Value $proc.Id -Encoding utf8 $ready = $false $deadline = (Get-Date).AddSeconds(30) while ((Get-Date) -lt $deadline) { if ($proc.HasExited) { break } try { $resp = Invoke-WebRequest -Uri "http://127.0.0.1:$port/api/health" -TimeoutSec 2 -UseBasicParsing if ($resp.StatusCode -eq 200) { $ready = $true; break } } catch { Start-Sleep -Milliseconds 300 } } if (-not $ready) { Write-Host "" Write-Host "[ошибка] Сервер не ответил за 30 секунд." if ($proc.HasExited) { Write-Host "Процесс завершился (код $($proc.ExitCode)). Последние строки журнала:" } else { Write-Host "Процесс всё ещё работает, но не отвечает. Последние строки журнала:" } Write-Host "--------------------------------------------------" Get-Content $logPath, "$logPath.err" -ErrorAction SilentlyContinue -Tail 20 Write-Host "--------------------------------------------------" Write-Host "Полный журнал: $logPath" if (-not $proc.HasExited) { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue } Remove-Item $pidPath -ErrorAction SilentlyContinue Stop-WithPause 1 } Write-Host "Готово! Открываю игру в браузере..." Start-Process "http://localhost:$port" $lanIPs = Get-LanIPs Write-Host "" Write-Host "==================================================" Write-Host " Игроки на этом компьютере: http://localhost:$port" if ($lanIPs) { Write-Host " Игроки на других устройствах в той же Wi-Fi сети:" foreach ($ip in $lanIPs) { Write-Host " http://${ip}:$port" } } else { Write-Host " Не удалось определить адрес в локальной сети автоматически." Write-Host " Посмотрите его командой ipconfig (строка IPv4-адрес) и откройте" Write-Host " http://<этот-адрес>:$port с телефона в той же Wi-Fi сети." } Write-Host " Панель администратора: http://localhost:$port/admin" Write-Host "==================================================" Write-Host "" Write-Host "НЕ ЗАКРЫВАЙТЕ ЭТО ОКНО — пока оно открыто, игра работает." Write-Host "Чтобы остановить игру — закройте окно или нажмите Ctrl+C." Write-Host "" try { Wait-Process -Id $proc.Id } finally { if (-not $proc.HasExited) { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue } Remove-Item $pidPath -ErrorAction SilentlyContinue }