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.
557 lines
17 KiB
557 lines
17 KiB
# 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)
|
|
$prev = $ErrorActionPreference
|
|
$ErrorActionPreference = "SilentlyContinue"
|
|
$base = Get-DockerComposeArgs
|
|
docker compose @base @ComposeArgs 2>&1 | Out-Null
|
|
$code = $LASTEXITCODE
|
|
$ErrorActionPreference = $prev
|
|
return $code
|
|
}
|
|
|
|
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(12000)
|
|
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)
|
|
Push-Location $script:DevConfig.ProjectRoot
|
|
try {
|
|
$composeBase = Get-DockerComposeArgs
|
|
while ((Get-Date) -lt $deadline) {
|
|
$allOk = $true
|
|
foreach ($svc in $Services) {
|
|
$prev = $ErrorActionPreference
|
|
$ErrorActionPreference = "SilentlyContinue"
|
|
$json = docker compose @composeBase ps --format json $svc 2>$null
|
|
$ErrorActionPreference = $prev
|
|
if (-not $json) {
|
|
$allOk = $false
|
|
break
|
|
}
|
|
$status = $json | ConvertFrom-Json
|
|
if (-not $status -or $status.Health -ne "healthy") {
|
|
$allOk = $false
|
|
break
|
|
}
|
|
}
|
|
if ($allOk) {
|
|
Write-DevLog "docker healthy: $($Services -join ', ')" "Ok"
|
|
return $true
|
|
}
|
|
Start-Sleep -Seconds 2
|
|
}
|
|
throw "docker not healthy within ${TimeoutSec}s"
|
|
}
|
|
finally {
|
|
Pop-Location
|
|
}
|
|
}
|
|
|
|
function Start-DockerInfra {
|
|
param([string]$Root)
|
|
|
|
Push-Location $Root
|
|
try {
|
|
if (-not (Wait-DockerDaemonQuick -TimeoutSec $script:DevConfig.DockerDaemonWaitSec)) {
|
|
return $false
|
|
}
|
|
|
|
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' })
|
|
"@
|
|
}
|