Ported only the dual-stack listen from PR #291 (the change accepted as #188). With HOST unset the server binds :: and, if that fails with EAFNOSUPPORT, EADDRNOTAVAIL or EINVAL, retries 0.0.0.0. An explicit HOST, including 0.0.0.0 and 127.0.0.1, is tried once. The boot banner and doctor list shareable IPv6 addresses in brackets, skip fe80::, and keep one URL per /64. An explicit IPv6 literal in the local URL is bracketed. The Windows service listing no longer drops those bracketed lines. Not ported: /dev/grant, the settings-default migration, and the firewall scripts. The Docker image stays on 0.0.0.0; the alpine fallback was not booted. Tests: node --test test/ipv6-bind.test.js test/doctor.test.js test/ci.test.js — 20 pass, 0 fail (8 bind, 4 doctor, 8 ci).
176 lines
9.6 KiB
PowerShell
176 lines
9.6 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 = '::',
|
||
[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朋友访问地址(局域网 / IPv6,方括号不能少):"
|
||
# doctor prints a full URL. An IPv6 literal is bracketed, so a pattern that requires a digit right after http://
|
||
# would drop it. Keep every http:// line; the rest of the report is classification text.
|
||
& $nodeExe tools\doctor.mjs --port $Port | Select-String -Pattern 'http://' | 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 }
|