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.

592 lines
18 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# Ensure MySQL + Redis are ready. Returns: docker | native
# Dot-sourced from start.ps1
function Get-DevInfraModeFromEnv {
param([string]$Root)
$envFile = Join-Path $Root ".env"
if (-not (Test-Path $envFile)) {
return "auto"
}
foreach ($line in Get-Content $envFile) {
if ($line -match '^\s*DEV_INFRA_MODE\s*=\s*(\S+)\s*$') {
return $Matches[1].Trim().ToLower()
}
}
return "auto"
}
function Get-EnvDbEndpoints {
param([string]$Root)
$envFile = Join-Path $Root ".env"
$mysqlHost = "localhost"
$mysqlPort = 3307
$redisHost = "localhost"
$redisPort = 6379
if (Test-Path $envFile) {
foreach ($line in Get-Content $envFile) {
if ($line -match '^\s*DATABASE_URL\s*=\s*mysql://[^@]+@([^:/]+):(\d+)/') {
$mysqlHost = $Matches[1]
$mysqlPort = [int]$Matches[2]
}
if ($line -match '^\s*REDIS_URL\s*=\s*redis://([^:/]+):(\d+)') {
$redisHost = $Matches[1]
$redisPort = [int]$Matches[2]
}
if ($line -match '^\s*MYSQL_HOST_PORT\s*=\s*(\d+)') {
$mysqlPort = [int]$Matches[1]
}
}
}
return @{
MysqlHost = $mysqlHost
MysqlPort = $mysqlPort
RedisHost = $redisHost
RedisPort = $redisPort
}
}
function Test-TcpPortOpen {
param(
[string]$HostName,
[int]$Port,
[int]$TimeoutMs = 2000
)
$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 Get-WslHostIp {
$distro = Get-DevWslDistro
if (-not $distro) { return $null }
$prev = $ErrorActionPreference
$ErrorActionPreference = "SilentlyContinue"
$raw = & wsl.exe -d $distro -e hostname -I 2>&1 | Out-String
$ErrorActionPreference = $prev
$text = ($raw -replace "`0", "").Trim()
$ip = ($text -split '\s+')[0]
if ($ip -match '^\d+\.\d+\.\d+\.\d+$') { return $ip }
return $null
}
function Resolve-InfraHost {
param(
[string]$Root,
[int]$MysqlPort,
[int]$RedisPort
)
$localRedisUrl = "redis://127.0.0.1:${RedisPort}"
if ((Test-TcpPortOpen -HostName "127.0.0.1" -Port $MysqlPort) -and
(Test-TcpPortOpen -HostName "127.0.0.1" -Port $RedisPort) -and
(Test-RedisPingFromNode -RedisUrl $localRedisUrl)) {
return "127.0.0.1"
}
$wslIp = Get-WslHostIp
if ($wslIp) {
$wslRedisUrl = "redis://${wslIp}:${RedisPort}"
if ((Test-TcpPortOpen -HostName $wslIp -Port $MysqlPort) -and
(Test-TcpPortOpen -HostName $wslIp -Port $RedisPort) -and
(Test-RedisPingFromNode -RedisUrl $wslRedisUrl)) {
return $wslIp
}
}
if ((Test-TcpPortOpen -HostName "127.0.0.1" -Port $MysqlPort) -and
(Test-TcpPortOpen -HostName "127.0.0.1" -Port $RedisPort)) {
return "127.0.0.1"
}
if ($wslIp -and
(Test-TcpPortOpen -HostName $wslIp -Port $MysqlPort) -and
(Test-TcpPortOpen -HostName $wslIp -Port $RedisPort)) {
return $wslIp
}
return "127.0.0.1"
}
function Apply-DevInfraEnv {
param([string]$Root)
$ep = Get-EnvDbEndpoints -Root $Root
$infraHost = Resolve-InfraHost -Root $Root -MysqlPort $ep.MysqlPort -RedisPort $ep.RedisPort
if ($infraHost -ne "127.0.0.1" -and $infraHost -ne "localhost") {
Write-DevLog "WSL localhost forward unavailable, using $infraHost" "Warn"
}
$envFile = Join-Path $Root ".env"
if (Test-Path $envFile) {
foreach ($line in Get-Content $envFile) {
if ($line -match '^\s*DATABASE_URL\s*=\s*(.+)$') {
$env:DATABASE_URL = ($Matches[1] -replace '@([^:/]+):', "@${infraHost}:")
}
if ($line -match '^\s*REDIS_URL\s*=\s*(.+)$') {
$env:REDIS_URL = ($Matches[1] -replace 'redis://([^:/]+):', "redis://${infraHost}:")
}
}
}
Ensure-DevDirs
$infraEnv = Join-Path $script:DevConfig.DevDir "infra.env"
$wslHostIp = Get-WslHostIp
$lines = @(
"DATABASE_URL=$($env:DATABASE_URL)"
"REDIS_URL=$($env:REDIS_URL)"
)
if ($wslHostIp) {
$lines += "WSL_HOST_IP=$wslHostIp"
}
$lines | Set-Content -Path $infraEnv -Encoding ascii
}
function Test-NativeInfraPorts {
param([string]$Root)
$ep = Get-EnvDbEndpoints -Root $Root
$infraHost = Resolve-InfraHost -Root $Root -MysqlPort $ep.MysqlPort -RedisPort $ep.RedisPort
$mysqlOk = Test-TcpPortOpen -HostName $infraHost -Port $ep.MysqlPort
$redisOk = Test-TcpPortOpen -HostName $infraHost -Port $ep.RedisPort
return @{
Ok = ($mysqlOk -and $redisOk)
Mysql = $mysqlOk
Redis = $redisOk
Host = $infraHost
Endpoints = $ep
}
}
function Test-NativeInfraReady {
param([string]$Root)
$ports = Test-NativeInfraPorts -Root $Root
if (-not $ports.Ok) {
return $false
}
Apply-DevInfraEnv -Root $Root
if (-not $env:REDIS_URL) {
return $false
}
return Test-RedisPingFromNode -RedisUrl $env:REDIS_URL
}
function Get-DockerComposeArgs {
$files = $script:DevConfig.ComposeFiles
$args = @()
foreach ($f in $files) {
$args += "-f"
$args += (Join-Path $script:DevConfig.ProjectRoot $f)
}
return $args
}
function Invoke-DockerComposeProject {
param(
[Parameter(ValueFromRemainingArguments = $true)][string[]]$ComposeArgs,
[int]$TimeoutSec = 90
)
$base = Get-DockerComposeArgs
$argList = @("compose") + $base + $ComposeArgs
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = "docker"
# Windows: quote paths with spaces
$psi.Arguments = ($argList | ForEach-Object {
if ($_ -match '\s') { '"' + $_ + '"' } else { $_ }
}) -join " "
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$proc = New-Object System.Diagnostics.Process
$proc.StartInfo = $psi
[void]$proc.Start()
$finished = $proc.WaitForExit($TimeoutSec * 1000)
if (-not $finished) {
try { $proc.Kill() } catch {}
Write-DevLog "docker compose timeout after ${TimeoutSec}s: $($psi.Arguments)" "Warn"
return 1
}
# drain pipes to avoid rare deadlocks
[void]$proc.StandardOutput.ReadToEnd()
[void]$proc.StandardError.ReadToEnd()
return $proc.ExitCode
}
function Test-DockerServicesHealthy {
param([string[]]$Services)
Push-Location $script:DevConfig.ProjectRoot
try {
$composeBase = Get-DockerComposeArgs
foreach ($svc in $Services) {
$prev = $ErrorActionPreference
$ErrorActionPreference = "SilentlyContinue"
$cid = (docker compose @composeBase ps -q $svc 2>$null | Select-Object -First 1)
if (-not $cid) {
$ErrorActionPreference = $prev
return $false
}
$health = (docker inspect -f "{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}" $cid 2>$null)
$ErrorActionPreference = $prev
if ($health -ne "healthy" -and $health -ne "running") {
return $false
}
# mysql/redis 有 healthcheck须为 healthy
if ($health -ne "healthy") {
return $false
}
}
return $true
}
finally {
Pop-Location
}
}
function Invoke-DockerInfoSafe {
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = "docker"
$psi.Arguments = "info"
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$proc = New-Object System.Diagnostics.Process
$proc.StartInfo = $psi
[void]$proc.Start()
$finished = $proc.WaitForExit(25000)
if (-not $finished) {
try { $proc.Kill() } catch {}
return @{ Code = -1; Output = "docker info timeout (engine likely stuck)" }
}
$output = $proc.StandardOutput.ReadToEnd() + $proc.StandardError.ReadToEnd()
return @{ Code = $proc.ExitCode; Output = $output }
}
function Get-DockerEngineStatus {
$info = Invoke-DockerInfoSafe
$output = $info.Output
$code = $info.Code
if ($code -eq 0 -and $output -match "(?m)^Server:") {
return "ok"
}
if ($output -match "500|Internal Server Error|request returned 500|engine likely stuck") {
return "engine_500"
}
if ($output -match "pipe|cannot find the file|connection refused|not running") {
return "not_running"
}
return "error"
}
function Get-WslInfraScriptPath {
param([string]$Root)
$drive = $Root.Substring(0, 1).ToLower()
$rest = $Root.Substring(2) -replace '\\', '/'
return "/mnt/$drive$rest/scripts/dev/wsl-infra.sh"
}
function Start-WslInfra {
param([string]$Root)
$scriptPath = Get-WslInfraScriptPath -Root $Root
$code = Invoke-DevWsl -BashCommand "bash '$scriptPath' status"
if ($code -ne 0) {
Write-DevLog "starting mysql+redis in WSL (no Docker Desktop)..." "Warn"
$code = Invoke-DevWsl -BashCommand "export MYSQL_HOST_PORT=3307 MYSQL_ROOT_PASSWORD=changeme; bash '$scriptPath' start"
}
if ($code -eq 0) {
$ep = Get-EnvDbEndpoints -Root $Root
$infraHost = Resolve-InfraHost -Root $Root -MysqlPort $ep.MysqlPort -RedisPort $ep.RedisPort
$redisUrl = "redis://${infraHost}:$($ep.RedisPort)"
if (Wait-InfraReadyFromWindows -HostName $infraHost -MysqlPort $ep.MysqlPort -RedisPort $ep.RedisPort -RedisUrl $redisUrl) {
Write-DevLog "wsl mysql:3307 + redis:6379 ready" "Ok"
}
else {
Write-DevLog "wsl infra started; Windows probe still waiting (will retry)" "Warn"
}
$distro = $env:WSL_DISTRO
if (-not $distro) { $distro = "Ubuntu-22.04" }
Start-WslKeepAlive -Distro $distro
}
return ($code -eq 0)
}
function Repair-DockerEngine {
param([int]$MaxAttempts = 1)
Write-DevLog "docker repair: quit desktop + wsl shutdown + restart (NO SwitchDaemon)" "Warn"
$desktopExe = Join-Path ${env:ProgramFiles} "Docker\Docker\Docker Desktop.exe"
if (Test-Path $desktopExe) {
$prev = $ErrorActionPreference
$ErrorActionPreference = "SilentlyContinue"
Start-Process -FilePath $desktopExe -ArgumentList "--quit" -ErrorAction SilentlyContinue | Out-Null
$ErrorActionPreference = $prev
Start-Sleep -Seconds 8
}
@(
"Docker Desktop",
"com.docker.backend",
"com.docker.proxy",
"vpnkit",
"wslrelay"
) | ForEach-Object {
Get-Process -Name $_ -ErrorAction SilentlyContinue |
Stop-Process -Force -ErrorAction SilentlyContinue
}
$prev = $ErrorActionPreference
$ErrorActionPreference = "SilentlyContinue"
wsl --shutdown 2>&1 | Out-Null
$ErrorActionPreference = $prev
Start-Sleep -Seconds 8
if (Test-Path $desktopExe) {
Start-Process -FilePath $desktopExe | Out-Null
}
$deadline = (Get-Date).AddSeconds(120)
while ((Get-Date) -lt $deadline) {
Start-Sleep -Seconds 5
if ((Get-DockerEngineStatus) -eq "ok") {
Write-DevLog "docker engine recovered" "Ok"
return $true
}
}
Write-DevLog "docker repair failed - use WSL infra instead: npm run dev:infra:setup" "Err"
return $false
}
function Wait-DockerDaemonQuick {
param([int]$TimeoutSec = 25)
$deadline = (Get-Date).AddSeconds($TimeoutSec)
while ((Get-Date) -lt $deadline) {
if ((Get-DockerEngineStatus) -eq "ok") {
Write-DevLog "docker daemon ready" "Ok"
return $true
}
Start-Sleep -Seconds 3
}
Write-DevLog "docker not ready in ${TimeoutSec}s (skip repair on start)" "Warn"
return $false
}
function Wait-DockerHealthy {
param(
[string[]]$Services,
[int]$TimeoutSec = 90
)
$deadline = (Get-Date).AddSeconds($TimeoutSec)
while ((Get-Date) -lt $deadline) {
if (Test-DockerServicesHealthy -Services $Services) {
Write-DevLog "docker healthy: $($Services -join ', ')" "Ok"
return $true
}
Start-Sleep -Seconds 2
}
throw "docker not healthy within ${TimeoutSec}s"
}
function Start-DockerInfra {
param([string]$Root)
Push-Location $Root
try {
if (-not (Wait-DockerDaemonQuick -TimeoutSec $script:DevConfig.DockerDaemonWaitSec)) {
return $false
}
if (Test-DockerServicesHealthy -Services $script:DevConfig.DockerServices) {
Write-DevLog "docker mysql/redis already healthy" "Ok"
return $true
}
Write-DevLog "docker compose up mysql redis (dev profile)..."
$code = Invoke-DockerComposeProject up -d mysql redis
if ($code -ne 0) {
return $false
}
Wait-DockerHealthy -Services $script:DevConfig.DockerServices -TimeoutSec $script:DevConfig.DockerWaitSec
return $true
}
finally {
Pop-Location
}
}
function Test-RedisPingFromNode {
param([string]$RedisUrl)
if (-not $RedisUrl) { return $false }
$escaped = $RedisUrl.Replace('"', '\"')
$nodeScript = @"
const Redis=require('ioredis');
(async()=>{
const r=new Redis('$escaped',{lazyConnect:true,connectTimeout:1500,maxRetriesPerRequest:1,retryStrategy:()=>null});
try{
await r.connect();
const pong=await r.ping();
await r.quit();
process.exit(pong==='PONG'?0:1);
}catch{
await r.quit().catch(()=>{});
process.exit(1);
}
})();
"@
$prev = $ErrorActionPreference
$ErrorActionPreference = "SilentlyContinue"
$null = node -e $nodeScript 2>&1
$ok = ($LASTEXITCODE -eq 0)
$ErrorActionPreference = $prev
return $ok
}
function Wait-InfraReadyFromWindows {
param(
[string]$HostName,
[int]$MysqlPort,
[int]$RedisPort,
[string]$RedisUrl,
[int]$TimeoutSec = 30
)
$deadline = (Get-Date).AddSeconds($TimeoutSec)
$attempt = 0
Wake-WslRedisForward
while ((Get-Date) -lt $deadline) {
$attempt += 1
$mysqlOk = Test-TcpPortOpen -HostName $HostName -Port $MysqlPort -TimeoutMs 800
$redisOk = Test-RedisPingFromNode -RedisUrl $RedisUrl
if ($mysqlOk -and $redisOk) {
return $true
}
if ($attempt -eq 1 -or ($attempt % 2) -eq 0) {
Wake-WslRedisForward
}
$sleepMs = if ($attempt -le 6) { 350 } elseif ($attempt -le 12) { 700 } else { 1200 }
Start-Sleep -Milliseconds $sleepMs
}
return $false
}
function Ensure-DevInfra {
param(
[string]$Root,
[string]$Mode = "auto"
)
$mode = $Mode.ToLower()
if ($mode -eq "auto") {
$mode = Get-DevInfraModeFromEnv -Root $Root
}
Write-DevLog "infra mode: $mode"
if ($mode -eq "native") {
if (Test-NativeInfraReady -Root $Root) {
Apply-DevInfraEnv -Root $Root
$distro = $env:WSL_DISTRO
if (-not $distro) { $distro = "Ubuntu-22.04" }
Start-WslKeepAlive -Distro $distro
Write-DevLog "using native mysql/redis" "Ok"
return "native"
}
Write-DevLog "native ports not ready, trying WSL mysql/redis..." "Warn"
if (Start-WslInfra -Root $Root) {
if (Test-NativeInfraReady -Root $Root) {
Apply-DevInfraEnv -Root $Root
Write-DevLog "using wsl mysql/redis" "Ok"
return "wsl"
}
}
$ports = Test-NativeInfraPorts -Root $Root
throw @"
native mode: mysql/redis not ready.
mysql $($ports.Endpoints.MysqlPort): $(if ($ports.Mysql) { 'open' } else { 'closed' })
redis $($ports.Endpoints.RedisPort): $(if ($ports.Redis) { 'open' } else { 'closed' })
Run once: npm run dev:infra:setup
Then: npm run dev:start
"@
}
if ($mode -eq "docker") {
if (-not (Start-DockerInfra -Root $Root)) {
throw "docker mode: failed to start mysql/redis. Run: npm run dev:repair-docker"
}
Write-DevLog "using docker mysql/redis" "Ok"
return "docker"
}
# auto: docker quick try -> wsl infra -> native ports
if (Start-DockerInfra -Root $Root) {
Write-DevLog "using docker mysql/redis" "Ok"
return "docker"
}
Write-DevLog "docker unavailable, trying WSL mysql/redis..." "Warn"
if (Start-WslInfra -Root $Root) {
if (Test-NativeInfraReady -Root $Root) {
Apply-DevInfraEnv -Root $Root
Write-DevLog "using wsl mysql/redis" "Ok"
return "wsl"
}
}
Write-DevLog "wsl infra failed, trying native ports..." "Warn"
if (Test-NativeInfraReady -Root $Root) {
Apply-DevInfraEnv -Root $Root
Write-DevLog "using native mysql/redis (fallback)" "Ok"
return "native"
}
$ports = Test-NativeInfraPorts -Root $Root
throw @"
Cannot start dev infrastructure.
Root cause: Docker Desktop WSL engine IPC deadlock (engine 500 / context deadline exceeded).
Do NOT use DockerCli -SwitchDaemon - it makes this worse.
Recommended permanent fix:
1. npm run dev:infra:setup # install mysql+redis in WSL (no Docker Desktop)
2. Set DEV_INFRA_MODE=native in .env
3. npm run dev:start
Or diagnose Docker: npm run dev:diagnose-docker
Current ports:
mysql $($ports.Endpoints.MysqlPort): $(if ($ports.Mysql) { 'open' } else { 'closed' })
redis $($ports.Endpoints.RedisPort): $(if ($ports.Redis) { 'open' } else { 'closed' })
"@
}