You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
678 lines
20 KiB
678 lines
20 KiB
# dev script helpers (ASCII only for Windows PowerShell 5.1)
|
|
|
|
. (Join-Path $PSScriptRoot "config.ps1")
|
|
|
|
function Write-DevLog {
|
|
param(
|
|
[string]$Message,
|
|
[ValidateSet("Info", "Ok", "Warn", "Err")]
|
|
[string]$Level = "Info"
|
|
)
|
|
$prefix = switch ($Level) {
|
|
"Ok" { "[OK] " }
|
|
"Warn" { "[WARN] " }
|
|
"Err" { "[ERR] " }
|
|
default { "[..] " }
|
|
}
|
|
Write-Host "$prefix$Message"
|
|
}
|
|
|
|
function Invoke-DevNativeCommand {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[scriptblock]$CommandBlock
|
|
)
|
|
$prevEap = $ErrorActionPreference
|
|
$ErrorActionPreference = "Continue"
|
|
try {
|
|
$lines = & $CommandBlock 2>&1
|
|
$textLines = @()
|
|
foreach ($line in $lines) {
|
|
$text = if ($line -is [System.Management.Automation.ErrorRecord]) {
|
|
$line.ToString()
|
|
}
|
|
else {
|
|
[string]$line
|
|
}
|
|
$textLines += $text
|
|
Write-Host $text
|
|
}
|
|
$script:LastDevNativeOutput = ($textLines -join [Environment]::NewLine)
|
|
return $LASTEXITCODE
|
|
}
|
|
finally {
|
|
$ErrorActionPreference = $prevEap
|
|
}
|
|
}
|
|
|
|
function Invoke-NpmScript {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ScriptName,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$FailMessage
|
|
)
|
|
$code = Invoke-DevNativeCommand { npm run $ScriptName }
|
|
if ($code -ne 0) {
|
|
throw $FailMessage
|
|
}
|
|
}
|
|
|
|
function Ensure-DevDirs {
|
|
foreach ($dir in @($script:DevConfig.DevDir, $script:DevConfig.PidDir, $script:DevConfig.LogDir)) {
|
|
if (-not (Test-Path $dir)) {
|
|
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
|
}
|
|
}
|
|
}
|
|
|
|
function Stop-PortListeners {
|
|
param([int[]]$Ports)
|
|
foreach ($port in $Ports) {
|
|
$killed = @()
|
|
try {
|
|
$conns = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue
|
|
foreach ($conn in $conns) {
|
|
$processId = $conn.OwningProcess
|
|
if ($processId -and $processId -gt 0 -and $killed -notcontains $processId) {
|
|
Stop-Process -Id $processId -Force -ErrorAction SilentlyContinue
|
|
$killed += $processId
|
|
Write-DevLog "freed port $port pid=$processId" "Ok"
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
$lines = netstat -ano | Select-String ":$port\s"
|
|
foreach ($line in $lines) {
|
|
if ($line -match '\s+(\d+)\s*$') {
|
|
$processId = [int]$Matches[1]
|
|
if ($processId -gt 0 -and $killed -notcontains $processId) {
|
|
Stop-Process -Id $processId -Force -ErrorAction SilentlyContinue
|
|
$killed += $processId
|
|
Write-DevLog "freed port $port pid=$processId (netstat)" "Ok"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Stop-PidFileProcesses {
|
|
Ensure-DevDirs
|
|
$pidFiles = Get-ChildItem -Path $script:DevConfig.PidDir -Filter "*.pid" -ErrorAction SilentlyContinue
|
|
foreach ($file in $pidFiles) {
|
|
$raw = Get-Content $file.FullName -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
if ($raw -match '^\d+$') {
|
|
$procId = [int]$raw
|
|
Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue
|
|
Write-DevLog "stopped $($file.BaseName) pid=$procId" "Ok"
|
|
}
|
|
Remove-Item $file.FullName -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
function Get-ProjectNodeProcessIds {
|
|
param([string]$Root)
|
|
$escaped = [regex]::Escape($Root)
|
|
$ids = @()
|
|
try {
|
|
Get-CimInstance Win32_Process -Filter "Name='node.exe'" -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.CommandLine -and $_.CommandLine -match $escaped } |
|
|
ForEach-Object { $ids += $_.ProcessId }
|
|
}
|
|
catch {
|
|
Write-DevLog "skip node query by path (no WMI)" "Warn"
|
|
}
|
|
return @($ids | Select-Object -Unique)
|
|
}
|
|
|
|
function Stop-ProjectNodeProcesses {
|
|
param([string]$Root)
|
|
foreach ($procId in (Get-ProjectNodeProcessIds -Root $Root)) {
|
|
Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue
|
|
Write-DevLog "stopped node pid=$procId" "Ok"
|
|
}
|
|
}
|
|
|
|
function Wait-DevProjectProcessesGone {
|
|
param(
|
|
[string]$Root,
|
|
[int]$TimeoutSec = 20
|
|
)
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
|
while ((Get-Date) -lt $deadline) {
|
|
$remaining = Get-ProjectNodeProcessIds -Root $Root
|
|
if ($remaining.Count -eq 0) {
|
|
return $true
|
|
}
|
|
foreach ($procId in $remaining) {
|
|
Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue
|
|
Write-DevLog "re-stop node pid=$procId" "Warn"
|
|
}
|
|
Start-Sleep -Milliseconds 800
|
|
}
|
|
|
|
$left = Get-ProjectNodeProcessIds -Root $Root
|
|
if ($left.Count -gt 0) {
|
|
Write-DevLog "dev processes still running pid=$($left -join ',')" "Warn"
|
|
return $false
|
|
}
|
|
return $true
|
|
}
|
|
|
|
function Test-PrismaEngineWritable {
|
|
param([string]$Root)
|
|
$engine = Join-Path $Root "node_modules\.prisma\client\query_engine-windows.dll.node"
|
|
if (-not (Test-Path $engine)) {
|
|
return $true
|
|
}
|
|
try {
|
|
$stream = [System.IO.File]::Open(
|
|
$engine,
|
|
[System.IO.FileMode]::Open,
|
|
[System.IO.FileAccess]::ReadWrite,
|
|
[System.IO.FileShare]::None
|
|
)
|
|
$stream.Close()
|
|
return $true
|
|
}
|
|
catch {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Wait-PrismaEngineWritable {
|
|
param(
|
|
[string]$Root,
|
|
[int]$TimeoutSec = 25
|
|
)
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
|
while ((Get-Date) -lt $deadline) {
|
|
if (Test-PrismaEngineWritable -Root $Root) {
|
|
return $true
|
|
}
|
|
Start-Sleep -Milliseconds 600
|
|
}
|
|
return $false
|
|
}
|
|
|
|
function Clear-PrismaEngineTempFiles {
|
|
param([string]$Root)
|
|
$clientDir = Join-Path $Root "node_modules\.prisma\client"
|
|
if (-not (Test-Path $clientDir)) {
|
|
return
|
|
}
|
|
Get-ChildItem -Path $clientDir -Filter "query_engine*.tmp*" -ErrorAction SilentlyContinue |
|
|
ForEach-Object {
|
|
Remove-Item -Path $_.FullName -Force -ErrorAction SilentlyContinue
|
|
Write-DevLog "removed stale prisma tmp: $($_.Name)" "Ok"
|
|
}
|
|
}
|
|
|
|
function Invoke-PrismaGenerateSafe {
|
|
param(
|
|
[string]$Root,
|
|
[int]$MaxAttempts = 5
|
|
)
|
|
Clear-PrismaEngineTempFiles -Root $Root
|
|
|
|
for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
|
|
if ($attempt -gt 1) {
|
|
Stop-AllDevProcesses
|
|
}
|
|
|
|
if (-not (Wait-DevProjectProcessesGone -Root $Root -TimeoutSec 15)) {
|
|
throw "dev processes still hold Prisma engine (run npm run dev:stop first)"
|
|
}
|
|
|
|
if (-not (Wait-PrismaEngineWritable -Root $Root -TimeoutSec 12)) {
|
|
Write-DevLog "query_engine DLL locked, retry $($attempt) of $($MaxAttempts)..." "Warn"
|
|
Start-Sleep -Seconds ([Math]::Min($attempt * 2, 10))
|
|
continue
|
|
}
|
|
|
|
Push-Location $Root
|
|
try {
|
|
$exitCode = Invoke-DevNativeCommand { npx prisma generate }
|
|
if ($exitCode -eq 0) {
|
|
return
|
|
}
|
|
$output = $script:LastDevNativeOutput
|
|
}
|
|
finally {
|
|
Pop-Location
|
|
}
|
|
|
|
if ($output -notmatch 'EPERM|operation not permitted|EBUSY|being used by another process') {
|
|
throw "prisma generate failed"
|
|
}
|
|
|
|
Write-DevLog "prisma generate EPERM, retry $($attempt) of $($MaxAttempts)..." "Warn"
|
|
Start-Sleep -Seconds ([Math]::Min($attempt * 2, 10))
|
|
}
|
|
|
|
throw "prisma generate failed: query_engine DLL locked (run npm run dev:stop first)"
|
|
}
|
|
|
|
function Clear-DevCaches {
|
|
param([string]$Root)
|
|
$targets = @(
|
|
(Join-Path $Root ".next"),
|
|
(Join-Path $Root "tsconfig.tsbuildinfo"),
|
|
(Join-Path $Root "node_modules/.cache"),
|
|
(Join-Path $Root "node_modules/.vite")
|
|
)
|
|
foreach ($path in $targets) {
|
|
if (Test-Path $path) {
|
|
Remove-Item -Path $path -Recurse -Force -ErrorAction SilentlyContinue
|
|
Write-DevLog "removed cache: $path" "Ok"
|
|
}
|
|
}
|
|
}
|
|
|
|
function Test-TcpPortQuick {
|
|
param(
|
|
[string]$HostName,
|
|
[int]$Port,
|
|
[int]$TimeoutMs = 800
|
|
)
|
|
$client = $null
|
|
try {
|
|
$client = New-Object System.Net.Sockets.TcpClient
|
|
$connect = $client.BeginConnect($HostName, $Port, $null, $null)
|
|
$ok = $connect.AsyncWaitHandle.WaitOne($TimeoutMs, $false)
|
|
if (-not $ok) {
|
|
return $false
|
|
}
|
|
$client.EndConnect($connect)
|
|
return $true
|
|
}
|
|
catch {
|
|
return $false
|
|
}
|
|
finally {
|
|
if ($client) {
|
|
$client.Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
function Import-DevEnvFiles {
|
|
param([string]$Root)
|
|
foreach ($file in @(
|
|
(Join-Path $Root ".env"),
|
|
(Join-Path $Root ".dev/infra.env")
|
|
)) {
|
|
if (-not (Test-Path $file)) { continue }
|
|
Get-Content $file | ForEach-Object {
|
|
if ($_ -match '^\s*(#|$)') { return }
|
|
$idx = $_.IndexOf('=')
|
|
if ($idx -gt 0) {
|
|
$k = $_.Substring(0, $idx).Trim()
|
|
$v = $_.Substring($idx + 1)
|
|
Set-Item -Path "env:$k" -Value $v
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Test-RedisReachableQuick {
|
|
param([int]$TimeoutMs = 800)
|
|
$url = $env:REDIS_URL
|
|
if (-not $url) { return $false }
|
|
if ($url -match 'redis://([^:/]+):(\d+)') {
|
|
$hostName = $Matches[1]
|
|
if ($hostName -eq "localhost") { $hostName = "127.0.0.1" }
|
|
return Test-TcpPortQuick -HostName $hostName -Port ([int]$Matches[2]) -TimeoutMs $TimeoutMs
|
|
}
|
|
return $false
|
|
}
|
|
|
|
function Wake-WslRedisForward {
|
|
if ($env:OS -notmatch "Windows") { return }
|
|
$distro = Get-DevWslDistro
|
|
if (-not $distro) { return }
|
|
$prev = $ErrorActionPreference
|
|
$ErrorActionPreference = "SilentlyContinue"
|
|
wsl.exe -d $distro -e redis-cli ping 2>&1 | Out-Null
|
|
$ErrorActionPreference = $prev
|
|
}
|
|
|
|
function Invoke-FlushRedis {
|
|
param([string]$Root)
|
|
Push-Location $Root
|
|
try {
|
|
Import-DevEnvFiles -Root $Root
|
|
if (-not (Test-RedisReachableQuick -TimeoutMs 800)) {
|
|
Wake-WslRedisForward
|
|
if (-not (Test-RedisReachableQuick -TimeoutMs 1200)) {
|
|
Write-DevLog "redis flush skipped (port closed, <2s)" "Warn"
|
|
return
|
|
}
|
|
}
|
|
|
|
$prev = $ErrorActionPreference
|
|
$ErrorActionPreference = "Continue"
|
|
npx tsx scripts/dev/flush-redis.ts *> $null
|
|
$code = $LASTEXITCODE
|
|
$ErrorActionPreference = $prev
|
|
if ($code -ne 0) {
|
|
Write-DevLog "redis flush skipped (redis not up?)" "Warn"
|
|
}
|
|
else {
|
|
Write-DevLog "redis flushed" "Ok"
|
|
}
|
|
}
|
|
catch {
|
|
Write-DevLog "redis flush skipped (redis not up?)" "Warn"
|
|
}
|
|
finally {
|
|
Pop-Location
|
|
}
|
|
}
|
|
|
|
function Wait-HttpOk {
|
|
param(
|
|
[string]$Url,
|
|
[int]$TimeoutSec = 120
|
|
)
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
|
while ((Get-Date) -lt $deadline) {
|
|
try {
|
|
$resp = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 5
|
|
if ($resp.StatusCode -ge 200 -and $resp.StatusCode -lt 500) {
|
|
Write-DevLog "$Url OK (HTTP $($resp.StatusCode))" "Ok"
|
|
return $true
|
|
}
|
|
}
|
|
catch {
|
|
Start-Sleep -Seconds 2
|
|
}
|
|
}
|
|
throw "timeout waiting for $Url (${TimeoutSec}s)"
|
|
}
|
|
|
|
function Start-DevBackgroundProcess {
|
|
param(
|
|
[string]$Name,
|
|
[string]$Command,
|
|
[string]$Root
|
|
)
|
|
Ensure-DevDirs
|
|
$logFile = Join-Path $script:DevConfig.LogDir "$Name.log"
|
|
$pidFile = Join-Path $script:DevConfig.PidDir "$Name.pid"
|
|
$rootEsc = $Root.Replace("'", "''")
|
|
$logEsc = $logFile.Replace("'", "''")
|
|
$infraEnv = Join-Path $Root ".dev/infra.env"
|
|
$rootEnv = Join-Path $Root ".env"
|
|
$envLoad = ""
|
|
if (Test-Path $rootEnv) {
|
|
$rootEnvEsc = $rootEnv.Replace("'", "''")
|
|
$envLoad += @"
|
|
Get-Content '$rootEnvEsc' | ForEach-Object {
|
|
if (`$_ -match '^\s*(#|$)') { return }
|
|
`$idx = `$_.IndexOf('=')
|
|
if (`$idx -gt 0) {
|
|
`$k = `$_.Substring(0, `$idx).Trim()
|
|
`$v = `$_.Substring(`$idx + 1)
|
|
Set-Item -Path env:`$k -Value `$v
|
|
}
|
|
};
|
|
"@
|
|
}
|
|
if (Test-Path $infraEnv) {
|
|
$infraEsc = $infraEnv.Replace("'", "''")
|
|
$envLoad += @"
|
|
Get-Content '$infraEsc' | ForEach-Object {
|
|
if (`$_ -match '^\s*(#|$)') { return }
|
|
`$idx = `$_.IndexOf('=')
|
|
if (`$idx -gt 0) {
|
|
`$k = `$_.Substring(0, `$idx).Trim()
|
|
`$v = `$_.Substring(`$idx + 1)
|
|
Set-Item -Path env:`$k -Value `$v
|
|
}
|
|
};
|
|
"@
|
|
}
|
|
|
|
# 新进程启动时清空旧日志,避免健康检查误读历史 fail-fast
|
|
Set-Content -Path $logFile -Value "" -Encoding utf8
|
|
|
|
$psCommand = "${envLoad}Set-Location '$rootEsc'; `$ErrorActionPreference='Continue'; $Command *>&1 | ForEach-Object { `"`$_`" | Out-File -LiteralPath '$logEsc' -Append -Encoding utf8 }"
|
|
$proc = Start-Process -FilePath "powershell.exe" `
|
|
-ArgumentList "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", $psCommand `
|
|
-PassThru `
|
|
-WindowStyle Hidden
|
|
|
|
Set-Content -Path $pidFile -Value $proc.Id -Encoding ascii
|
|
Write-DevLog "started $Name pid=$($proc.Id) log=$logFile" "Ok"
|
|
return $proc.Id
|
|
}
|
|
|
|
function Get-DevLogLatestRunText {
|
|
param([string]$LogFile)
|
|
if (-not (Test-Path $LogFile)) {
|
|
return ""
|
|
}
|
|
$lines = Get-Content $LogFile -ErrorAction SilentlyContinue
|
|
if (-not $lines) {
|
|
return ""
|
|
}
|
|
$startIdx = 0
|
|
for ($i = $lines.Count - 1; $i -ge 0; $i--) {
|
|
if ($lines[$i] -match '^> chajia@') {
|
|
$startIdx = $i
|
|
break
|
|
}
|
|
}
|
|
return ($lines[$startIdx..($lines.Count - 1)] -join "`n")
|
|
}
|
|
|
|
function Assert-DevWorkerHealthy {
|
|
param(
|
|
[string]$Name,
|
|
[int]$WaitSeconds = 3
|
|
)
|
|
Ensure-DevDirs
|
|
Start-Sleep -Seconds $WaitSeconds
|
|
|
|
$pidFile = Join-Path $script:DevConfig.PidDir "$Name.pid"
|
|
$logFile = Join-Path $script:DevConfig.LogDir "$Name.log"
|
|
$latest = Get-DevLogLatestRunText -LogFile $logFile
|
|
|
|
if (Test-Path $pidFile) {
|
|
$workerPid = [int](Get-Content $pidFile -ErrorAction SilentlyContinue)
|
|
$proc = Get-Process -Id $workerPid -ErrorAction SilentlyContinue
|
|
if (-not $proc) {
|
|
$hint = Get-DevWorkerFailureHint -LogFile $logFile -RunText $latest
|
|
throw "$Name process exited. $hint Log: $logFile"
|
|
}
|
|
}
|
|
|
|
if (-not $latest) {
|
|
return
|
|
}
|
|
|
|
if ($latest -match "MOTHERSHIP_QUOTE_URLS|缺少 RPA selector|assertRpaWorkerEnv|启动失败") {
|
|
$hint = Get-DevWorkerFailureHint -LogFile $logFile -RunText $latest
|
|
throw "$Name failed to start. $hint Log: $logFile"
|
|
}
|
|
}
|
|
|
|
function Get-DevWorkerFailureHint {
|
|
param(
|
|
[string]$LogFile,
|
|
[string]$RunText = ""
|
|
)
|
|
if (-not $RunText -and (Test-Path $LogFile)) {
|
|
$RunText = Get-DevLogLatestRunText -LogFile $LogFile
|
|
}
|
|
if (-not $RunText) {
|
|
return "Configure RPA env per docs/rpa-recording-sop.md."
|
|
}
|
|
if ($RunText -match "MOTHERSHIP_QUOTE_URLS") {
|
|
return "Set MOTHERSHIP_QUOTE_URLS (headed recording required, see docs/rpa-recording-sop.md)."
|
|
}
|
|
if ($RunText -match "缺少 RPA selector env:\s*RPA_SELECTOR_FASTEST_OPTION\s*$") {
|
|
return "Update code: RPA_SELECTOR_FASTEST_OPTION is optional; run git pull or rebuild."
|
|
}
|
|
if ($RunText -match "缺少 RPA selector env") {
|
|
return "Set required RPA_SELECTOR_* from Playwright recording (FASTEST optional)."
|
|
}
|
|
return "Check RPA env (MOTHERSHIP_QUOTE_URLS + RPA_SELECTOR_*)."
|
|
}
|
|
|
|
function Get-DevWslDistro {
|
|
$prev = $ErrorActionPreference
|
|
$ErrorActionPreference = "SilentlyContinue"
|
|
$raw = & wsl.exe -l -q 2>&1 | Out-String
|
|
$ErrorActionPreference = $prev
|
|
|
|
# wsl -l -q emits UTF-16LE; strip null bytes for PowerShell 5.1
|
|
$text = $raw -replace "`0", ""
|
|
|
|
$distros = @()
|
|
foreach ($line in ($text -split "`r?`n")) {
|
|
$name = ($line -replace '^\*\s*', '').Trim()
|
|
if ($name -and $name -notmatch '^(docker-desktop|docker-desktop-data)$') {
|
|
$distros += $name
|
|
}
|
|
}
|
|
|
|
$ubuntu = $distros | Where-Object { $_ -match 'Ubuntu' } | Select-Object -First 1
|
|
if ($ubuntu) { return $ubuntu }
|
|
if ($distros.Count -gt 0) { return $distros[0] }
|
|
return $null
|
|
}
|
|
|
|
function Ensure-DevWslDistro {
|
|
$distro = Get-DevWslDistro
|
|
if ($distro) { return $distro }
|
|
|
|
Write-DevLog "no Linux WSL distro, installing Ubuntu-22.04 (one-time)..." "Warn"
|
|
$prev = $ErrorActionPreference
|
|
$ErrorActionPreference = "SilentlyContinue"
|
|
wsl --install -d Ubuntu-22.04 --no-launch 2>&1 | Out-Null
|
|
$ErrorActionPreference = $prev
|
|
|
|
$distro = Get-DevWslDistro
|
|
if (-not $distro) {
|
|
throw "WSL Linux distro required. Run: wsl --install -d Ubuntu-22.04"
|
|
}
|
|
return $distro
|
|
}
|
|
|
|
function Invoke-DevWsl {
|
|
param([string]$BashCommand)
|
|
|
|
$distro = Ensure-DevWslDistro
|
|
$prev = $ErrorActionPreference
|
|
$ErrorActionPreference = "SilentlyContinue"
|
|
wsl -d $distro -e bash -lc $BashCommand 2>&1 | Out-Null
|
|
$code = $LASTEXITCODE
|
|
$ErrorActionPreference = $prev
|
|
return $code
|
|
}
|
|
|
|
function Stop-AllDevProcesses {
|
|
Write-DevLog "stopping dev processes..."
|
|
Stop-PidFileProcesses
|
|
Stop-WslKeepAlive
|
|
$ports = @($script:DevConfig.NextPort) + $script:DevConfig.ExtraPorts
|
|
Stop-PortListeners -Ports $ports
|
|
Stop-ProjectNodeProcesses -Root $script:DevConfig.ProjectRoot
|
|
$null = Wait-DevProjectProcessesGone -Root $script:DevConfig.ProjectRoot -TimeoutSec 12
|
|
Start-Sleep -Milliseconds 800
|
|
}
|
|
|
|
function Start-WslKeepAlive {
|
|
param([string]$Distro)
|
|
Ensure-DevDirs
|
|
$pidFile = Join-Path $script:DevConfig.PidDir "wsl-keepalive.pid"
|
|
if (Test-Path $pidFile) {
|
|
$oldPid = [int](Get-Content $pidFile -ErrorAction SilentlyContinue)
|
|
if ($oldPid -gt 0 -and (Get-Process -Id $oldPid -ErrorAction SilentlyContinue)) {
|
|
return
|
|
}
|
|
}
|
|
$proc = Start-Process -FilePath "wsl.exe" `
|
|
-ArgumentList "-d", $Distro, "-e", "sleep", "infinity" `
|
|
-WindowStyle Hidden -PassThru
|
|
Set-Content -Path $pidFile -Value $proc.Id -Encoding ascii
|
|
Write-DevLog "wsl keepalive pid=$($proc.Id)" "Ok"
|
|
}
|
|
|
|
function Stop-WslKeepAlive {
|
|
$pidFile = Join-Path $script:DevConfig.PidDir "wsl-keepalive.pid"
|
|
if (-not (Test-Path $pidFile)) {
|
|
return
|
|
}
|
|
$keepPid = [int](Get-Content $pidFile -ErrorAction SilentlyContinue)
|
|
if ($keepPid -gt 0) {
|
|
Stop-Process -Id $keepPid -Force -ErrorAction SilentlyContinue
|
|
}
|
|
Remove-Item $pidFile -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
# Resolve LAN IPv4 for colleague testing (skip WSL/Hyper-V virtual adapters).
|
|
function Resolve-DevLanHost {
|
|
param(
|
|
[string]$Configured = "auto"
|
|
)
|
|
if ($Configured -and $Configured -ne "auto") {
|
|
return $Configured.Trim()
|
|
}
|
|
|
|
$virtualPattern = 'WSL|Hyper-V|vEthernet|Docker|VMware|Loopback|Teredo|isatap|Npcap|VirtualBox'
|
|
$addrs = @()
|
|
try {
|
|
$addrs = @(Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop |
|
|
Where-Object {
|
|
$_.IPAddress -notmatch '^(127\.|169\.254\.)' -and
|
|
$_.InterfaceAlias -notmatch $virtualPattern
|
|
})
|
|
}
|
|
catch {
|
|
return $null
|
|
}
|
|
|
|
if ($addrs.Count -eq 0) {
|
|
return $null
|
|
}
|
|
|
|
$preferred = @($addrs | Where-Object {
|
|
$_.InterfaceAlias -match 'WLAN|Wi-?Fi|Wireless|Ethernet|以太网|无线'
|
|
} | Sort-Object InterfaceMetric)[0]
|
|
if ($preferred) {
|
|
return $preferred.IPAddress
|
|
}
|
|
|
|
return ($addrs | Sort-Object InterfaceMetric)[0].IPAddress
|
|
}
|
|
|
|
# 内网测试:允许同事访问本机 Next 端口(需管理员权限时仅告警)
|
|
function Ensure-DevLanFirewall {
|
|
$port = $script:DevConfig.NextPort
|
|
$ruleName = "chajia-dev-tcp-$port"
|
|
$existing = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
|
|
if ($existing) {
|
|
return
|
|
}
|
|
try {
|
|
New-NetFirewallRule `
|
|
-DisplayName $ruleName `
|
|
-Direction Inbound `
|
|
-LocalPort $port `
|
|
-Protocol TCP `
|
|
-Profile Any `
|
|
-Action Allow `
|
|
-ErrorAction Stop | Out-Null
|
|
Write-DevLog "firewall inbound TCP $port allowed ($ruleName)" "Ok"
|
|
}
|
|
catch {
|
|
Write-DevLog "firewall: could not open TCP $port (run dev:start as Administrator once)" "Warn"
|
|
}
|
|
}
|
|
|
|
. (Join-Path $PSScriptRoot "ensure-infra.ps1")
|