mirror of
https://github.com/hansjone/netx.git
synced 2026-10-12 23:33:14 +08:00
Harden Windows packaging: data-root paths, service health, safer updates.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
d0b260b67d
commit
d10d47e8f5
10 changed files with 228 additions and 56 deletions
|
|
@ -125,6 +125,7 @@ Both sides should publish the same release assets (`NetX-*-win64.zip`). Code mir
|
||||||
## Windows Service (admin)
|
## Windows Service (admin)
|
||||||
|
|
||||||
Uses [WinSW](https://github.com/winsw/winsw) (downloaded on first install into `packaging/cache`).
|
Uses [WinSW](https://github.com/winsw/winsw) (downloaded on first install into `packaging/cache`).
|
||||||
|
`service_run.ps1` probes `/health` and restarts children after consecutive failures.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Elevated PowerShell
|
# Elevated PowerShell
|
||||||
|
|
|
||||||
|
|
@ -135,3 +135,72 @@ function Import-NetxEnvFile {
|
||||||
}
|
}
|
||||||
return $map
|
return $map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ConvertTo-NetxFsPath([string]$Path) {
|
||||||
|
# Forward slashes work in .env and most Windows APIs via Python pathlib.
|
||||||
|
return ($Path -replace '\\', '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-NetxDataEnvMap {
|
||||||
|
param([string]$DataRoot)
|
||||||
|
$d = $DataRoot
|
||||||
|
return @{
|
||||||
|
"NETX_AUTH_MCP_TOKEN_FILE" = (ConvertTo-NetxFsPath (Join-Path $d "data\auth\mcp_token"))
|
||||||
|
"NETX_AUTH_SECRET_FILE" = (ConvertTo-NetxFsPath (Join-Path $d "data\auth\jwt_secret"))
|
||||||
|
"NETX_SCHEDULER_HEARTBEAT_PATH" = (ConvertTo-NetxFsPath (Join-Path $d "data\runtime\scheduler_heartbeat.json"))
|
||||||
|
"NETX_BIZ_STATE_SPOOL_DIR" = (ConvertTo-NetxFsPath (Join-Path $d "data\biz_state_spool"))
|
||||||
|
"NETX_NE_COLLECTION_DATA_DIR" = (ConvertTo-NetxFsPath (Join-Path $d "data\ne_collections"))
|
||||||
|
"NETX_NE_EXEC_JOB_DIR" = (ConvertTo-NetxFsPath (Join-Path $d "data\ne_exec_jobs"))
|
||||||
|
"NETX_WEBCRT_DATA_DIR" = (ConvertTo-NetxFsPath (Join-Path $d "data\webcrt"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-NetxDataDirectories {
|
||||||
|
param([string]$DataRoot)
|
||||||
|
$subs = @(
|
||||||
|
"data", "data\auth", "data\runtime", "data\biz_state_spool",
|
||||||
|
"data\ne_collections", "data\ne_exec_jobs", "data\webcrt",
|
||||||
|
"backups", "pgdata"
|
||||||
|
)
|
||||||
|
foreach ($sub in $subs) {
|
||||||
|
$p = Join-Path $DataRoot $sub
|
||||||
|
if (-not (Test-Path $p)) {
|
||||||
|
New-Item -ItemType Directory -Path $p -Force | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-NetxTcpPortFree {
|
||||||
|
param([string]$HostName = "127.0.0.1", [int]$Port)
|
||||||
|
try {
|
||||||
|
$client = New-Object System.Net.Sockets.TcpClient
|
||||||
|
$iar = $client.BeginConnect($HostName, $Port, $null, $null)
|
||||||
|
$ok = $iar.AsyncWaitHandle.WaitOne(400)
|
||||||
|
if ($ok -and $client.Connected) {
|
||||||
|
$client.Close()
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
$client.Close()
|
||||||
|
return $true
|
||||||
|
} catch {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-NetxApiHealthy {
|
||||||
|
param([string]$HostName = "127.0.0.1", [int]$Port = 8890, [int]$TimeoutSec = 2)
|
||||||
|
try {
|
||||||
|
$url = "http://${HostName}:${Port}/health"
|
||||||
|
$r = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec $TimeoutSec -MaximumRedirection 0
|
||||||
|
if ($r.StatusCode -ne 200) { return $false }
|
||||||
|
$body = $r.Content | ConvertFrom-Json -ErrorAction Stop
|
||||||
|
return ($body.status -eq "ok")
|
||||||
|
} catch {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-NetxSqlLiteral([string]$Value) {
|
||||||
|
# Single-quote escaping for PostgreSQL string literals.
|
||||||
|
return ($Value -replace "'", "''")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -316,7 +316,14 @@ try {
|
||||||
$zipName = "NetX-$latest-win64.zip"
|
$zipName = "NetX-$latest-win64.zip"
|
||||||
$zipPath = Join-Path $dlDir $zipName
|
$zipPath = Join-Path $dlDir $zipName
|
||||||
Write-Host "==> Downloading $zipName from $($result.source) ..."
|
Write-Host "==> Downloading $zipName from $($result.source) ..."
|
||||||
$dlHeaders = Get-AuthHeaders -Style "token"
|
# Only attach token for non-GitHub hosts (Forgejo/private). Public GitHub assets need no auth;
|
||||||
|
# a Forgejo token would break anonymous GitHub downloads.
|
||||||
|
$dlHeaders = @{ "User-Agent" = "NetX-UpdateCheck" }
|
||||||
|
$dlUri = [uri]$result.download_url
|
||||||
|
$isGithub = ($dlUri.Host -match '(^|\.)github\.com$' -or $dlUri.Host -match '(^|\.)githubusercontent\.com$')
|
||||||
|
if ($UpdateToken -and -not $isGithub) {
|
||||||
|
$dlHeaders["Authorization"] = "token $UpdateToken"
|
||||||
|
}
|
||||||
Invoke-WebRequest -Uri $result.download_url -OutFile $zipPath -Headers $dlHeaders -UseBasicParsing
|
Invoke-WebRequest -Uri $result.download_url -OutFile $zipPath -Headers $dlHeaders -UseBasicParsing
|
||||||
if ($result.sha256 -and $result.sha256 -notmatch 'REPLACE' -and $result.sha256.Trim()) {
|
if ($result.sha256 -and $result.sha256 -notmatch 'REPLACE' -and $result.sha256.Trim()) {
|
||||||
$hash = (Get-FileHash -Path $zipPath -Algorithm SHA256).Hash.ToLowerInvariant()
|
$hash = (Get-FileHash -Path $zipPath -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
#define MyAppName "NetX"
|
#define MyAppName "NetX"
|
||||||
#ifndef MyAppVersion
|
#ifndef MyAppVersion
|
||||||
#define MyAppVersion "0.3.0"
|
#define MyAppVersion "0.4.0"
|
||||||
#endif
|
#endif
|
||||||
#define MyAppPublisher "NetX"
|
#define MyAppPublisher "NetX"
|
||||||
#define MyAppURL "https://github.com/hansjone/netx"
|
#define MyAppURL "https://github.com/hansjone/netx"
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"channel": "stable",
|
"channel": "stable",
|
||||||
"latest": "0.3.0",
|
"latest": "0.4.0",
|
||||||
"min_compatible": "0.3.0",
|
"min_compatible": "0.3.0",
|
||||||
"notes_url": "https://github.com/hansjone/netx/releases/tag/v0.3.0",
|
"notes_url": "https://github.com/hansjone/netx/releases/tag/v0.4.0",
|
||||||
"windows": {
|
"windows": {
|
||||||
"url": "https://github.com/hansjone/netx/releases/download/v0.3.0/NetX-0.3.0-win64.zip",
|
"url": "https://github.com/hansjone/netx/releases/download/v0.4.0/NetX-0.4.0-win64.zip",
|
||||||
"setup_url": "https://github.com/hansjone/netx/releases/download/v0.3.0/NetX-Setup-0.3.0.exe",
|
"setup_url": "https://github.com/hansjone/netx/releases/download/v0.4.0/NetX-Setup-0.4.0.exe",
|
||||||
"sha256": "",
|
"sha256": "",
|
||||||
"size": 0
|
"size": 0
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
param(
|
param(
|
||||||
[string]$ProgramRoot = "",
|
[string]$ProgramRoot = "",
|
||||||
[string]$DataRoot = ""
|
[string]$DataRoot = "",
|
||||||
|
[int]$HealthFailLimit = 6,
|
||||||
|
[int]$HealthIntervalSec = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
# Long-running supervisor for Windows Service / Task Scheduler.
|
# Long-running supervisor for Windows Service / Task Scheduler.
|
||||||
# Starts NetX (and bundled PG), waits until stopped, then tears down.
|
# Starts NetX, probes /health; repeated failures trigger restart (or exit for WinSW).
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
. "$PSScriptRoot\_common.ps1"
|
. "$PSScriptRoot\_common.ps1"
|
||||||
|
|
@ -16,6 +18,7 @@ if (-not (Test-Path $logDir)) {
|
||||||
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
|
||||||
}
|
}
|
||||||
$logFile = Join-Path $logDir "service_run.log"
|
$logFile = Join-Path $logDir "service_run.log"
|
||||||
|
$stopFlag = Join-Path $logDir "service.stop"
|
||||||
|
|
||||||
function Write-SvcLog([string]$Msg) {
|
function Write-SvcLog([string]$Msg) {
|
||||||
$line = "{0} {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Msg
|
$line = "{0} {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Msg
|
||||||
|
|
@ -23,40 +26,75 @@ function Write-SvcLog([string]$Msg) {
|
||||||
Write-Host $line
|
Write-Host $line
|
||||||
}
|
}
|
||||||
|
|
||||||
$stopFlag = Join-Path $logDir "service.stop"
|
function Get-BindInfo {
|
||||||
Remove-Item -Force $stopFlag -ErrorAction SilentlyContinue
|
$envPath = Join-Path $data ".env"
|
||||||
|
$hostBind = "127.0.0.1"
|
||||||
|
$port = 8890
|
||||||
|
if (Test-Path $envPath) {
|
||||||
|
$map = Read-DotEnv -Path $envPath
|
||||||
|
if ($map["NETX_HOST"]) { $hostBind = $map["NETX_HOST"] }
|
||||||
|
if ($map["NETX_PORT"]) { try { $port = [int]$map["NETX_PORT"] } catch {} }
|
||||||
|
}
|
||||||
|
return @{ Host = $hostBind; Port = $port }
|
||||||
|
}
|
||||||
|
|
||||||
|
Remove-Item -Force $stopFlag -ErrorAction SilentlyContinue
|
||||||
Write-SvcLog "service_run starting program=$prog data=$data"
|
Write-SvcLog "service_run starting program=$prog data=$data"
|
||||||
|
|
||||||
$startPs1 = Join-Path $PSScriptRoot "start_netx_app.ps1"
|
$startPs1 = Join-Path $PSScriptRoot "start_netx_app.ps1"
|
||||||
$stopPs1 = Join-Path $PSScriptRoot "stop_netx_app.ps1"
|
$stopPs1 = Join-Path $PSScriptRoot "stop_netx_app.ps1"
|
||||||
|
$bind = Get-BindInfo
|
||||||
|
$failStreak = 0
|
||||||
|
$restartCount = 0
|
||||||
|
|
||||||
try {
|
function Start-NetxChildren {
|
||||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $startPs1 `
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $startPs1 `
|
||||||
-ProgramRoot $prog -DataRoot $data -SkipBrowser
|
-ProgramRoot $prog -DataRoot $data -SkipBrowser -Force
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
throw "start_netx_app_failed exit=$LASTEXITCODE"
|
throw "start_netx_app_failed exit=$LASTEXITCODE"
|
||||||
}
|
}
|
||||||
Write-SvcLog "NetX started; entering watch loop"
|
}
|
||||||
|
|
||||||
|
function Stop-NetxChildren {
|
||||||
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $stopPs1 `
|
||||||
|
-ProgramRoot $prog -DataRoot $data
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Start-NetxChildren
|
||||||
|
Write-SvcLog "NetX started; health watch host=$($bind.Host) port=$($bind.Port)"
|
||||||
|
|
||||||
while ($true) {
|
while ($true) {
|
||||||
if (Test-Path $stopFlag) {
|
if (Test-Path $stopFlag) {
|
||||||
Write-SvcLog "stop flag detected"
|
Write-SvcLog "stop flag detected"
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
Start-Sleep -Seconds 5
|
|
||||||
|
if (Test-NetxApiHealthy -HostName $bind.Host -Port $bind.Port -TimeoutSec 3) {
|
||||||
|
$failStreak = 0
|
||||||
|
} else {
|
||||||
|
$failStreak++
|
||||||
|
Write-SvcLog "health fail streak=$failStreak/$HealthFailLimit"
|
||||||
|
if ($failStreak -ge $HealthFailLimit) {
|
||||||
|
$restartCount++
|
||||||
|
Write-SvcLog "restarting NetX (attempt=$restartCount)"
|
||||||
|
try { Stop-NetxChildren } catch { Write-SvcLog "stop warning: $($_.Exception.Message)" }
|
||||||
|
Start-Sleep -Seconds 3
|
||||||
|
Start-NetxChildren
|
||||||
|
$failStreak = 0
|
||||||
|
# Give API time to come up before counting failures again.
|
||||||
|
Start-Sleep -Seconds ([Math]::Max(15, $HealthIntervalSec))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Start-Sleep -Seconds $HealthIntervalSec
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
Write-SvcLog "ERROR: $($_.Exception.Message)"
|
Write-SvcLog "ERROR: $($_.Exception.Message)"
|
||||||
throw
|
throw
|
||||||
} finally {
|
} finally {
|
||||||
Write-SvcLog "stopping NetX"
|
Write-SvcLog "stopping NetX"
|
||||||
try {
|
try { Stop-NetxChildren } catch { Write-SvcLog "stop warning: $($_.Exception.Message)" }
|
||||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $stopPs1 `
|
|
||||||
-ProgramRoot $prog -DataRoot $data
|
|
||||||
} catch {
|
|
||||||
Write-SvcLog "stop warning: $($_.Exception.Message)"
|
|
||||||
}
|
|
||||||
Remove-Item -Force $stopFlag -ErrorAction SilentlyContinue
|
Remove-Item -Force $stopFlag -ErrorAction SilentlyContinue
|
||||||
Write-SvcLog "service_run exited"
|
Write-SvcLog "service_run exited"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,15 +20,7 @@ Write-Host "==> Program root: $prog"
|
||||||
Write-Host "==> Data root: $data"
|
Write-Host "==> Data root: $data"
|
||||||
Write-Host "==> Env file: $envPath"
|
Write-Host "==> Env file: $envPath"
|
||||||
|
|
||||||
if (-not (Test-Path $data)) {
|
Ensure-NetxDataDirectories -DataRoot $data
|
||||||
New-Item -ItemType Directory -Path $data -Force | Out-Null
|
|
||||||
}
|
|
||||||
foreach ($sub in @("data", "data\auth", "data\runtime", "backups", "pgdata")) {
|
|
||||||
$p = Join-Path $data $sub
|
|
||||||
if (-not (Test-Path $p)) {
|
|
||||||
New-Item -ItemType Directory -Path $p -Force | Out-Null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$existing = Read-DotEnv -Path $envPath
|
$existing = Read-DotEnv -Path $envPath
|
||||||
if (-not $DbMode) {
|
if (-not $DbMode) {
|
||||||
|
|
@ -58,16 +50,21 @@ $values = @{}
|
||||||
$values["NETX_DB_MODE"] = $DbMode
|
$values["NETX_DB_MODE"] = $DbMode
|
||||||
$values["NETX_HOST"] = "127.0.0.1"
|
$values["NETX_HOST"] = "127.0.0.1"
|
||||||
$values["NETX_PORT"] = "8890"
|
$values["NETX_PORT"] = "8890"
|
||||||
$values["NETX_UI_DIST_DIR"] = "web/dist"
|
# Absolute UI path when present; else relative for source trees.
|
||||||
|
$distAbs = Join-Path $prog "web\dist"
|
||||||
|
if (Test-Path (Join-Path $distAbs "index.html")) {
|
||||||
|
$values["NETX_UI_DIST_DIR"] = (ConvertTo-NetxFsPath $distAbs)
|
||||||
|
} else {
|
||||||
|
$values["NETX_UI_DIST_DIR"] = "web/dist"
|
||||||
|
}
|
||||||
|
|
||||||
# Point runtime data dirs into the data root (absolute).
|
# All runtime data under data root (never under Program Files).
|
||||||
$values["NETX_AUTH_MCP_TOKEN_FILE"] = ((Join-Path $data "data\auth\mcp_token") -replace '\\', '/')
|
$dataEnv = Get-NetxDataEnvMap -DataRoot $data
|
||||||
$values["NETX_SCHEDULER_HEARTBEAT_PATH"] = ((Join-Path $data "data\runtime\scheduler_heartbeat.json") -replace '\\', '/')
|
foreach ($k in $dataEnv.Keys) { $values[$k] = $dataEnv[$k] }
|
||||||
|
|
||||||
if ($DbMode -eq "bundled") {
|
if ($DbMode -eq "bundled") {
|
||||||
$pgsql = Join-Path $prog "postgres\pgsql"
|
$pgsql = Join-Path $prog "postgres\pgsql"
|
||||||
if (-not (Test-Path (Join-Path $pgsql "bin\initdb.exe"))) {
|
if (-not (Test-Path (Join-Path $pgsql "bin\initdb.exe"))) {
|
||||||
# In-repo layout
|
|
||||||
$alt = Join-Path $PSScriptRoot "postgres\pgsql"
|
$alt = Join-Path $PSScriptRoot "postgres\pgsql"
|
||||||
if (Test-Path (Join-Path $alt "bin\initdb.exe")) {
|
if (Test-Path (Join-Path $alt "bin\initdb.exe")) {
|
||||||
$pgsql = $alt
|
$pgsql = $alt
|
||||||
|
|
@ -94,18 +91,16 @@ if ($DbMode -eq "bundled") {
|
||||||
}
|
}
|
||||||
$pgData = Join-Path $data "pgdata"
|
$pgData = Join-Path $data "pgdata"
|
||||||
$values["NETX_BUNDLED_PG_PORT"] = "$BundledPort"
|
$values["NETX_BUNDLED_PG_PORT"] = "$BundledPort"
|
||||||
$values["NETX_BUNDLED_PG_DATA_DIR"] = ($pgData -replace '\\', '/')
|
$values["NETX_BUNDLED_PG_DATA_DIR"] = (ConvertTo-NetxFsPath $pgData)
|
||||||
$pwFile = Join-Path $data "pg_netx.pw"
|
$pwFile = Join-Path $data "pg_netx.pw"
|
||||||
Set-Content -Path $pwFile -Value $BundledPassword -Encoding ascii -NoNewline
|
Set-Content -Path $pwFile -Value $BundledPassword -Encoding ascii -NoNewline
|
||||||
$encPw = [uri]::EscapeDataString($BundledPassword)
|
$encPw = [uri]::EscapeDataString($BundledPassword)
|
||||||
$values["NETX_DATABASE_URL"] = "postgresql+psycopg://netx:${encPw}@127.0.0.1:${BundledPort}/netx"
|
$values["NETX_DATABASE_URL"] = "postgresql+psycopg://netx:${encPw}@127.0.0.1:${BundledPort}/netx"
|
||||||
|
|
||||||
# Initialize cluster if needed
|
|
||||||
$initdb = Join-Path $pgsql "bin\initdb.exe"
|
$initdb = Join-Path $pgsql "bin\initdb.exe"
|
||||||
$pgCtl = Join-Path $pgsql "bin\pg_ctl.exe"
|
$pgCtl = Join-Path $pgsql "bin\pg_ctl.exe"
|
||||||
$psql = Join-Path $pgsql "bin\psql.exe"
|
$psql = Join-Path $pgsql "bin\psql.exe"
|
||||||
$createdb = Join-Path $pgsql "bin\createdb.exe"
|
$sqlPw = ConvertTo-NetxSqlLiteral $BundledPassword
|
||||||
$createuser = Join-Path $pgsql "bin\createuser.exe"
|
|
||||||
|
|
||||||
if (-not (Test-Path (Join-Path $pgData "PG_VERSION"))) {
|
if (-not (Test-Path (Join-Path $pgData "PG_VERSION"))) {
|
||||||
Write-Host "==> initdb $pgData"
|
Write-Host "==> initdb $pgData"
|
||||||
|
|
@ -115,7 +110,6 @@ if ($DbMode -eq "bundled") {
|
||||||
if ($LASTEXITCODE -ne 0) { throw "initdb_failed" }
|
if ($LASTEXITCODE -ne 0) { throw "initdb_failed" }
|
||||||
}
|
}
|
||||||
|
|
||||||
# Ensure listen / port in postgresql.conf
|
|
||||||
$conf = Join-Path $pgData "postgresql.conf"
|
$conf = Join-Path $pgData "postgresql.conf"
|
||||||
$hba = Join-Path $pgData "pg_hba.conf"
|
$hba = Join-Path $pgData "pg_hba.conf"
|
||||||
if (Test-Path $conf) {
|
if (Test-Path $conf) {
|
||||||
|
|
@ -126,6 +120,8 @@ if ($DbMode -eq "bundled") {
|
||||||
$confText = $confText -replace '(?m)^\s*port\s*=\s*\d+', "port = $BundledPort"
|
$confText = $confText -replace '(?m)^\s*port\s*=\s*\d+', "port = $BundledPort"
|
||||||
if ($confText -notmatch "(?m)^\s*listen_addresses\s*=") {
|
if ($confText -notmatch "(?m)^\s*listen_addresses\s*=") {
|
||||||
$confText += "`nlisten_addresses = '127.0.0.1'`n"
|
$confText += "`nlisten_addresses = '127.0.0.1'`n"
|
||||||
|
} else {
|
||||||
|
$confText = $confText -replace "(?m)^\s*listen_addresses\s*=\s*'[^']*'", "listen_addresses = '127.0.0.1'"
|
||||||
}
|
}
|
||||||
Set-Content -Path $conf -Value $confText -Encoding utf8
|
Set-Content -Path $conf -Value $confText -Encoding utf8
|
||||||
}
|
}
|
||||||
|
|
@ -137,19 +133,26 @@ if ($DbMode -eq "bundled") {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "==> Starting bundled PostgreSQL for bootstrap"
|
$status = & $pgCtl -D $pgData status 2>&1 | Out-String
|
||||||
& $pgCtl -D $pgData -l (Join-Path $data "pgdata\pg.log") start
|
if ($status -notmatch "server is running") {
|
||||||
Start-Sleep -Seconds 2
|
if (-not (Test-NetxTcpPortFree -HostName "127.0.0.1" -Port $BundledPort)) {
|
||||||
|
throw "port_in_use: 127.0.0.1:$BundledPort already occupied (bundled Postgres)"
|
||||||
|
}
|
||||||
|
Write-Host "==> Starting bundled PostgreSQL for bootstrap"
|
||||||
|
& $pgCtl -D $pgData -l (Join-Path $pgData "pg.log") start
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "pg_start_failed" }
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
}
|
||||||
|
|
||||||
$env:PGPASSWORD = $BundledPassword
|
$env:PGPASSWORD = $BundledPassword
|
||||||
try {
|
try {
|
||||||
$role = & $psql -h 127.0.0.1 -p $BundledPort -U postgres -d postgres -tAc "SELECT 1 FROM pg_roles WHERE rolname='netx'"
|
$role = & $psql -h 127.0.0.1 -p $BundledPort -U postgres -d postgres -tAc "SELECT 1 FROM pg_roles WHERE rolname='netx'"
|
||||||
if ($role -notmatch "1") {
|
if ($role -notmatch "1") {
|
||||||
& $psql -h 127.0.0.1 -p $BundledPort -U postgres -d postgres -v ON_ERROR_STOP=1 `
|
& $psql -h 127.0.0.1 -p $BundledPort -U postgres -d postgres -v ON_ERROR_STOP=1 `
|
||||||
-c "CREATE ROLE netx LOGIN PASSWORD '$BundledPassword';"
|
-c "CREATE ROLE netx LOGIN PASSWORD '$sqlPw';"
|
||||||
} else {
|
} else {
|
||||||
& $psql -h 127.0.0.1 -p $BundledPort -U postgres -d postgres -v ON_ERROR_STOP=1 `
|
& $psql -h 127.0.0.1 -p $BundledPort -U postgres -d postgres -v ON_ERROR_STOP=1 `
|
||||||
-c "ALTER ROLE netx WITH LOGIN PASSWORD '$BundledPassword';"
|
-c "ALTER ROLE netx WITH LOGIN PASSWORD '$sqlPw';"
|
||||||
}
|
}
|
||||||
$db = & $psql -h 127.0.0.1 -p $BundledPort -U postgres -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname='netx'"
|
$db = & $psql -h 127.0.0.1 -p $BundledPort -U postgres -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname='netx'"
|
||||||
if ($db -notmatch "1") {
|
if ($db -notmatch "1") {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ param(
|
||||||
[string]$ProgramRoot = "",
|
[string]$ProgramRoot = "",
|
||||||
[string]$DataRoot = "",
|
[string]$DataRoot = "",
|
||||||
[switch]$SkipBrowser = $false,
|
[switch]$SkipBrowser = $false,
|
||||||
[switch]$InlineSchedulers = $false
|
[switch]$InlineSchedulers = $false,
|
||||||
|
[switch]$Force = $false
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
@ -21,25 +22,39 @@ if (-not (Test-Path (Join-Path $prog "netx_api"))) {
|
||||||
throw "netx_api not found under program root: $prog"
|
throw "netx_api not found under program root: $prog"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ensure-NetxDataDirectories -DataRoot $data
|
||||||
|
|
||||||
$map = Import-NetxEnvFile -Path $envPath
|
$map = Import-NetxEnvFile -Path $envPath
|
||||||
|
# Force data-root paths even if an older .env missed them.
|
||||||
|
$dataEnv = Get-NetxDataEnvMap -DataRoot $data
|
||||||
|
foreach ($k in $dataEnv.Keys) {
|
||||||
|
Set-Item -Path "Env:$k" -Value $dataEnv[$k]
|
||||||
|
}
|
||||||
|
|
||||||
Set-Location $prog
|
Set-Location $prog
|
||||||
$env:PYTHONPATH = $prog
|
$env:PYTHONPATH = $prog
|
||||||
|
|
||||||
# Keep runtime artifacts in the data root (not under Program Files).
|
|
||||||
$env:NETX_AUTH_MCP_TOKEN_FILE = Join-Path $data "data\auth\mcp_token"
|
|
||||||
$env:NETX_SCHEDULER_HEARTBEAT_PATH = Join-Path $data "data\runtime\scheduler_heartbeat.json"
|
|
||||||
$env:NETX_AUTH_SECRET_FILE = Join-Path $data "data\auth\jwt_secret"
|
|
||||||
|
|
||||||
$dist = Join-Path $prog "web\dist"
|
$dist = Join-Path $prog "web\dist"
|
||||||
if (Test-Path (Join-Path $dist "index.html")) {
|
if (Test-Path (Join-Path $dist "index.html")) {
|
||||||
$env:NETX_UI_DIST_DIR = $dist
|
$env:NETX_UI_DIST_DIR = $dist
|
||||||
}
|
}
|
||||||
|
|
||||||
$mode = Get-DbMode -EnvMap $map
|
$mode = Get-DbMode -EnvMap $map
|
||||||
|
$hostBind = if ($env:NETX_HOST) { $env:NETX_HOST } else { "127.0.0.1" }
|
||||||
|
$port = if ($env:NETX_PORT) { [int]$env:NETX_PORT } else { 8890 }
|
||||||
|
|
||||||
Write-Host "==> Program: $prog"
|
Write-Host "==> Program: $prog"
|
||||||
Write-Host "==> Data: $data"
|
Write-Host "==> Data: $data"
|
||||||
Write-Host "==> DB mode: $mode"
|
Write-Host "==> DB mode: $mode"
|
||||||
|
|
||||||
|
if (-not $Force -and (Test-NetxApiHealthy -HostName $hostBind -Port $port)) {
|
||||||
|
Write-Host "==> NetX already healthy at http://${hostBind}:${port}/ — skip start (use -Force to restart)" -ForegroundColor Green
|
||||||
|
if (-not $SkipBrowser) {
|
||||||
|
try { Start-Process "http://${hostBind}:${port}/" } catch {}
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
function Get-PgsqlBin {
|
function Get-PgsqlBin {
|
||||||
foreach ($b in @(
|
foreach ($b in @(
|
||||||
(Join-Path $prog "postgres\pgsql\bin"),
|
(Join-Path $prog "postgres\pgsql\bin"),
|
||||||
|
|
@ -54,13 +69,20 @@ if ($mode -eq "bundled") {
|
||||||
$pgBin = Get-PgsqlBin
|
$pgBin = Get-PgsqlBin
|
||||||
if (-not $pgBin) { throw "bundled_postgres_missing" }
|
if (-not $pgBin) { throw "bundled_postgres_missing" }
|
||||||
$pgData = if ($map["NETX_BUNDLED_PG_DATA_DIR"]) {
|
$pgData = if ($map["NETX_BUNDLED_PG_DATA_DIR"]) {
|
||||||
$map["NETX_BUNDLED_PG_DATA_DIR"]
|
$map["NETX_BUNDLED_PG_DATA_DIR"] -replace '/', '\'
|
||||||
} else {
|
} else {
|
||||||
Join-Path $data "pgdata"
|
Join-Path $data "pgdata"
|
||||||
}
|
}
|
||||||
|
$pgPort = 15432
|
||||||
|
if ($map["NETX_BUNDLED_PG_PORT"]) {
|
||||||
|
try { $pgPort = [int]$map["NETX_BUNDLED_PG_PORT"] } catch {}
|
||||||
|
}
|
||||||
$pgCtl = Join-Path $pgBin "pg_ctl.exe"
|
$pgCtl = Join-Path $pgBin "pg_ctl.exe"
|
||||||
$status = & $pgCtl -D $pgData status 2>&1 | Out-String
|
$status = & $pgCtl -D $pgData status 2>&1 | Out-String
|
||||||
if ($status -notmatch "server is running") {
|
if ($status -notmatch "server is running") {
|
||||||
|
if (-not (Test-NetxTcpPortFree -HostName "127.0.0.1" -Port $pgPort)) {
|
||||||
|
throw "port_in_use: 127.0.0.1:$pgPort (bundled Postgres)"
|
||||||
|
}
|
||||||
Write-Host "==> Starting bundled PostgreSQL"
|
Write-Host "==> Starting bundled PostgreSQL"
|
||||||
if (-not (Test-Path $pgData)) {
|
if (-not (Test-Path $pgData)) {
|
||||||
New-Item -ItemType Directory -Path $pgData -Force | Out-Null
|
New-Item -ItemType Directory -Path $pgData -Force | Out-Null
|
||||||
|
|
@ -74,7 +96,6 @@ if ($mode -eq "bundled") {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Ensure venv exists for start_netx.ps1
|
|
||||||
$venvPy = Join-Path $prog ".venv\Scripts\python.exe"
|
$venvPy = Join-Path $prog ".venv\Scripts\python.exe"
|
||||||
if (-not (Test-Path $venvPy)) {
|
if (-not (Test-Path $venvPy)) {
|
||||||
Write-Host "==> Creating .venv (one-time)"
|
Write-Host "==> Creating .venv (one-time)"
|
||||||
|
|
@ -86,8 +107,13 @@ if (-not (Test-Path $venvPy)) {
|
||||||
if ($LASTEXITCODE -ne 0) { throw "pip_install_failed" }
|
if ($LASTEXITCODE -ne 0) { throw "pip_install_failed" }
|
||||||
}
|
}
|
||||||
|
|
||||||
$hostBind = if ($env:NETX_HOST) { $env:NETX_HOST } else { "127.0.0.1" }
|
if (-not (Test-NetxTcpPortFree -HostName $hostBind -Port $port)) {
|
||||||
$port = if ($env:NETX_PORT) { [int]$env:NETX_PORT } else { 8890 }
|
if (Test-NetxApiHealthy -HostName $hostBind -Port $port) {
|
||||||
|
Write-Host "==> Port $port already serves NetX — skip start" -ForegroundColor Green
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
throw "port_in_use: ${hostBind}:${port} occupied by non-NetX process"
|
||||||
|
}
|
||||||
|
|
||||||
$startScript = Join-Path $prog "scripts\start_netx.ps1"
|
$startScript = Join-Path $prog "scripts\start_netx.ps1"
|
||||||
if (-not (Test-Path $startScript)) {
|
if (-not (Test-Path $startScript)) {
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,35 @@ try {
|
||||||
if (Test-Path $envFile) {
|
if (Test-Path $envFile) {
|
||||||
Copy-Item $envFile (Join-Path $bak ".env")
|
Copy-Item $envFile (Join-Path $bak ".env")
|
||||||
}
|
}
|
||||||
Write-Host "==> Backup marker: $bak (data/pgdata left in place; optional pg_dump not run)"
|
# Best-effort logical backup when psql is available (bundled or PATH).
|
||||||
|
$map = Read-DotEnv -Path $envFile
|
||||||
|
$pgDump = $null
|
||||||
|
foreach ($c in @(
|
||||||
|
(Join-Path $prog "postgres\pgsql\bin\pg_dump.exe"),
|
||||||
|
(Join-Path $PSScriptRoot "postgres\pgsql\bin\pg_dump.exe")
|
||||||
|
)) {
|
||||||
|
if (Test-Path $c) { $pgDump = $c; break }
|
||||||
|
}
|
||||||
|
if (-not $pgDump) {
|
||||||
|
$cmd = Get-Command pg_dump -ErrorAction SilentlyContinue
|
||||||
|
if ($cmd) { $pgDump = $cmd.Source }
|
||||||
|
}
|
||||||
|
if ($pgDump -and $map["NETX_DATABASE_URL"] -match 'postgresql\+?[^:]*://([^:]+):([^@]+)@([^:/]+):?(\d+)?/([^?\s]+)') {
|
||||||
|
$u = $Matches[1]; $p = [uri]::UnescapeDataString($Matches[2]); $h = $Matches[3]
|
||||||
|
$pt = if ($Matches[4]) { $Matches[4] } else { "5432" }
|
||||||
|
$dbn = $Matches[5]
|
||||||
|
$dumpOut = Join-Path $bak "netx.dump"
|
||||||
|
Write-Host "==> pg_dump -> $dumpOut"
|
||||||
|
$env:PGPASSWORD = $p
|
||||||
|
try {
|
||||||
|
& $pgDump -h $h -p $pt -U $u -d $dbn -Fc -f $dumpOut
|
||||||
|
} catch {
|
||||||
|
Write-Host "[WARN] pg_dump failed: $($_.Exception.Message)" -ForegroundColor Yellow
|
||||||
|
} finally {
|
||||||
|
Remove-Item Env:PGPASSWORD -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Host "==> Backup: $bak"
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "==> Stopping services"
|
Write-Host "==> Stopping services"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "netx-ops"
|
name = "netx-ops"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
description = "netx operations tool: alarm-centric workflows with REST API and stdio MCP"
|
description = "netx operations tool: alarm-centric workflows with REST API and stdio MCP"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue