295 lines
9.1 KiB
PowerShell
295 lines
9.1 KiB
PowerShell
param(
|
|
[string]$Workspace = "D:\workspace\vignette",
|
|
[int]$ApiPort = 8001,
|
|
[int]$WebPort = 5174,
|
|
[int]$EnginePort = 9099,
|
|
[string]$Python = "$env:LOCALAPPDATA\Programs\Python\Python311\python.exe",
|
|
[string]$Cloudflared = "$env:LOCALAPPDATA\Microsoft\WinGet\Links\cloudflared.exe",
|
|
[string]$CloudflaredConfig = "$env:USERPROFILE\.cloudflared\vignette-config.yml",
|
|
[switch]$SkipEngineRestart,
|
|
[switch]$SkipWebRestart,
|
|
[switch]$RouteCloudflareDns,
|
|
[string]$CloudflareTunnelName = "vignette",
|
|
[switch]$SkipCloudflaredRestart
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
# 엔진 readiness 캐시 TTL. 기본 30초는 워치독 주기(5분)보다 짧아 매 헬스체크마다
|
|
# 실제 claude -p 생성을 새로 돌리게 만든다(재시작 폭풍의 근본 원인). 크게 늘려
|
|
# /ready 가 거의 항상 캐시를 반환하게 한다 → 헬스체크가 LLM 호출에 묶이지 않는다.
|
|
if (-not $env:ENGINE_READY_TTL_SECONDS) {
|
|
$env:ENGINE_READY_TTL_SECONDS = "1800"
|
|
}
|
|
|
|
$ApiDir = Join-Path $Workspace "apps\api"
|
|
$WebDir = Join-Path $Workspace "apps\web"
|
|
$OutLog = Join-Path $ApiDir "api.public.out.log"
|
|
$ErrLog = Join-Path $ApiDir "api.public.err.log"
|
|
$EngineOutLog = Join-Path $ApiDir "engine.public.out.log"
|
|
$EngineErrLog = Join-Path $ApiDir "engine.public.err.log"
|
|
$WebOutLog = Join-Path $Workspace "web.public.out.log"
|
|
$WebErrLog = Join-Path $Workspace "web.public.err.log"
|
|
|
|
function Get-JsonHealth {
|
|
param(
|
|
[string]$Uri,
|
|
[int]$TimeoutSec = 5
|
|
)
|
|
|
|
try {
|
|
Invoke-RestMethod -Uri $Uri -TimeoutSec $TimeoutSec
|
|
} catch {
|
|
$null
|
|
}
|
|
}
|
|
|
|
function Wait-JsonHealth {
|
|
param(
|
|
[string]$Uri,
|
|
[scriptblock]$IsHealthy,
|
|
[int]$TimeoutSec = 30
|
|
)
|
|
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
|
do {
|
|
$health = Get-JsonHealth -Uri $Uri -TimeoutSec 5
|
|
if ($null -ne $health -and (& $IsHealthy $health)) {
|
|
return $health
|
|
}
|
|
Start-Sleep -Seconds 1
|
|
} while ((Get-Date) -lt $deadline)
|
|
|
|
throw "Timed out waiting for healthy response from $Uri"
|
|
}
|
|
|
|
function Wait-HttpStatus {
|
|
param(
|
|
[string]$Uri,
|
|
[int]$TimeoutSec = 30
|
|
)
|
|
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
|
do {
|
|
try {
|
|
$response = Invoke-WebRequest -UseBasicParsing -Uri $Uri -TimeoutSec 5
|
|
if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 500) {
|
|
return $response
|
|
}
|
|
} catch {
|
|
Start-Sleep -Seconds 1
|
|
}
|
|
} while ((Get-Date) -lt $deadline)
|
|
|
|
throw "Timed out waiting for HTTP response from $Uri"
|
|
}
|
|
|
|
function Stop-UvicornByPort {
|
|
param(
|
|
[string]$AppImport,
|
|
[int]$Port
|
|
)
|
|
|
|
Get-CimInstance Win32_Process |
|
|
Where-Object {
|
|
$_.CommandLine -and
|
|
$_.CommandLine -like "*uvicorn $AppImport*" -and
|
|
$_.CommandLine -like "*--port $Port*"
|
|
} |
|
|
ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
|
|
}
|
|
|
|
function Stop-NodeByPortHint {
|
|
param([int]$Port)
|
|
|
|
Get-CimInstance Win32_Process |
|
|
Where-Object {
|
|
$_.Name -eq "node.exe" -and
|
|
$_.CommandLine -and
|
|
$_.CommandLine -like "*vite*preview*" -and
|
|
$_.CommandLine -like "*$Port*"
|
|
} |
|
|
ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
|
|
}
|
|
|
|
function ConvertTo-CompactJson {
|
|
param([object]$Value)
|
|
ConvertTo-Json -InputObject $Value -Compress
|
|
}
|
|
|
|
function Set-CloudflaredIngress {
|
|
param(
|
|
[string]$ConfigPath,
|
|
[string[]]$ApiHostnames,
|
|
[string[]]$WebHostnames,
|
|
[int]$ApiPortValue,
|
|
[int]$WebPortValue
|
|
)
|
|
|
|
$lines = Get-Content -Encoding UTF8 -Path $ConfigPath
|
|
$ingressIndex = -1
|
|
for ($i = 0; $i -lt $lines.Count; $i++) {
|
|
if ($lines[$i] -match "^\s*ingress:\s*$") {
|
|
$ingressIndex = $i
|
|
break
|
|
}
|
|
}
|
|
if ($ingressIndex -lt 0) {
|
|
throw "Could not find ingress: in $ConfigPath"
|
|
}
|
|
|
|
$nextLines = @()
|
|
if ($ingressIndex -gt 0) {
|
|
$nextLines += $lines[0..($ingressIndex - 1)]
|
|
}
|
|
$nextLines += "ingress:"
|
|
foreach ($hostname in $WebHostnames) {
|
|
$nextLines += " - hostname: $hostname"
|
|
$nextLines += " service: http://127.0.0.1:$WebPortValue"
|
|
}
|
|
foreach ($hostname in $ApiHostnames) {
|
|
$nextLines += " - hostname: $hostname"
|
|
$nextLines += " service: http://127.0.0.1:$ApiPortValue"
|
|
}
|
|
$nextLines += " - service: http_status:404"
|
|
|
|
Set-Content -Encoding UTF8 -Path $ConfigPath -Value $nextLines
|
|
}
|
|
|
|
if (!(Test-Path $Python)) {
|
|
throw "Python 3.11 not found at $Python"
|
|
}
|
|
if (!(Test-Path $ApiDir)) {
|
|
throw "API directory not found at $ApiDir"
|
|
}
|
|
if (!(Test-Path $WebDir)) {
|
|
throw "Web directory not found at $WebDir"
|
|
}
|
|
|
|
$engineHealth = Get-JsonHealth -Uri "http://127.0.0.1:$EnginePort/health"
|
|
if ($SkipEngineRestart) {
|
|
if ($null -eq $engineHealth -or -not $engineHealth.ok) {
|
|
throw "Engine gateway is not healthy on http://127.0.0.1:$EnginePort/health"
|
|
}
|
|
} elseif ($null -eq $engineHealth -or -not $engineHealth.ok) {
|
|
Stop-UvicornByPort -AppImport "engine_gateway.gateway:app" -Port $EnginePort
|
|
|
|
Start-Process -WindowStyle Hidden -FilePath $Python `
|
|
-ArgumentList @("-m", "uvicorn", "engine_gateway.gateway:app", "--host", "127.0.0.1", "--port", "$EnginePort") `
|
|
-WorkingDirectory $ApiDir `
|
|
-RedirectStandardOutput $EngineOutLog `
|
|
-RedirectStandardError $EngineErrLog `
|
|
-PassThru | Out-Null
|
|
|
|
$engineHealth = Wait-JsonHealth `
|
|
-Uri "http://127.0.0.1:$EnginePort/health" `
|
|
-IsHealthy { param($health) $health.ok -eq $true } `
|
|
-TimeoutSec 30
|
|
}
|
|
|
|
Stop-UvicornByPort -AppImport "app.main:app" -Port $ApiPort
|
|
|
|
$env:ENVIRONMENT = "prod"
|
|
$env:ENGINE_URL = "http://127.0.0.1:$EnginePort"
|
|
$env:ENGINE_MODE = "claude_cli"
|
|
$env:AUTH_DEV_LOGIN_ENABLED = "false"
|
|
$env:AUTO_SEED_PERSONAS = "false"
|
|
$env:ALLOW_SEED_PERSONA_FALLBACK = "false"
|
|
$env:VIGNETTE_VOICE_POC_SAMPLE_TTS = "false"
|
|
$env:FRONTEND_BASE_URL = "https://vignette.chanpaca.net"
|
|
$frontendOrigins = @("https://vignette.chanpaca.net", "https://vnet.18ka.net", "https://vignette-b1q.pages.dev")
|
|
$localViteOrigins = @()
|
|
foreach ($port in 5170..5180) {
|
|
$localViteOrigins += "http://localhost:$port"
|
|
$localViteOrigins += "http://127.0.0.1:$port"
|
|
}
|
|
$env:CORS_ORIGINS = ConvertTo-CompactJson -Value ($frontendOrigins + $localViteOrigins)
|
|
$env:FRONTEND_ORIGIN_MAP = ConvertTo-CompactJson -Value ([ordered]@{
|
|
"api-vignette.chanpaca.net" = "https://vignette.chanpaca.net"
|
|
"api-vnet.18ka.net" = "https://vnet.18ka.net"
|
|
})
|
|
|
|
$proc = Start-Process -WindowStyle Hidden -FilePath $Python `
|
|
-ArgumentList @("-m", "uvicorn", "app.main:app", "--host", "127.0.0.1", "--port", "$ApiPort") `
|
|
-WorkingDirectory $ApiDir `
|
|
-RedirectStandardOutput $OutLog `
|
|
-RedirectStandardError $ErrLog `
|
|
-PassThru
|
|
|
|
Start-Sleep -Seconds 3
|
|
$health = Wait-JsonHealth `
|
|
-Uri "http://127.0.0.1:$ApiPort/health" `
|
|
-IsHealthy { param($health) $health.environment -eq "prod" -and $health.db -and $health.engine } `
|
|
-TimeoutSec 30
|
|
if ($health.environment -ne "prod" -or -not $health.db -or -not $health.engine) {
|
|
throw "Public API health is not production-safe: $($health | ConvertTo-Json -Compress)"
|
|
}
|
|
|
|
if (!$SkipWebRestart) {
|
|
Stop-NodeByPortHint -Port $WebPort
|
|
|
|
$build = Start-Process -FilePath "cmd.exe" `
|
|
-ArgumentList @("/c", "npm run build") `
|
|
-WorkingDirectory $WebDir `
|
|
-NoNewWindow `
|
|
-Wait `
|
|
-PassThru
|
|
if ($build.ExitCode -ne 0) {
|
|
throw "Web build failed with exit code $($build.ExitCode)"
|
|
}
|
|
|
|
Start-Process -WindowStyle Hidden -FilePath "cmd.exe" `
|
|
-ArgumentList @("/c", "npm run preview -- --host 127.0.0.1 --port $WebPort") `
|
|
-WorkingDirectory $WebDir `
|
|
-RedirectStandardOutput $WebOutLog `
|
|
-RedirectStandardError $WebErrLog `
|
|
-PassThru | Out-Null
|
|
|
|
Wait-HttpStatus -Uri "http://127.0.0.1:$WebPort/" -TimeoutSec 30 | Out-Null
|
|
}
|
|
|
|
if (!$SkipCloudflaredRestart) {
|
|
if (!(Test-Path $Cloudflared)) {
|
|
throw "cloudflared not found at $Cloudflared"
|
|
}
|
|
if (!(Test-Path $CloudflaredConfig)) {
|
|
throw "cloudflared config not found at $CloudflaredConfig"
|
|
}
|
|
|
|
$apiHostnames = @("api-vignette.chanpaca.net", "api-vnet.18ka.net")
|
|
$webHostnames = @("vnet.18ka.net")
|
|
|
|
if ($RouteCloudflareDns) {
|
|
foreach ($hostname in ($webHostnames + $apiHostnames)) {
|
|
& $Cloudflared tunnel route dns $CloudflareTunnelName $hostname
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Warning "cloudflared DNS route failed for $hostname"
|
|
}
|
|
}
|
|
}
|
|
|
|
Set-CloudflaredIngress `
|
|
-ConfigPath $CloudflaredConfig `
|
|
-ApiHostnames $apiHostnames `
|
|
-WebHostnames $webHostnames `
|
|
-ApiPortValue $ApiPort `
|
|
-WebPortValue $WebPort
|
|
|
|
Get-CimInstance Win32_Process |
|
|
Where-Object { $_.Name -eq "cloudflared.exe" -and $_.CommandLine -like "*vignette-config.yml*" } |
|
|
ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
|
|
|
|
Start-Sleep -Seconds 2
|
|
Start-Process -WindowStyle Hidden -FilePath $Cloudflared `
|
|
-ArgumentList @("tunnel", "--config", $CloudflaredConfig, "run") `
|
|
-RedirectStandardOutput (Join-Path $Workspace "cloudflared.public.out.log") `
|
|
-RedirectStandardError (Join-Path $Workspace "cloudflared.public.err.log") `
|
|
-PassThru | Out-Null
|
|
}
|
|
|
|
Write-Output "Engine gateway healthy on http://127.0.0.1:$EnginePort"
|
|
Write-Output "Public API running on http://127.0.0.1:$ApiPort with PID $($proc.Id)"
|
|
if (!$SkipWebRestart) {
|
|
Write-Output "Public vnet web preview running on http://127.0.0.1:$WebPort"
|
|
}
|
|
Write-Output "Health: $($health | ConvertTo-Json -Compress)"
|