From 41ab4921a07c3d281f9c910749c434d0c7285f6f Mon Sep 17 00:00:00 2001 From: oliver Date: Wed, 7 Oct 2026 10:53:29 +0800 Subject: [PATCH 1/3] Fix Windows PowerShell parsing on Server 2012: UTF-8 BOM scripts. Chinese packaging scripts without BOM were misread as ANSI and failed at first-run. Document that bundled Python 3.13+ needs Server 2016+ / Win10+. Co-authored-by: Cursor --- packaging/README.md | 2 ++ packaging/_common.ps1 | 2 +- packaging/build_release.ps1 | 22 ++++++++++++++++++++-- packaging/check_update.ps1 | 2 +- packaging/download_postgres.ps1 | 2 +- packaging/install_autostart.ps1 | 2 +- packaging/install_service.ps1 | 2 +- packaging/install_update_task.ps1 | 2 +- packaging/installer/netx.iss | 2 +- packaging/launch_shortcut.ps1 | 2 +- packaging/manifest.example.json | 10 +++++----- packaging/netx_tray.ps1 | 2 +- packaging/publish_forgejo_release.ps1 | 2 +- packaging/publish_release.ps1 | 2 +- packaging/service_run.ps1 | 2 +- packaging/setup_first_run.ps1 | 2 +- packaging/sign_release.ps1 | 2 +- packaging/start_netx_app.ps1 | 2 +- packaging/stop_netx_app.ps1 | 2 +- packaging/update_netx.ps1 | 2 +- pyproject.toml | 2 +- scripts/init_pg.ps1 | 2 +- scripts/start_netx.ps1 | 2 +- scripts/stop_netx.ps1 | 2 +- 24 files changed, 48 insertions(+), 28 deletions(-) diff --git a/packaging/README.md b/packaging/README.md index 3da2d65..9703c61 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -21,6 +21,8 @@ This directory builds a Windows deliverable with: **Offline:** Setup ships portable PostgreSQL, `.venv` (when built with `-CreateVenv`), and WinSW. First-run uses `-NonInteractive -DbMode bundled` — **no GitHub/EDB download on the target PC**. Service install uses bundled WinSW (pass `-AllowDownload` only on a build/dev machine if the binary is missing). Auto-update still needs network later, and is unchecked by default. +**OS:** Windows 10/11 or Windows Server **2016+** recommended. Packaging scripts are UTF-8 **with BOM** so Chinese UI works on Windows PowerShell 5.x. Bundled Python in current releases is **3.13+**, which does **not** support Windows Server 2012 R2 — use Server 2016+ or a newer desktop OS. + ## Build (developer machine) Prerequisites: Python 3.11+, Node 20+, PowerShell, network (to download PG binaries once). diff --git a/packaging/_common.ps1 b/packaging/_common.ps1 index 4bba789..9f5636f 100644 --- a/packaging/_common.ps1 +++ b/packaging/_common.ps1 @@ -1,4 +1,4 @@ -# Shared path helpers for Windows packaging scripts. +# Shared path helpers for Windows packaging scripts. # Dot-source: . "$PSScriptRoot\_common.ps1" $ErrorActionPreference = "Stop" diff --git a/packaging/build_release.ps1 b/packaging/build_release.ps1 index b8fc5c4..7580f15 100644 --- a/packaging/build_release.ps1 +++ b/packaging/build_release.ps1 @@ -1,4 +1,4 @@ -param( +param( [string]$Version = "", [string]$OutDir = "", [switch]$SkipWebBuild = $false, @@ -80,6 +80,22 @@ $webOut = Join-Path $stage "web\dist" New-Item -ItemType Directory -Path (Split-Path $webOut -Parent) -Force | Out-Null Copy-Item -Path $dist -Destination $webOut -Recurse -Force +function Write-Utf8BomFile { + param([string]$Path) + # Windows PowerShell 4/5 (Server 2012+) mis-parses UTF-8 without BOM when scripts contain CJK. + $utf8Bom = New-Object System.Text.UTF8Encoding $true + $bytes = [IO.File]::ReadAllBytes($Path) + $hasBom = ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) + $text = if ($hasBom) { + [Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3) + } else { + [Text.Encoding]::UTF8.GetString($bytes) + } + $text = $text -replace "`r`n", "`n" -replace "`n", "`r`n" + if (-not $text.EndsWith("`r`n")) { $text += "`r`n" } + [IO.File]::WriteAllText($Path, $text, $utf8Bom) +} + $packOut = Join-Path $stage "packaging" New-Item -ItemType Directory -Path $packOut -Force | Out-Null Copy-Item -Path (Join-Path $PSScriptRoot "_common.ps1") -Destination $packOut -Force @@ -87,13 +103,15 @@ foreach ($name in @( "download_postgres.ps1", "setup_first_run.ps1", "start_netx_app.ps1", "stop_netx_app.ps1", "update_netx.ps1", "check_update.ps1", "launch_shortcut.ps1", "netx_tray.ps1", "install_autostart.ps1", "install_service.ps1", - "service_run.ps1", "install_update_task.ps1", "sign_release.ps1", "build_release.ps1", "publish_release.ps1", "publish_forgejo_release.ps1", "README.md", "manifest.example.json" )) { $src = Join-Path $PSScriptRoot $name if (Test-Path $src) { Copy-Item $src (Join-Path $packOut $name) -Force } } +Get-ChildItem -Path $packOut -Filter *.ps1 -File | ForEach-Object { Write-Utf8BomFile -Path $_.FullName } +Get-ChildItem -Path (Join-Path $stage "scripts") -Filter *.ps1 -File -ErrorAction SilentlyContinue | + ForEach-Object { Write-Utf8BomFile -Path $_.FullName } Copy-Item -Path (Join-Path $PSScriptRoot "config") -Destination (Join-Path $packOut "config") -Recurse -Force Copy-Item -Path (Join-Path $PSScriptRoot "installer") -Destination (Join-Path $packOut "installer") -Recurse -Force $assetsSrc = Join-Path $PSScriptRoot "assets" diff --git a/packaging/check_update.ps1 b/packaging/check_update.ps1 index 75f552f..7f90413 100644 --- a/packaging/check_update.ps1 +++ b/packaging/check_update.ps1 @@ -1,4 +1,4 @@ -param( +param( [string]$ProgramRoot = "", [string]$DataRoot = "", [string]$UpdateUrl = "", diff --git a/packaging/download_postgres.ps1 b/packaging/download_postgres.ps1 index 38c7b41..6398fd1 100644 --- a/packaging/download_postgres.ps1 +++ b/packaging/download_postgres.ps1 @@ -1,4 +1,4 @@ -param( +param( [string]$Version = "16.4-1", [string]$OutDir = "" ) diff --git a/packaging/install_autostart.ps1 b/packaging/install_autostart.ps1 index d5c2ebd..50510df 100644 --- a/packaging/install_autostart.ps1 +++ b/packaging/install_autostart.ps1 @@ -1,4 +1,4 @@ -param( +param( [string]$ProgramRoot = "", [switch]$Remove = $false ) diff --git a/packaging/install_service.ps1 b/packaging/install_service.ps1 index ff97f17..f9e87f6 100644 --- a/packaging/install_service.ps1 +++ b/packaging/install_service.ps1 @@ -1,4 +1,4 @@ -param( +param( [string]$ProgramRoot = "", [string]$DataRoot = "", [ValidateSet("winsw", "task")] diff --git a/packaging/install_update_task.ps1 b/packaging/install_update_task.ps1 index 9abd2c3..07b3ab9 100644 --- a/packaging/install_update_task.ps1 +++ b/packaging/install_update_task.ps1 @@ -1,4 +1,4 @@ -param( +param( [string]$ProgramRoot = "", [string]$DataRoot = "", [ValidateSet("Daily", "AtLogOn", "Hourly")] diff --git a/packaging/installer/netx.iss b/packaging/installer/netx.iss index 0c68bdb..2ee015d 100644 --- a/packaging/installer/netx.iss +++ b/packaging/installer/netx.iss @@ -4,7 +4,7 @@ #define MyAppName "NetX" #ifndef MyAppVersion - #define MyAppVersion "0.4.2" + #define MyAppVersion "0.4.3" #endif #define MyAppPublisher "NetX" #define MyAppURL "https://github.com/hansjone/netx" diff --git a/packaging/launch_shortcut.ps1 b/packaging/launch_shortcut.ps1 index f9f9da7..fd9e12d 100644 --- a/packaging/launch_shortcut.ps1 +++ b/packaging/launch_shortcut.ps1 @@ -1,4 +1,4 @@ -param( +param( [ValidateSet("tray", "start", "stop", "setup", "update")] [string]$Action = "tray", [string]$ProgramRoot = "", diff --git a/packaging/manifest.example.json b/packaging/manifest.example.json index 3e49db5..06459d3 100644 --- a/packaging/manifest.example.json +++ b/packaging/manifest.example.json @@ -1,11 +1,11 @@ -{ +{ "channel": "stable", - "latest": "0.4.2", + "latest": "0.4.3", "min_compatible": "0.3.0", - "notes_url": "https://github.com/hansjone/netx/releases/tag/v0.4.2", + "notes_url": "https://github.com/hansjone/netx/releases/tag/v0.4.3", "windows": { - "url": "https://github.com/hansjone/netx/releases/download/v0.4.2/NetX-0.4.2-win64.zip", - "setup_url": "https://github.com/hansjone/netx/releases/download/v0.4.2/NetX-Setup-0.4.2.exe", + "url": "https://github.com/hansjone/netx/releases/download/v0.4.3/NetX-0.4.3-win64.zip", + "setup_url": "https://github.com/hansjone/netx/releases/download/v0.4.3/NetX-Setup-0.4.3.exe", "sha256": "", "size": 0 } diff --git a/packaging/netx_tray.ps1 b/packaging/netx_tray.ps1 index 7c19b4f..e822fa7 100644 --- a/packaging/netx_tray.ps1 +++ b/packaging/netx_tray.ps1 @@ -1,4 +1,4 @@ -param( +param( [string]$ProgramRoot = "", [string]$DataRoot = "", [switch]$StartOnLaunch = $true, diff --git a/packaging/publish_forgejo_release.ps1 b/packaging/publish_forgejo_release.ps1 index 40fd352..da5629d 100644 --- a/packaging/publish_forgejo_release.ps1 +++ b/packaging/publish_forgejo_release.ps1 @@ -1,4 +1,4 @@ -param( +param( [string]$Version = "", [string]$ForgejoBase = "http://10.0.0.131:3000", [string]$Owner = "hansjone", diff --git a/packaging/publish_release.ps1 b/packaging/publish_release.ps1 index 19fa044..6d9e648 100644 --- a/packaging/publish_release.ps1 +++ b/packaging/publish_release.ps1 @@ -1,4 +1,4 @@ -param( +param( [string]$Version = "", [switch]$SkipBuild = $false, [switch]$SkipInstaller = $false, diff --git a/packaging/service_run.ps1 b/packaging/service_run.ps1 index 40cc61f..e514352 100644 --- a/packaging/service_run.ps1 +++ b/packaging/service_run.ps1 @@ -1,4 +1,4 @@ -param( +param( [string]$ProgramRoot = "", [string]$DataRoot = "", [int]$HealthFailLimit = 6, diff --git a/packaging/setup_first_run.ps1 b/packaging/setup_first_run.ps1 index 8889308..1a268a7 100644 --- a/packaging/setup_first_run.ps1 +++ b/packaging/setup_first_run.ps1 @@ -1,4 +1,4 @@ -param( +param( [ValidateSet("bundled", "external", "")] [string]$DbMode = "", [string]$ProgramRoot = "", diff --git a/packaging/sign_release.ps1 b/packaging/sign_release.ps1 index 121c8d9..a859447 100644 --- a/packaging/sign_release.ps1 +++ b/packaging/sign_release.ps1 @@ -1,4 +1,4 @@ -param( +param( [Parameter(Mandatory = $true)] [string[]]$Files, [string]$Thumbprint = "", diff --git a/packaging/start_netx_app.ps1 b/packaging/start_netx_app.ps1 index d57781c..6a80f9d 100644 --- a/packaging/start_netx_app.ps1 +++ b/packaging/start_netx_app.ps1 @@ -1,4 +1,4 @@ -param( +param( [string]$ProgramRoot = "", [string]$DataRoot = "", [switch]$SkipBrowser = $false, diff --git a/packaging/stop_netx_app.ps1 b/packaging/stop_netx_app.ps1 index 174c631..4d04f19 100644 --- a/packaging/stop_netx_app.ps1 +++ b/packaging/stop_netx_app.ps1 @@ -1,4 +1,4 @@ -param( +param( [string]$ProgramRoot = "", [string]$DataRoot = "", [switch]$KeepPostgres = $false diff --git a/packaging/update_netx.ps1 b/packaging/update_netx.ps1 index c959f66..0f12cc5 100644 --- a/packaging/update_netx.ps1 +++ b/packaging/update_netx.ps1 @@ -1,4 +1,4 @@ -param( +param( [Parameter(Mandatory = $true)] [string]$PackagePath, [string]$ProgramRoot = "", diff --git a/pyproject.toml b/pyproject.toml index 8e30807..ee59d1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "netx-ops" -version = "0.4.2" +version = "0.4.3" description = "netx operations tool: alarm-centric workflows with REST API and stdio MCP" readme = "README.md" requires-python = ">=3.11" diff --git a/scripts/init_pg.ps1 b/scripts/init_pg.ps1 index d9c02de..cfcb909 100644 --- a/scripts/init_pg.ps1 +++ b/scripts/init_pg.ps1 @@ -1,4 +1,4 @@ -param( +param( [string]$PgHost = "127.0.0.1", [int]$PgPort = 5432, [string]$SuperUser = "postgres", diff --git a/scripts/start_netx.ps1 b/scripts/start_netx.ps1 index ed2caec..e89fcaf 100644 --- a/scripts/start_netx.ps1 +++ b/scripts/start_netx.ps1 @@ -1,4 +1,4 @@ -param( +param( [string]$BindHost = "127.0.0.1", [int]$Port = 8890, [int]$WebPort = 5173, diff --git a/scripts/stop_netx.ps1 b/scripts/stop_netx.ps1 index 2d91466..2e930f0 100644 --- a/scripts/stop_netx.ps1 +++ b/scripts/stop_netx.ps1 @@ -1,4 +1,4 @@ -param( +param( [int]$Port = 8890, [int]$WebPort = 5173, # Windows often ignores graceful Stop-Process on python; default hard-kill. From ca97a2a597fa9bd7ab7ba72fe1a105b821905a8f Mon Sep 17 00:00:00 2001 From: oliver Date: Wed, 7 Oct 2026 11:03:38 +0800 Subject: [PATCH 2/3] Add Explorer .cmd launchers and always-on desktop First-time setup shortcuts. Server 2012 Start Menu often hides PowerShell .lnk entries; .cmd targets are visible. Co-authored-by: Cursor --- packaging/build_release.ps1 | 9 +++++++++ packaging/cmd/NetX-FirstRun.cmd | 7 +++++++ packaging/cmd/NetX-Start.cmd | 7 +++++++ packaging/cmd/NetX-Tray.cmd | 7 +++++++ packaging/installer/netx.iss | 16 ++++++++++------ packaging/launch_shortcut.ps1 | 5 +++-- packaging/manifest.example.json | 8 ++++---- pyproject.toml | 2 +- 8 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 packaging/cmd/NetX-FirstRun.cmd create mode 100644 packaging/cmd/NetX-Start.cmd create mode 100644 packaging/cmd/NetX-Tray.cmd diff --git a/packaging/build_release.ps1 b/packaging/build_release.ps1 index 7580f15..a1f1437 100644 --- a/packaging/build_release.ps1 +++ b/packaging/build_release.ps1 @@ -157,6 +157,15 @@ $builtAt = (Get-Date).ToUniversalTime().ToString("o") Set-Content -Path (Join-Path $stage ".portable") -Value "1" -Encoding ascii +# Root .cmd launchers (Explorer / Start Menu friendly; ASCII-only). +$cmdSrc = Join-Path $PSScriptRoot "cmd" +foreach ($cmdName in @("NetX-FirstRun.cmd", "NetX-Start.cmd", "NetX-Tray.cmd")) { + $c = Join-Path $cmdSrc $cmdName + if (Test-Path $c) { + Copy-Item -Path $c -Destination (Join-Path $stage $cmdName) -Force + } +} + if ($CreateVenv) { Write-Host "==> Creating .venv in stage (requires Python 3.11+ on PATH)" $py = (Get-Command python -ErrorAction SilentlyContinue) diff --git a/packaging/cmd/NetX-FirstRun.cmd b/packaging/cmd/NetX-FirstRun.cmd new file mode 100644 index 0000000..c07282e --- /dev/null +++ b/packaging/cmd/NetX-FirstRun.cmd @@ -0,0 +1,7 @@ +@echo off +setlocal +set "ROOT=%~dp0" +if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%" +cd /d "%ROOT%" +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%ROOT%\packaging\launch_shortcut.ps1" -Action setup -ProgramRoot "%ROOT%" -DataRoot "%ProgramData%\NetX" +exit /b %ERRORLEVEL% diff --git a/packaging/cmd/NetX-Start.cmd b/packaging/cmd/NetX-Start.cmd new file mode 100644 index 0000000..67f3675 --- /dev/null +++ b/packaging/cmd/NetX-Start.cmd @@ -0,0 +1,7 @@ +@echo off +setlocal +set "ROOT=%~dp0" +if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%" +cd /d "%ROOT%" +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%ROOT%\packaging\launch_shortcut.ps1" -Action start -ProgramRoot "%ROOT%" -DataRoot "%ProgramData%\NetX" +exit /b %ERRORLEVEL% diff --git a/packaging/cmd/NetX-Tray.cmd b/packaging/cmd/NetX-Tray.cmd new file mode 100644 index 0000000..262f21e --- /dev/null +++ b/packaging/cmd/NetX-Tray.cmd @@ -0,0 +1,7 @@ +@echo off +setlocal +set "ROOT=%~dp0" +if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%" +cd /d "%ROOT%" +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%ROOT%\packaging\launch_shortcut.ps1" -Action tray -ProgramRoot "%ROOT%" -DataRoot "%ProgramData%\NetX" -StartOnLaunch +exit /b %ERRORLEVEL% diff --git a/packaging/installer/netx.iss b/packaging/installer/netx.iss index 2ee015d..9ed8b9f 100644 --- a/packaging/installer/netx.iss +++ b/packaging/installer/netx.iss @@ -4,7 +4,7 @@ #define MyAppName "NetX" #ifndef MyAppVersion - #define MyAppVersion "0.4.3" + #define MyAppVersion "0.4.4" #endif #define MyAppPublisher "NetX" #define MyAppURL "https://github.com/hansjone/netx" @@ -59,7 +59,7 @@ chinesesimplified.EnableAutoUpdate=启用每日静默自动更新(以后需要 chinesesimplified.InstallService=安装为 Windows 服务(使用已捆绑的 WinSW,可离线) [Tasks] -Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce Name: "firstrun"; Description: "{cm:RunFirstSetup}"; GroupDescription: "{cm:SetupOptions}"; Flags: checkedonce [Files] @@ -75,16 +75,20 @@ Name: "{commonappdata}\NetX\pgdata" Name: "{commonappdata}\NetX\backups" [Icons] -Name: "{group}\NetX Tray"; Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\packaging\launch_shortcut.ps1"" -Action tray -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"" -StartOnLaunch"; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico" -Name: "{group}\Start NetX"; Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\packaging\launch_shortcut.ps1"" -Action start -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico" +; Prefer .cmd targets so Server 2012 / classic Start Menu shows normal program entries. +; Also pin First-time setup + Start to common desktop (always), not only optional tray icon. +Name: "{group}\NetX Tray"; Filename: "{app}\NetX-Tray.cmd"; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico" +Name: "{group}\Start NetX"; Filename: "{app}\NetX-Start.cmd"; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico" Name: "{group}\Stop NetX"; Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\packaging\launch_shortcut.ps1"" -Action stop -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico" Name: "{group}\Check for updates"; Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\packaging\launch_shortcut.ps1"" -Action update -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico" Name: "{group}\Open NetX UI"; Filename: "http://127.0.0.1:8890/"; IconFilename: "{app}\packaging\assets\netx.ico" -Name: "{group}\First-time setup"; Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\packaging\launch_shortcut.ps1"" -Action setup -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico" +Name: "{group}\First-time setup"; Filename: "{app}\NetX-FirstRun.cmd"; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico" Name: "{group}\Install Windows Service (admin)"; Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\install_service.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"" -Start"; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico" Name: "{group}\Enable silent auto-update (daily)"; Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\install_update_task.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico" Name: "{group}\Enable start at logon"; Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\install_autostart.ps1"" -ProgramRoot ""{app}"""; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico" -Name: "{autodesktop}\NetX"; Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\packaging\launch_shortcut.ps1"" -Action tray -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"" -StartOnLaunch"; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico"; Tasks: desktopicon +Name: "{commondesktop}\NetX First-time setup"; Filename: "{app}\NetX-FirstRun.cmd"; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico" +Name: "{commondesktop}\NetX Start"; Filename: "{app}\NetX-Start.cmd"; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico" +Name: "{commondesktop}\NetX"; Filename: "{app}\NetX-Tray.cmd"; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico"; Tasks: desktopicon [Run] ; First-run is NonInteractive + bundled DB so offline machines succeed without prompts or downloads. diff --git a/packaging/launch_shortcut.ps1 b/packaging/launch_shortcut.ps1 index fd9e12d..5a43966 100644 --- a/packaging/launch_shortcut.ps1 +++ b/packaging/launch_shortcut.ps1 @@ -50,9 +50,10 @@ function Ensure-NetxEnv { & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $setup ` -ProgramRoot $prog -DataRoot $data -NonInteractive -DbMode bundled if ($LASTEXITCODE -ne 0 -or -not (Test-Path $envPath)) { + $cmd = Join-Path $prog "NetX-FirstRun.cmd" throw (T ` - "First-time setup failed. Run Start Menu → NetX → First-time setup.`nLog: $log" ` - "首次配置失败。请运行「开始菜单 → NetX → First-time setup」。`n日志: $log") + "First-time setup failed.`nDouble-click: $cmd`nOr: Start Menu → All Programs → NetX → First-time setup`nLog: $log" ` + "首次配置失败。`n请双击: $cmd`n或: 开始菜单 → 所有程序 → NetX → First-time setup`n日志: $log") } } diff --git a/packaging/manifest.example.json b/packaging/manifest.example.json index 06459d3..952a592 100644 --- a/packaging/manifest.example.json +++ b/packaging/manifest.example.json @@ -1,11 +1,11 @@ { "channel": "stable", - "latest": "0.4.3", + "latest": "0.4.4", "min_compatible": "0.3.0", - "notes_url": "https://github.com/hansjone/netx/releases/tag/v0.4.3", + "notes_url": "https://github.com/hansjone/netx/releases/tag/v0.4.4", "windows": { - "url": "https://github.com/hansjone/netx/releases/download/v0.4.3/NetX-0.4.3-win64.zip", - "setup_url": "https://github.com/hansjone/netx/releases/download/v0.4.3/NetX-Setup-0.4.3.exe", + "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", "sha256": "", "size": 0 } diff --git a/pyproject.toml b/pyproject.toml index ee59d1a..3c04c34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "netx-ops" -version = "0.4.3" +version = "0.4.4" description = "netx operations tool: alarm-centric workflows with REST API and stdio MCP" readme = "README.md" requires-python = ">=3.11" From 86dfd41f44d80b02f729d9de3b502e703bb7ca7c Mon Sep 17 00:00:00 2001 From: oliver Date: Wed, 7 Oct 2026 11:22:01 +0800 Subject: [PATCH 3/3] 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"