# Unified stack teardown: Docker containers (if any) + ALL native leftovers. # Used by stop.ps1 / restart.ps1 / start.ps1 / start-host.ps1. # Console messages are ASCII-only to avoid GBK mojibake. param( [switch]$Quiet ) $ErrorActionPreference = "SilentlyContinue" $Root = Split-Path -Parent $PSScriptRoot if (-not (Test-Path (Join-Path $Root "start.ps1"))) { # allow calling from repo root as -File scripts\Stop-Stack.ps1 if (Test-Path (Join-Path $PSScriptRoot "..\start.ps1")) { $Root = Resolve-Path (Join-Path $PSScriptRoot "..") } } Set-Location -LiteralPath $Root $LogDir = Join-Path $Root ".runtime\logs" function Write-Info([string]$msg) { if (-not $Quiet) { Write-Host $msg } } function Kill-Tree([int]$ProcessId) { if ($ProcessId -le 0) { return } # Skip self / critical system if ($ProcessId -eq $PID) { return } & taskkill.exe /F /T /PID $ProcessId 2>$null | Out-Null Stop-Process -Id $ProcessId -Force -ErrorAction SilentlyContinue } function Kill-Port([int]$Port) { $pids = @() Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue | ForEach-Object { $pids += [int]$_.OwningProcess } $net = & netstat.exe -ano 2>$null foreach ($line in $net) { if ($line -notmatch [regex]::Escape(":${Port}")) { continue } if ($line -notmatch "LISTENING") { continue } $parts = ($line -split "\s+") | Where-Object { $_ } $procId = 0 if ([int]::TryParse($parts[-1], [ref]$procId) -and $procId -gt 0) { $pids += $procId } } foreach ($procId in ($pids | Select-Object -Unique)) { try { $proc = Get-Process -Id $procId -ErrorAction Stop } catch { continue } Write-Info (" free port {0} (pid={1} {2})" -f $Port, $procId, $proc.ProcessName) Kill-Tree $procId } } function Kill-ByCommand([string]$ProcessName, [string]$Pattern) { # WMI/CIM 在 Docker 半挂或本机负载高时会卡死;用限时 Job,超时则跳过 $job = Start-Job -ScriptBlock { param($ProcessName, $Pattern) Get-CimInstance Win32_Process -Filter ("Name='{0}'" -f $ProcessName) -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -and ($_.CommandLine -match $Pattern) } | ForEach-Object { [int]$_.ProcessId } } -ArgumentList $ProcessName, $Pattern if (-not (Wait-Job $job -Timeout 5)) { Stop-Job $job -ErrorAction SilentlyContinue Remove-Job $job -Force -ErrorAction SilentlyContinue return } $ids = @(Receive-Job $job -ErrorAction SilentlyContinue) Remove-Job $job -Force -ErrorAction SilentlyContinue foreach ($procId in $ids) { if ($procId -gt 0) { Write-Info (" kill {0} pid={1}" -f $ProcessName, $procId) Kill-Tree $procId } } } # ----- 1) Docker first (do NOT return; native may still be running) ----- # Docker Desktop 半挂时 `docker info` 会无限阻塞,必须限时。 $dockerOk = $false try { $outFile = Join-Path $env:TEMP "aijz-docker-info.out" $errFile = Join-Path $env:TEMP "aijz-docker-info.err" $p = Start-Process -FilePath "docker" -ArgumentList @("info") -NoNewWindow -PassThru ` -RedirectStandardOutput $outFile -RedirectStandardError $errFile if (-not $p.WaitForExit(8000)) { try { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue } catch {} Write-Info " docker info timeout (8s); skip compose down" $dockerOk = $false } else { $dockerOk = ($p.ExitCode -eq 0) } } catch { $dockerOk = $false } if ($dockerOk) { Write-Info "==> docker compose down --remove-orphans" & docker compose down --remove-orphans 2>$null # also stop any leftover named containers from older compose projects $names = @("ai-web-1", "ai-ai-1", "ai-gateway-1", "ai-platform-1", "ai-postgres-1") foreach ($n in $names) { & docker rm -f $n 2>$null | Out-Null } } # ----- 2) Watchdog first (otherwise it revives ai) ----- Write-Info "==> stopping native processes..." $watchPidFile = Join-Path $LogDir "watchdog.pid" if (Test-Path $watchPidFile) { $wid = Get-Content $watchPidFile -ErrorAction SilentlyContinue if ($wid) { Write-Info (" kill watchdog pid={0}" -f $wid) Kill-Tree ([int]$wid) } Remove-Item $watchPidFile -Force -ErrorAction SilentlyContinue } Kill-ByCommand "powershell.exe" "watch-services\.ps1" Kill-ByCommand "pwsh.exe" "watch-services\.ps1" Kill-ByCommand "powershell.exe" "start-host\.ps1" Kill-ByCommand "powershell.exe" "Restart-AiOnly|restart_ai_only|hard_restart" # ----- 3) Pid files ----- foreach ($name in @("ai-service.pid", "platform.pid", "gateway.pid", "web.pid", "watchdog.pid")) { $pf = Join-Path $LogDir $name if (Test-Path $pf) { $pidText = Get-Content $pf -ErrorAction SilentlyContinue if ($pidText) { Write-Info (" kill {0} -> {1}" -f $name, $pidText) Kill-Tree ([int]$pidText) } Remove-Item $pf -Force -ErrorAction SilentlyContinue } } # ----- 4) By process name / cmdline ----- Get-Process -Name "platform", "gateway" -ErrorAction SilentlyContinue | ForEach-Object { Write-Info (" kill {0} pid={1}" -f $_.ProcessName, $_.Id) Kill-Tree ([int]$_.Id) } # uvicorn reloader = parent + workers Kill-ByCommand "python.exe" "uvicorn" Kill-ByCommand "python.exe" "app:app" Kill-ByCommand "python.exe" "shot_worker\.py" Kill-ByCommand "python.exe" "fidelity_loop|FIDELITY_SHOT" Kill-ByCommand "python.exe" "ai-service" # vite / node tooling for this repo Kill-ByCommand "node.exe" "vite" Kill-ByCommand "node.exe" "esbuild" Kill-ByCommand "cmd.exe" "npm.*vite|vite\.js" # Playwright Chromium leftovers from fidelity shots(限时,避免 CIM 卡死) Kill-ByCommand "chrome.exe" "ms-playwright|playwright" Kill-ByCommand "chromium.exe" "ms-playwright|playwright" # ----- 5) Ports (authoritative) ----- foreach ($port in 8888, 8001, 8180, 5173) { Kill-Port $port } Start-Sleep -Seconds 2 # second pass for stubborn reloaders Kill-ByCommand "python.exe" "uvicorn|app:app|shot_worker" Kill-ByCommand "node.exe" "vite" foreach ($port in 8888, 8001, 8180, 5173) { Kill-Port $port } Start-Sleep -Seconds 1 # ----- 6) Verify (only live processes count) ----- $still = @() foreach ($port in 8888, 8001, 8180, 5173) { Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue | ForEach-Object { $proc = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue if ($proc) { $still += (":{0} pid={1} {2}" -f $port, $_.OwningProcess, $proc.ProcessName) } } } if ($still.Count -gt 0) { Write-Host "WARN still listening after stop:" -ForegroundColor Yellow $still | ForEach-Object { Write-Host (" {0}" -f $_) } exit 1 } Write-Info "OK stopped (docker + native; ports 8888/8001/8180/5173 free)" exit 0