mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 04:20:45 +08:00
Some checks failed
Release Windows / build (push) Has been cancelled
0.4.5 venv pointed at the build machine user profile; other PCs failed to start API. Bundle stdlib under python/runtime, repair pyvenv.cfg, and health-check before start. Co-authored-by: Cursor <cursoragent@cursor.com>
348 lines
13 KiB
PowerShell
348 lines
13 KiB
PowerShell
param(
|
|
[string]$BindHost = "127.0.0.1",
|
|
[int]$Port = 8890,
|
|
[int]$WebPort = 5173,
|
|
[switch]$SkipInstall = $false,
|
|
[Alias("Bg")]
|
|
[switch]$Background = $false,
|
|
# Common typo for -Background (otherwise PowerShell ignores intent / or fails).
|
|
[Parameter(DontShow)]
|
|
[switch]$Backgtound = $false,
|
|
[switch]$WithWeb = $false,
|
|
# Keep collectors inside the API process (legacy). Default: split API + worker.
|
|
[switch]$InlineSchedulers = $false
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
if ($Backgtound -and -not $Background) {
|
|
Write-Host "[WARN] -Backgtound is a typo; treating as -Background." -ForegroundColor Yellow
|
|
$Background = $true
|
|
}
|
|
|
|
$projectRoot = Split-Path -Parent $PSScriptRoot
|
|
Set-Location $projectRoot
|
|
|
|
$packCommon = Join-Path $projectRoot "packaging\_common.ps1"
|
|
if (Test-Path -LiteralPath $packCommon) {
|
|
. $packCommon
|
|
if (Get-Command Repair-NetxShippedVenv -ErrorAction SilentlyContinue) {
|
|
$null = Repair-NetxShippedVenv -ProgramRoot $projectRoot
|
|
}
|
|
}
|
|
|
|
# Prefer writable data-root runtime dir (Program Files is not writable after Setup install).
|
|
if ($env:NETX_RUN_DIR) {
|
|
$runDir = $env:NETX_RUN_DIR
|
|
} elseif ($env:NETX_SCHEDULER_HEARTBEAT_PATH) {
|
|
$runDir = Split-Path -Parent ($env:NETX_SCHEDULER_HEARTBEAT_PATH -replace '/', '\')
|
|
} else {
|
|
$runDir = Join-Path $PSScriptRoot ".run"
|
|
}
|
|
if (-not (Test-Path $runDir)) {
|
|
New-Item -ItemType Directory -Path $runDir -Force | Out-Null
|
|
}
|
|
|
|
$pidFile = Join-Path $runDir "netx.pid"
|
|
$logFile = Join-Path $runDir "netx.out.log"
|
|
$errFile = Join-Path $runDir "netx.err.log"
|
|
$workerPidFile = Join-Path $runDir "worker.pid"
|
|
$workerLogFile = Join-Path $runDir "worker.out.log"
|
|
$workerErrFile = Join-Path $runDir "worker.err.log"
|
|
$bizStateWorkerPidDir = Join-Path $runDir "biz_state_workers"
|
|
$webPidFile = Join-Path $runDir "web.pid"
|
|
$webLogFile = Join-Path $runDir "web.out.log"
|
|
$webErrFile = Join-Path $runDir "web.err.log"
|
|
|
|
$venvPython = Join-Path $projectRoot ".venv\Scripts\python.exe"
|
|
if (Get-Command Test-NetxVenvRunnable -ErrorAction SilentlyContinue) {
|
|
if (-not (Test-NetxVenvRunnable -VenvPython $venvPython)) {
|
|
throw "venv_not_runnable: reinstall NetX or run setup as administrator"
|
|
}
|
|
} elseif (-not (Test-Path -LiteralPath $venvPython)) {
|
|
Write-Host "==> .venv not found, creating virtual environment"
|
|
python -m venv (Join-Path $projectRoot ".venv")
|
|
if (-not (Test-Path -LiteralPath $venvPython)) {
|
|
throw "failed_to_create_venv"
|
|
}
|
|
}
|
|
$pythonExe = $venvPython
|
|
|
|
Write-Host "==> Project root: $projectRoot"
|
|
Write-Host "==> Using python: $pythonExe"
|
|
|
|
if (-not $SkipInstall) {
|
|
Write-Host "==> Installing dependencies"
|
|
& $pythonExe -m pip install -r (Join-Path $projectRoot "requirements.txt")
|
|
} else {
|
|
Write-Host "==> Skip dependency install (if API fails with ModuleNotFoundError, run once without -SkipInstall)"
|
|
}
|
|
|
|
if ($WithWeb) {
|
|
$webRoot = Join-Path $projectRoot "web"
|
|
$webNodeModules = Join-Path $webRoot "node_modules"
|
|
if (-not (Get-Command "npm.cmd" -ErrorAction SilentlyContinue)) {
|
|
throw "npm_not_found: install Node.js (includes npm) and reopen terminal"
|
|
}
|
|
$needWebInstall = (-not (Test-Path $webNodeModules)) -or (-not $SkipInstall)
|
|
if ($needWebInstall) {
|
|
Write-Host "==> Installing web dependencies (npm install)"
|
|
& npm.cmd install --prefix $webRoot
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "npm_install_failed"
|
|
}
|
|
} else {
|
|
Write-Host "==> Skip web dependency install (node_modules exists)"
|
|
}
|
|
}
|
|
|
|
$env:NETX_HOST = $BindHost
|
|
$env:NETX_PORT = "$Port"
|
|
# netx_api is a source tree (not always pip -e installed); ensure imports work from any launcher cwd.
|
|
$env:PYTHONPATH = $projectRoot
|
|
# Split collectors from API so HTTP/WebCRT stay responsive under multi-user load.
|
|
# Override with -InlineSchedulers to keep legacy single-process mode.
|
|
if ($InlineSchedulers) {
|
|
$env:NETX_RUN_INLINE_SCHEDULERS = "true"
|
|
} else {
|
|
$env:NETX_RUN_INLINE_SCHEDULERS = "false"
|
|
}
|
|
$baseUrl = "http://$BindHost`:$Port"
|
|
$webUrl = "http://$BindHost`:$WebPort"
|
|
|
|
Write-Host ""
|
|
Write-Host "==> netx API URL"
|
|
Write-Host "Base: $baseUrl/"
|
|
Write-Host "Health: $baseUrl/health"
|
|
Write-Host "Ready: $baseUrl/health/ready"
|
|
Write-Host "Metrics: $baseUrl/metrics"
|
|
Write-Host "Integrations: $baseUrl/v1/integrations/status"
|
|
if (-not $InlineSchedulers) {
|
|
Write-Host "Schedulers: external worker (auto-started by this script)"
|
|
} else {
|
|
Write-Host "Schedulers: inline with API (-InlineSchedulers)"
|
|
}
|
|
if ($WithWeb) {
|
|
Write-Host ""
|
|
Write-Host "==> netx UI URL"
|
|
Write-Host "Vite Dev UI: $webUrl/"
|
|
}
|
|
Write-Host ""
|
|
|
|
function Show-LogTail {
|
|
param([string]$Path, [int]$Lines = 40)
|
|
if (Test-Path $Path) {
|
|
$tail = Get-Content -Path $Path -Tail $Lines -ErrorAction SilentlyContinue
|
|
if ($tail) {
|
|
Write-Host "--- tail $Path ---"
|
|
$tail | ForEach-Object { Write-Host $_ }
|
|
}
|
|
}
|
|
}
|
|
|
|
function Test-NetxApiListening {
|
|
param([string]$HostName, [int]$LocalPort, [int]$WaitSec = 45)
|
|
$deadline = (Get-Date).AddSeconds($WaitSec)
|
|
$healthUrl = "http://${HostName}:${LocalPort}/health"
|
|
while ((Get-Date) -lt $deadline) {
|
|
try {
|
|
# Only HTTP 200 with {"status":"ok"} counts; no redirects / other codes.
|
|
$r = Invoke-WebRequest -Uri $healthUrl -UseBasicParsing -TimeoutSec 2 -MaximumRedirection 0
|
|
if ($r.StatusCode -ne 200) {
|
|
Start-Sleep -Milliseconds 500
|
|
continue
|
|
}
|
|
$body = $r.Content | ConvertFrom-Json -ErrorAction Stop
|
|
if ($body.status -eq "ok") { return $true }
|
|
} catch {}
|
|
Start-Sleep -Milliseconds 500
|
|
}
|
|
return $false
|
|
}
|
|
|
|
function Reset-LogFile {
|
|
param([string]$Path)
|
|
try {
|
|
Set-Content -Path $Path -Value "" -Encoding utf8 -ErrorAction Stop
|
|
return $Path
|
|
} catch {
|
|
$alt = "$Path.$(Get-Date -Format 'yyyyMMdd_HHmmss')"
|
|
Write-Host "[WARN] cannot truncate $Path ($($_.Exception.Message))" -ForegroundColor Yellow
|
|
Write-Host " using $alt (old worker may still hold the previous file; run stop_netx.ps1 elevated)" -ForegroundColor Yellow
|
|
return $alt
|
|
}
|
|
}
|
|
|
|
function Start-NetxWorker {
|
|
if ($InlineSchedulers) {
|
|
return
|
|
}
|
|
$outLog = Reset-LogFile -Path $workerLogFile
|
|
$errLog = Reset-LogFile -Path $workerErrFile
|
|
Write-Host "==> Starting netx worker (config_sync / lldp / port_traffic)"
|
|
$workerProc = Start-Process -FilePath $pythonExe `
|
|
-ArgumentList @("-m", "netx_api.worker") `
|
|
-WorkingDirectory $projectRoot `
|
|
-WindowStyle Hidden `
|
|
-RedirectStandardOutput $outLog `
|
|
-RedirectStandardError $errLog `
|
|
-PassThru
|
|
Set-Content -Path $workerPidFile -Value "$($workerProc.Id)"
|
|
Write-Host "worker.pid = $workerPidFile"
|
|
Write-Host "PID = $($workerProc.Id)"
|
|
Write-Host "Log = $outLog"
|
|
Write-Host "Err = $errLog"
|
|
Start-Sleep -Seconds 1
|
|
$alive = $false
|
|
try {
|
|
$alive = -not $workerProc.HasExited
|
|
} catch {
|
|
$alive = $false
|
|
}
|
|
if (-not $alive) {
|
|
Write-Host "[ERR] netx worker exited immediately (PID $($workerProc.Id))." -ForegroundColor Red
|
|
Show-LogTail -Path $workerErrFile
|
|
Show-LogTail -Path $workerLogFile
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
function Start-BizStateWorkers {
|
|
if ($InlineSchedulers) {
|
|
return
|
|
}
|
|
$replicas = 2
|
|
if ($env:NETX_BIZ_STATE_WORKER_REPLICAS) {
|
|
try { $replicas = [int]$env:NETX_BIZ_STATE_WORKER_REPLICAS } catch { $replicas = 2 }
|
|
}
|
|
if ($replicas -lt 1) { $replicas = 1 }
|
|
if (-not (Test-Path $bizStateWorkerPidDir)) {
|
|
New-Item -ItemType Directory -Path $bizStateWorkerPidDir | Out-Null
|
|
}
|
|
Write-Host "==> Starting biz_state workers (replicas=$replicas)"
|
|
for ($i = 0; $i -lt $replicas; $i++) {
|
|
$outLog = Reset-LogFile -Path (Join-Path $bizStateWorkerPidDir "worker$i.out.log")
|
|
$errLog = Reset-LogFile -Path (Join-Path $bizStateWorkerPidDir "worker$i.err.log")
|
|
$pidPath = Join-Path $bizStateWorkerPidDir "worker$i.pid"
|
|
$proc = Start-Process -FilePath $pythonExe `
|
|
-ArgumentList @("-m", "netx_api.biz_state_worker") `
|
|
-WorkingDirectory $projectRoot `
|
|
-WindowStyle Hidden `
|
|
-RedirectStandardOutput $outLog `
|
|
-RedirectStandardError $errLog `
|
|
-PassThru
|
|
Set-Content -Path $pidPath -Value "$($proc.Id)"
|
|
Write-Host "biz_state_worker[$i] PID=$($proc.Id)"
|
|
Start-Sleep -Milliseconds 400
|
|
if ($proc.HasExited) {
|
|
Write-Host "[ERR] biz_state_worker[$i] exited immediately." -ForegroundColor Red
|
|
Show-LogTail -Path $errLog
|
|
exit 1
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($Background) {
|
|
# Truncate logs so a failed start is not confused with an old run.
|
|
Set-Content -Path $logFile -Value "" -Encoding utf8
|
|
Set-Content -Path $errFile -Value "" -Encoding utf8
|
|
Write-Host "==> Starting netx API in background"
|
|
$proc = Start-Process -FilePath $pythonExe `
|
|
-ArgumentList @("-m", "netx_api.main") `
|
|
-WorkingDirectory $projectRoot `
|
|
-WindowStyle Hidden `
|
|
-RedirectStandardOutput $logFile `
|
|
-RedirectStandardError $errFile `
|
|
-PassThru
|
|
Set-Content -Path $pidFile -Value "$($proc.Id)"
|
|
Write-Host "netx.pid = $pidFile"
|
|
Write-Host "PID = $($proc.Id)"
|
|
Write-Host "Log = $logFile"
|
|
Write-Host "Err = $errFile"
|
|
Start-Sleep -Seconds 2
|
|
$alive = $false
|
|
try {
|
|
$alive = -not $proc.HasExited
|
|
} catch {
|
|
$alive = $false
|
|
}
|
|
if (-not $alive) {
|
|
Write-Host "[ERR] netx process exited immediately (PID $($proc.Id))." -ForegroundColor Red
|
|
Show-LogTail -Path $errFile
|
|
Show-LogTail -Path $logFile
|
|
exit 1
|
|
}
|
|
# Fresh installs run Alembic upgrades on first boot; 45s is often too short.
|
|
$healthWaitSec = 180
|
|
if ($env:NETX_START_HEALTH_WAIT_SEC) {
|
|
try { $healthWaitSec = [int]$env:NETX_START_HEALTH_WAIT_SEC } catch {}
|
|
}
|
|
if (-not (Test-NetxApiListening -HostName $BindHost -LocalPort $Port -WaitSec $healthWaitSec)) {
|
|
Write-Host "[ERR] Port $Port not listening /health not OK within ${healthWaitSec}s (process may be stuck on DB or schema migration)." -ForegroundColor Red
|
|
Show-LogTail -Path $errFile
|
|
Show-LogTail -Path $logFile
|
|
exit 1
|
|
}
|
|
Write-Host "==> netx API ready: http://${BindHost}:${Port}/health" -ForegroundColor Green
|
|
Start-NetxWorker
|
|
Start-BizStateWorkers
|
|
if ($WithWeb) {
|
|
Write-Host "==> Starting Vite dev server in background"
|
|
$webRoot = Join-Path $projectRoot "web"
|
|
$webProc = Start-Process -FilePath "npm.cmd" `
|
|
-ArgumentList @("run", "dev", "--", "--host", $BindHost, "--port", "$WebPort") `
|
|
-WorkingDirectory $webRoot `
|
|
-WindowStyle Hidden `
|
|
-RedirectStandardOutput $webLogFile `
|
|
-RedirectStandardError $webErrFile `
|
|
-PassThru
|
|
Set-Content -Path $webPidFile -Value "$($webProc.Id)"
|
|
Write-Host "web.pid = $webPidFile"
|
|
Write-Host "PID = $($webProc.Id)"
|
|
Write-Host "Log = $webLogFile"
|
|
Write-Host "Err = $webErrFile"
|
|
}
|
|
Write-Host ""
|
|
Write-Host "==> Background services started; this script will exit now." -ForegroundColor Cyan
|
|
Write-Host " API/worker/web keep running in the background — you can close this terminal." -ForegroundColor Cyan
|
|
Write-Host " API log: $logFile" -ForegroundColor DarkGray
|
|
Write-Host " Err log: $errFile" -ForegroundColor DarkGray
|
|
if (-not $InlineSchedulers) {
|
|
Write-Host " Worker: $workerLogFile" -ForegroundColor DarkGray
|
|
}
|
|
if ($WithWeb) {
|
|
Write-Host " Web log: $webLogFile" -ForegroundColor DarkGray
|
|
Write-Host " UI: $webUrl/" -ForegroundColor Green
|
|
}
|
|
Write-Host " Stop: .\scripts\stop_netx.ps1" -ForegroundColor DarkGray
|
|
exit 0
|
|
}
|
|
|
|
if (-not $Background) {
|
|
Write-Host "==> Foreground mode (no -Background): this terminal stays attached to the API." -ForegroundColor Yellow
|
|
Write-Host " Schedule/UME logs will keep printing here until you Ctrl+C." -ForegroundColor Yellow
|
|
Write-Host " For detach: .\scripts\start_netx.ps1 -Background -WithWeb" -ForegroundColor Yellow
|
|
Write-Host ""
|
|
}
|
|
|
|
if ($WithWeb) {
|
|
Write-Host "==> Starting Vite dev server in background (foreground API mode)"
|
|
$webRoot = Join-Path $projectRoot "web"
|
|
$webProc = Start-Process -FilePath "npm.cmd" `
|
|
-ArgumentList @("run", "dev", "--", "--host", $BindHost, "--port", "$WebPort") `
|
|
-WorkingDirectory $webRoot `
|
|
-WindowStyle Hidden `
|
|
-RedirectStandardOutput $webLogFile `
|
|
-RedirectStandardError $webErrFile `
|
|
-PassThru
|
|
Set-Content -Path $webPidFile -Value "$($webProc.Id)"
|
|
Write-Host "web.pid = $webPidFile"
|
|
Write-Host "PID = $($webProc.Id)"
|
|
Write-Host "Log = $webLogFile"
|
|
Write-Host "Err = $webErrFile"
|
|
}
|
|
|
|
Start-NetxWorker
|
|
Start-BizStateWorkers
|
|
Write-Host "==> Starting netx API in foreground"
|
|
& $pythonExe -m netx_api.main
|