Files
ark-stronghold-protocol/scripts/install-service-windows.ps1
LimitlessHPPK 51c53de189 feedback7 G-TOOLS: listen on :: by default so one socket serves IPv6 and IPv4 (PR #291 by @LimitlessHPPK)
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).
2026-10-08 18:53:24 +08:00

176 lines
9.6 KiB
PowerShell
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<#
卫戍协议:盟约 · 开机自动在后台运行服务器(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 }