From 86dfd41f44d80b02f729d9de3b502e703bb7ca7c Mon Sep 17 00:00:00 2001 From: oliver Date: Wed, 7 Oct 2026 11:22:01 +0800 Subject: [PATCH] Fix Windows install start: writable runtime logs and robust Postgres bootstrap. API logs go under ProgramData; first-run verifies CREATE ROLE/DATABASE and login. Co-authored-by: Cursor --- packaging/_common.ps1 | 1 + packaging/installer/netx.iss | 26 ++++++++++----------- packaging/manifest.example.json | 8 +++---- packaging/setup_first_run.ps1 | 40 +++++++++++++++++++++++---------- pyproject.toml | 3 ++- scripts/start_netx.ps1 | 11 +++++++-- scripts/stop_netx.ps1 | 8 ++++++- 7 files changed, 64 insertions(+), 33 deletions(-) diff --git a/packaging/_common.ps1 b/packaging/_common.ps1 index 9f5636f..b69c87b 100644 --- a/packaging/_common.ps1 +++ b/packaging/_common.ps1 @@ -148,6 +148,7 @@ function Get-NetxDataEnvMap { "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_RUN_DIR" = (ConvertTo-NetxFsPath (Join-Path $d "data\runtime")) "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")) diff --git a/packaging/installer/netx.iss b/packaging/installer/netx.iss index 9ed8b9f..5eb1683 100644 --- a/packaging/installer/netx.iss +++ b/packaging/installer/netx.iss @@ -1,10 +1,10 @@ -; NetX Windows installer (Inno Setup 6+) +; NetX Windows installer (Inno Setup 6+) ; Compile after: packaging\build_release.ps1 ; ISCC.exe packaging\installer\netx.iss #define MyAppName "NetX" #ifndef MyAppVersion - #define MyAppVersion "0.4.4" + #define MyAppVersion "0.4.5" #endif #define MyAppPublisher "NetX" #define MyAppURL "https://github.com/hansjone/netx" @@ -34,7 +34,7 @@ WizardStyle=modern ArchitecturesAllowed=x64compatible ArchitecturesInstallIn64BitMode=x64compatible PrivilegesRequired=admin -; Data lives under {commonappdata}\NetX — not overwritten by upgrades +; Data lives under {commonappdata}\NetX 鈥?not overwritten by upgrades CloseApplications=yes ShowLanguageDialog=yes @@ -50,20 +50,19 @@ english.StartTrayNow=Start NetX tray now english.EnableAutostart=Start NetX tray at Windows logon english.EnableAutoUpdate=Enable daily silent auto-update (needs network later) english.InstallService=Install as Windows Service (uses bundled WinSW, offline) -chinesesimplified.CreateDesktopIcon=创建桌面快捷方式 -chinesesimplified.SetupOptions=安装选项: -chinesesimplified.RunFirstSetup=安装后运行首次配置(内置数据库,可离线) -chinesesimplified.StartTrayNow=立即启动 NetX 托盘 -chinesesimplified.EnableAutostart=开机时自动启动 NetX 托盘 -chinesesimplified.EnableAutoUpdate=启用每日静默自动更新(以后需要联网) -chinesesimplified.InstallService=安装为 Windows 服务(使用已捆绑的 WinSW,可离线) - +chinesesimplified.CreateDesktopIcon=鍒涘缓妗岄潰蹇嵎鏂瑰紡 +chinesesimplified.SetupOptions=瀹夎閫夐」: +chinesesimplified.RunFirstSetup=瀹夎鍚庤繍琛岄娆¢厤缃紙鍐呯疆鏁版嵁搴擄紝鍙绾匡級 +chinesesimplified.StartTrayNow=绔嬪嵆鍚姩 NetX 鎵樼洏 +chinesesimplified.EnableAutostart=寮€鏈烘椂鑷姩鍚姩 NetX 鎵樼洏 +chinesesimplified.EnableAutoUpdate=鍚敤姣忔棩闈欓粯鑷姩鏇存柊锛堜互鍚庨渶瑕佽仈缃戯級 +chinesesimplified.InstallService=瀹夎涓?Windows 鏈嶅姟锛堜娇鐢ㄥ凡鎹嗙粦鐨?WinSW锛屽彲绂荤嚎锛? [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce Name: "firstrun"; Description: "{cm:RunFirstSetup}"; GroupDescription: "{cm:SetupOptions}"; Flags: checkedonce [Files] -; Entire release stage → {app}. Exclude .portable so installed builds use ProgramData. +; Entire release stage 鈫?{app}. Exclude .portable so installed builds use ProgramData. Source: "{#StageDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Excludes: ".portable" [Dirs] @@ -99,5 +98,6 @@ Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\p Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\install_service.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"" -Start"; WorkingDir: "{app}"; Description: "{cm:InstallService}"; Flags: postinstall skipifsilent unchecked [UninstallDelete] -; Do NOT delete {commonappdata}\NetX — preserves DB and secrets across reinstall +; Do NOT delete {commonappdata}\NetX 鈥?preserves DB and secrets across reinstall Type: filesandordirs; Name: "{app}" + diff --git a/packaging/manifest.example.json b/packaging/manifest.example.json index 952a592..1d23d3e 100644 --- a/packaging/manifest.example.json +++ b/packaging/manifest.example.json @@ -1,11 +1,11 @@ { "channel": "stable", - "latest": "0.4.4", + "latest": "0.4.5", "min_compatible": "0.3.0", - "notes_url": "https://github.com/hansjone/netx/releases/tag/v0.4.4", + "notes_url": "https://github.com/hansjone/netx/releases/tag/v0.4.5", "windows": { - "url": "https://github.com/hansjone/netx/releases/download/v0.4.4/NetX-0.4.4-win64.zip", - "setup_url": "https://github.com/hansjone/netx/releases/download/v0.4.4/NetX-Setup-0.4.4.exe", + "url": "https://github.com/hansjone/netx/releases/download/v0.4.5/NetX-0.4.5-win64.zip", + "setup_url": "https://github.com/hansjone/netx/releases/download/v0.4.5/NetX-Setup-0.4.5.exe", "sha256": "", "size": 0 } diff --git a/packaging/setup_first_run.ps1 b/packaging/setup_first_run.ps1 index 1a268a7..6a73e65 100644 --- a/packaging/setup_first_run.ps1 +++ b/packaging/setup_first_run.ps1 @@ -153,21 +153,37 @@ if ($DbMode -eq "bundled") { $env:PGPASSWORD = $BundledPassword try { - $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") { - & $psql -h 127.0.0.1 -p $BundledPort -U postgres -d postgres -v ON_ERROR_STOP=1 ` - -c "CREATE ROLE netx LOGIN PASSWORD '$sqlPw';" + function Invoke-NetxPsql { + param([string]$Sql, [string]$Database = "postgres") + $out = & $psql -h 127.0.0.1 -p $BundledPort -U postgres -d $Database -v ON_ERROR_STOP=1 -tAc $Sql 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "psql_failed ($LASTEXITCODE): $Sql`n$out" + } + return (($out | Out-String).Trim()) + } + + $role = Invoke-NetxPsql -Sql "SELECT 1 FROM pg_roles WHERE rolname='netx'" + if ($role -eq "1") { + Invoke-NetxPsql -Sql "ALTER ROLE netx WITH LOGIN PASSWORD '$sqlPw'" | Out-Null } else { - & $psql -h 127.0.0.1 -p $BundledPort -U postgres -d postgres -v ON_ERROR_STOP=1 ` - -c "ALTER ROLE netx WITH LOGIN PASSWORD '$sqlPw';" + Invoke-NetxPsql -Sql "CREATE ROLE netx LOGIN PASSWORD '$sqlPw'" | Out-Null } - $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") { - & $psql -h 127.0.0.1 -p $BundledPort -U postgres -d postgres -v ON_ERROR_STOP=1 ` - -c "CREATE DATABASE netx OWNER netx;" + $db = Invoke-NetxPsql -Sql "SELECT 1 FROM pg_database WHERE datname='netx'" + if ($db -ne "1") { + Invoke-NetxPsql -Sql "CREATE DATABASE netx OWNER netx" | Out-Null + } + Invoke-NetxPsql -Sql "GRANT ALL PRIVILEGES ON DATABASE netx TO netx" | Out-Null + # Verify login as app role (catches auth/hba mismatches early). + $prev = $env:PGPASSWORD + $env:PGPASSWORD = $BundledPassword + try { + $ping = & $psql -h 127.0.0.1 -p $BundledPort -U netx -d netx -v ON_ERROR_STOP=1 -tAc "SELECT 1" 2>&1 + if ($LASTEXITCODE -ne 0 -or (($ping | Out-String).Trim()) -ne "1") { + throw "netx_role_login_failed: $ping" + } + } finally { + $env:PGPASSWORD = $prev } - & $psql -h 127.0.0.1 -p $BundledPort -U postgres -d postgres -v ON_ERROR_STOP=1 ` - -c "GRANT ALL PRIVILEGES ON DATABASE netx TO netx;" } finally { Remove-Item Env:PGPASSWORD -ErrorAction SilentlyContinue } diff --git a/pyproject.toml b/pyproject.toml index 3c04c34..8c54492 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "netx-ops" -version = "0.4.4" +version = "0.4.5" description = "netx operations tool: alarm-centric workflows with REST API and stdio MCP" readme = "README.md" requires-python = ">=3.11" @@ -43,3 +43,4 @@ netx-topology-mcp = "netx_topology_mcp.server:main" [tool.setuptools.packages.find] where = ["."] include = ["netx_api*"] + diff --git a/scripts/start_netx.ps1 b/scripts/start_netx.ps1 index e89fcaf..7053271 100644 --- a/scripts/start_netx.ps1 +++ b/scripts/start_netx.ps1 @@ -23,9 +23,16 @@ if ($Backgtound -and -not $Background) { $projectRoot = Split-Path -Parent $PSScriptRoot Set-Location $projectRoot -$runDir = Join-Path $PSScriptRoot ".run" +# 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 | Out-Null + New-Item -ItemType Directory -Path $runDir -Force | Out-Null } $pidFile = Join-Path $runDir "netx.pid" diff --git a/scripts/stop_netx.ps1 b/scripts/stop_netx.ps1 index 2e930f0..1106bb3 100644 --- a/scripts/stop_netx.ps1 +++ b/scripts/stop_netx.ps1 @@ -9,7 +9,13 @@ $ErrorActionPreference = "Continue" $useForce = if ($NoForce) { $false } else { [bool]$Force } -$runDir = Join-Path $PSScriptRoot ".run" +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" +} $pidFile = Join-Path $runDir "netx.pid" $workerPidFile = Join-Path $runDir "worker.pid" $webPidFile = Join-Path $runDir "web.pid"