- Node minimum is 22 in package.json engines, the start scripts, the service installer, setup and doctor, matching the README, the release notes and the CI matrix (22 / 24). - .gitattributes: CSS, HTML, JSON, Markdown and YAML check out with LF, so `node --test` on Windows matches the CSS rules the UI tests read; the Windows test step is no longer continue-on-error. - CI: actions/checkout@v7 and actions/setup-node@v7 (Node 24 runtime).
174 lines
9.4 KiB
PowerShell
174 lines
9.4 KiB
PowerShell
<#
|
||
卫戍协议:盟约 · 开机自动在后台运行服务器(Windows 任务计划程序)+ 防火墙规则。文档:docs\DEPLOY.md
|
||
用法(会自动请求管理员权限):
|
||
powershell -ExecutionPolicy Bypass -File scripts\install-service-windows.ps1 # 安装并立即启动
|
||
powershell -ExecutionPolicy Bypass -File scripts\install-service-windows.ps1 -Port 8080 -Verify sample
|
||
powershell -ExecutionPolicy Bypass -File scripts\install-service-windows.ps1 -Status # 查看状态
|
||
powershell -ExecutionPolicy Bypass -File scripts\install-service-windows.ps1 -Restart # 更新代码后重启
|
||
powershell -ExecutionPolicy Bypass -File scripts\install-service-windows.ps1 -Stop
|
||
powershell -ExecutionPolicy Bypass -File scripts\install-service-windows.ps1 -Uninstall # 删除任务和防火墙规则
|
||
做的事:写入 scripts\service.env.cmd(node.exe 路径、PORT、HOST、SP_COMBAT、SP_VERIFY)→ 注册计划任务
|
||
「StrongholdProtocol」(开机时以 SYSTEM 身份运行 scripts\run-server.cmd,无需登录;进程退出 5 秒后自动重启;
|
||
日志 logs\server.log)→ 添加入站防火墙规则「Stronghold Protocol」(TCP 端口,专用/域网络;-AllowPublicNetwork 也放行公用网络)。
|
||
#>
|
||
param(
|
||
[int]$Port = 3000,
|
||
[string]$BindHost = '0.0.0.0',
|
||
[ValidateSet('client', 'server')][string]$Combat = 'client',
|
||
[ValidateSet('off', 'sample', 'all')][string]$Verify = 'off',
|
||
[string]$TaskName = 'StrongholdProtocol',
|
||
[switch]$AllowPublicNetwork,
|
||
[switch]$NoFirewall,
|
||
[switch]$Status,
|
||
[switch]$Stop,
|
||
[switch]$Restart,
|
||
[switch]$Uninstall
|
||
)
|
||
# 'Continue': native tools (netsh, node) report through exit codes; Windows PowerShell 5.1 would turn their
|
||
# redirected stderr into terminating errors under 'Stop'. Cmdlets that must succeed use -ErrorAction Stop.
|
||
$ErrorActionPreference = 'Continue'
|
||
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch { }
|
||
$Root = Split-Path -Parent $PSScriptRoot
|
||
$RuleName = 'Stronghold Protocol'
|
||
$EnvFile = Join-Path $PSScriptRoot 'service.env.cmd'
|
||
$Runner = Join-Path $PSScriptRoot 'run-server.cmd'
|
||
$Log = Join-Path $Root 'logs\server.log'
|
||
|
||
function Test-Admin {
|
||
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||
return (New-Object Security.Principal.WindowsPrincipal($id)).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||
}
|
||
|
||
function Stop-ServerProcesses {
|
||
# the task runs cmd.exe (run-server.cmd, a restart loop) → node.exe: stop the loop first, then its children
|
||
$runners = @(Get-CimInstance Win32_Process -Filter "Name='cmd.exe'" -ErrorAction SilentlyContinue |
|
||
Where-Object { $_.CommandLine -like '*run-server.cmd*' })
|
||
foreach ($r in $runners) {
|
||
$children = @(Get-CimInstance Win32_Process -Filter "ParentProcessId=$($r.ProcessId)" -ErrorAction SilentlyContinue)
|
||
Stop-Process -Id $r.ProcessId -Force -ErrorAction SilentlyContinue
|
||
foreach ($ch in $children) { Stop-Process -Id $ch.ProcessId -Force -ErrorAction SilentlyContinue }
|
||
}
|
||
}
|
||
|
||
function Show-Status {
|
||
$t = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||
if (-not $t) { Write-Host "计划任务「$TaskName」未安装。" -ForegroundColor Yellow }
|
||
else {
|
||
$info = Get-ScheduledTaskInfo -TaskName $TaskName
|
||
Write-Host "计划任务「$TaskName」:$($t.State),上次运行 $($info.LastRunTime),结果 $($info.LastTaskResult)"
|
||
}
|
||
$p = $Port
|
||
if (Test-Path $EnvFile) {
|
||
$m = Select-String -Path $EnvFile -Pattern 'set "PORT=(\d+)"' | Select-Object -First 1
|
||
if ($m) { $p = [int]$m.Matches[0].Groups[1].Value }
|
||
}
|
||
try {
|
||
$h = Invoke-RestMethod -Uri "http://127.0.0.1:$p/healthz" -TimeoutSec 3
|
||
Write-Host "服务器正在运行:端口 $p,房间 $($h.rooms),对局 $($h.matches),连接 $($h.sockets)" -ForegroundColor Green
|
||
} catch { Write-Host "端口 $p 上没有响应(刚启动时请等几秒;日志:$Log)" -ForegroundColor Yellow }
|
||
& netsh advfirewall firewall show rule name="$RuleName" | Out-Null
|
||
if ($LASTEXITCODE -eq 0) { Write-Host "防火墙规则「$RuleName」已存在。" } else { Write-Host "防火墙规则「$RuleName」不存在。" -ForegroundColor Yellow }
|
||
if (Test-Path $Log) { Write-Host "`n日志最后 10 行($Log):"; Get-Content $Log -Tail 10 }
|
||
}
|
||
|
||
# Status needs no elevation
|
||
if ($Status) { Show-Status; exit 0 }
|
||
|
||
if (-not (Test-Admin)) {
|
||
Write-Host '需要管理员权限,正在请求提升…' -ForegroundColor Yellow
|
||
$argList = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$PSCommandPath`"")
|
||
foreach ($kv in $PSBoundParameters.GetEnumerator()) {
|
||
if ($kv.Value -is [System.Management.Automation.SwitchParameter]) { if ($kv.Value.IsPresent) { $argList += "-$($kv.Key)" } }
|
||
else { $argList += @("-$($kv.Key)", "`"$($kv.Value)`"") }
|
||
}
|
||
try {
|
||
$p = Start-Process -FilePath 'powershell.exe' -ArgumentList $argList -Verb RunAs -Wait -PassThru -ErrorAction Stop
|
||
exit $p.ExitCode
|
||
} catch {
|
||
Write-Host "未获得管理员权限,已取消:$($_.Exception.Message)" -ForegroundColor Red
|
||
exit 1
|
||
}
|
||
}
|
||
|
||
try {
|
||
if ($Uninstall) {
|
||
if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) {
|
||
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction Stop
|
||
}
|
||
Stop-ServerProcesses
|
||
& netsh advfirewall firewall delete rule name="$RuleName" | Out-Null
|
||
if (Test-Path $EnvFile) { Remove-Item $EnvFile -Force }
|
||
Write-Host '已删除计划任务、防火墙规则和 scripts\service.env.cmd。' -ForegroundColor Green
|
||
exit 0
|
||
}
|
||
if ($Stop -or $Restart) {
|
||
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||
Stop-ServerProcesses
|
||
Write-Host '服务器已停止。'
|
||
if ($Stop) { exit 0 }
|
||
Start-ScheduledTask -TaskName $TaskName -ErrorAction Stop
|
||
Start-Sleep -Seconds 3
|
||
Show-Status
|
||
exit 0
|
||
}
|
||
|
||
# --- install ------------------------------------------------------------------------------------
|
||
$nodeCmd = Get-Command node -ErrorAction SilentlyContinue
|
||
if (-not $nodeCmd) { throw '未找到 Node.js:请先安装(winget install OpenJS.NodeJS.LTS),然后重新打开 PowerShell。' }
|
||
$nodeExe = $nodeCmd.Source
|
||
$major = [int](((& $nodeExe -v) -replace '^v', '').Split('.')[0])
|
||
if ($major -lt 22) { throw "Node.js 版本太旧(需要 22+):$(& $nodeExe -v)" }
|
||
if ($nodeExe -like "$env:USERPROFILE*") {
|
||
Write-Host "注意:node.exe 位于用户目录($nodeExe,nvm/便携版?)。SYSTEM 账户一般也能运行它;若服务起不来,请改用 winget / 官网安装包安装的 Node.js。" -ForegroundColor Yellow
|
||
}
|
||
|
||
Set-Location $Root
|
||
Write-Host '准备运行环境(node tools\setup.mjs)…' -ForegroundColor Cyan
|
||
& $nodeExe tools\setup.mjs --quiet
|
||
if ($LASTEXITCODE -ne 0) { throw 'setup 失败,请先解决上面的问题(node tools\doctor.mjs 可诊断)。' }
|
||
|
||
@(
|
||
'@rem Written by scripts\install-service-windows.ps1 - re-run it to change these values.',
|
||
"set `"NODE_EXE=$nodeExe`"",
|
||
"set `"PORT=$Port`"",
|
||
"set `"HOST=$BindHost`"",
|
||
"set `"SP_COMBAT=$Combat`"",
|
||
"set `"SP_VERIFY=$Verify`""
|
||
) | Set-Content -Path $EnvFile -Encoding Oem -ErrorAction Stop
|
||
|
||
if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) {
|
||
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||
Stop-ServerProcesses
|
||
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
|
||
}
|
||
$action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument "/d /c `"`"$Runner`"`"" -WorkingDirectory $Root
|
||
$trigger = New-ScheduledTaskTrigger -AtStartup
|
||
$trigger.Delay = 'PT20S' # give the network a moment after boot
|
||
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
|
||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable `
|
||
-ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount 10 -RestartInterval (New-TimeSpan -Minutes 1) -MultipleInstances IgnoreNew
|
||
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings `
|
||
-Description "Stronghold Protocol web server ($Root)" -ErrorAction Stop | Out-Null
|
||
Write-Host "已注册计划任务「$TaskName」(开机自动启动,无需登录)。" -ForegroundColor Green
|
||
|
||
if (-not $NoFirewall) {
|
||
& netsh advfirewall firewall delete rule name="$RuleName" | Out-Null
|
||
$profiles = if ($AllowPublicNetwork) { 'private,domain,public' } else { 'private,domain' }
|
||
& netsh advfirewall firewall add rule name="$RuleName" dir=in action=allow protocol=TCP localport=$Port profile=$profiles | Out-Null
|
||
Write-Host "已添加防火墙入站规则「$RuleName」:TCP $Port($profiles)。" -ForegroundColor Green
|
||
}
|
||
|
||
Start-ScheduledTask -TaskName $TaskName -ErrorAction Stop
|
||
Start-Sleep -Seconds 4
|
||
Show-Status
|
||
Write-Host "`n朋友访问地址(局域网):"
|
||
& $nodeExe tools\doctor.mjs --port $Port | Select-String -Pattern 'http://\d' | ForEach-Object { Write-Host " $($_.Line.Trim())" }
|
||
Write-Host "`n停止:-Stop 重启:-Restart 状态:-Status 卸载:-Uninstall 日志:$Log"
|
||
} catch {
|
||
Write-Host "`n出错:$($_.Exception.Message)" -ForegroundColor Red
|
||
Read-Host '按回车键关闭' | Out-Null
|
||
exit 1
|
||
}
|
||
if ($Host.Name -eq 'ConsoleHost' -and -not $env:SP_NO_PAUSE) { Read-Host "`n完成。按回车键关闭" | Out-Null }
|