diff --git a/netx_api/app_startup.py b/netx_api/app_startup.py index beb5497..ea16cd6 100644 --- a/netx_api/app_startup.py +++ b/netx_api/app_startup.py @@ -15,7 +15,6 @@ from .schema_patches import ( apply_topology_schema_safety_net, run_alembic_upgrade_to_head, ) -from .ne_crypto import ensure_credential_secret_key from .security_bootstrap import assert_secure_defaults_or_exit from .ume_runtime import start_api_sideband_threads, start_device_schedulers import netx_api.ume_support as ume_support @@ -43,8 +42,6 @@ def _configure_ume_diag_logging() -> None: def run_api_startup() -> None: """Full API boot sequence previously inlined in ``main.on_startup``.""" assert_secure_defaults_or_exit() - # Persist Fernet key for managed-NE passwords (same idea as JWT secret file). - ensure_credential_secret_key() _configure_ume_diag_logging() _log.info( "startup: ne_exec_policy_enabled=%s", diff --git a/netx_api/config.py b/netx_api/config.py index e12b5fd..1126fc5 100644 --- a/netx_api/config.py +++ b/netx_api/config.py @@ -80,9 +80,8 @@ class Settings(BaseSettings): ume_topo_nodes_path: str = "/restconf/data/zte-resources-module:TopoNodes" ume_topological_links_path: str = "/restconf/data/zte-resources-module:TopologicalLinks" ume_sync_alarms_history_every_hours: int = 24 - # Managed NE credentials (Fernet). Empty = auto-generate & persist to credential_secret_file. + # Managed NE credentials (Fernet key; generate with cryptography.fernet.Fernet.generate_key()) credential_secret_key: str = "" - credential_secret_file: str = "data/auth/credential_secret" # Shared-server worker caps (sized for multi-operator use; raise if bastion/DB allow). ne_connect_max_workers: int = 8 ne_connect_timeout_sec: int = 30 diff --git a/netx_api/ne_crypto.py b/netx_api/ne_crypto.py index 7624c2a..4ba7617 100644 --- a/netx_api/ne_crypto.py +++ b/netx_api/ne_crypto.py @@ -1,73 +1,18 @@ from __future__ import annotations -import logging -from pathlib import Path - from cryptography.fernet import Fernet, InvalidToken from .config import settings -_log = logging.getLogger("netx.crypto") -_cached_credential_key: str | None = None - class CredentialCryptoError(RuntimeError): pass -def credential_secret_file_path() -> Path: - raw = str(getattr(settings, "credential_secret_file", None) or "data/auth/credential_secret").strip() - path = Path(raw) - if not path.is_absolute(): - path = Path.cwd() / path - return path - - -def ensure_credential_secret_key() -> str: - """Resolve Fernet key: env ``NETX_CREDENTIAL_SECRET_KEY`` > file > generate once. - - Packaged installs should also write the key into ``.env`` via setup_first_run; - this startup/path fallback covers upgrades and missed first-run keys. - """ - global _cached_credential_key - configured = str(settings.credential_secret_key or "").strip() - if configured: - return configured - if _cached_credential_key: - return _cached_credential_key - - path = credential_secret_file_path() - try: - if path.is_file(): - existing = path.read_text(encoding="utf-8").strip() - if existing: - _cached_credential_key = existing - settings.credential_secret_key = existing - return existing - except Exception: - _log.exception("read credential secret file failed path=%s", path) - - generated = Fernet.generate_key().decode("ascii") - try: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(generated + "\n", encoding="utf-8") - try: - path.chmod(0o600) - except Exception: - pass - _log.info("wrote per-install credential Fernet key to %s", path) - except Exception: - _log.exception( - "write credential secret file failed path=%s; using in-memory key only", - path, - ) - _cached_credential_key = generated - settings.credential_secret_key = generated - return generated - - def _fernet() -> Fernet: - key = ensure_credential_secret_key() + key = str(settings.credential_secret_key or "").strip() + if not key: + raise CredentialCryptoError("credential_secret_key_not_configured") try: return Fernet(key.encode("ascii")) except Exception as exc: @@ -92,7 +37,4 @@ def decrypt_secret(value: str) -> str: def credentials_configured() -> bool: - try: - return bool(ensure_credential_secret_key()) - except Exception: - return False + return bool(str(settings.credential_secret_key or "").strip()) diff --git a/packaging/README.md b/packaging/README.md index 951db52..9703c61 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -19,7 +19,7 @@ This directory builds a Windows deliverable with: **Installer:** English + 简体中文 (language dialog). Icons use `packaging/assets/netx.ico`. -**Offline:** Setup ships portable PostgreSQL, **`python/runtime` + `.venv`** (portable; not tied to the build PC’s user profile), and WinSW. The installer wizard chooses **built-in or external** PostgreSQL and validates external credentials (`psql SELECT 1`) before files are installed — **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. +**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. @@ -54,14 +54,10 @@ Override version in the `.iss` or edit `#define MyAppVersion`. ### Setup.exe 1. Run `NetX-Setup-x.y.z.exe` (admin). -2. On the **Database** page: choose built-in (offline) or external PostgreSQL (host/port/user/password/db; connection must succeed to continue). -3. Files → `%ProgramFiles%\NetX\` (program root). -4. Data → `%ProgramData%\NetX\` (`.env`, `pgdata`, spool, secrets). Post-install writes `.env` automatically. -5. Start menu: **Start NetX** / **Stop NetX** / **Open NetX UI** / **Reconfigure database** (repair only). - -Silent (bundled default): `/SILENT /DbMode=bundled` -Silent external: `/SILENT /DbMode=external /DbHost=... /DbPort=5432 /DbUser=... /DbPassword=... /DbName=...` -Optional credential key (reuse encrypted NE passwords from another install): `/CredentialSecretKey=...` — omit to auto-generate. +2. Files → `%ProgramFiles%\NetX\` (program root). +3. Data → `%ProgramData%\NetX\` (`.env`, `pgdata`, spool, secrets). +4. Optional post-install task runs `setup_first_run.ps1` (choose bundled vs external DB). +5. Start menu: **Start NetX** / **Stop NetX** / **Open NetX UI**. ### Zip (portable) diff --git a/packaging/_common.ps1 b/packaging/_common.ps1 index a73f544..b69c87b 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" @@ -147,7 +147,6 @@ function Get-NetxDataEnvMap { 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_CREDENTIAL_SECRET_FILE" = (ConvertTo-NetxFsPath (Join-Path $d "data\auth\credential_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")) @@ -157,18 +156,6 @@ function Get-NetxDataEnvMap { } } -function New-NetxFernetKey { - # cryptography.fernet.Fernet.generate_key() == urlsafe_b64encode(os.urandom(32)) - $bytes = New-Object byte[] 32 - $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() - try { - $rng.GetBytes($bytes) - } finally { - $rng.Dispose() - } - return [Convert]::ToBase64String($bytes).Replace('+', '-').Replace('/', '_') -} - function Ensure-NetxDataDirectories { param([string]$DataRoot) $subs = @( @@ -214,233 +201,6 @@ function Test-NetxApiHealthy { } } -function Wait-NetxApiHealthy { - param( - [string]$HostName = "127.0.0.1", - [int]$Port = 8890, - [int]$TimeoutSec = 180, - [int]$PollMs = 500 - ) - $deadline = (Get-Date).AddSeconds($TimeoutSec) - while ((Get-Date) -lt $deadline) { - if (Test-NetxApiHealthy -HostName $HostName -Port $Port -TimeoutSec 2) { - return $true - } - Start-Sleep -Milliseconds $PollMs - } - return $false -} - -function ConvertTo-NetxProcessArgument { - param([Parameter(Mandatory = $true)][AllowEmptyString()][string]$Value) - # Start-Process joins string[] args with spaces and does NOT quote values that - # contain spaces (e.g. C:\Program Files\NetX). Always emit one argv token. - if ($Value -match '[\s"]') { - return '"' + ($Value -replace '"', '\"') + '"' - } - return $Value -} - -function ConvertTo-NetxProcessArgumentString { - param([Parameter(Mandatory = $true)][string[]]$Arguments) - return (($Arguments | ForEach-Object { ConvertTo-NetxProcessArgument -Value "$_" }) -join " ") -} - -function Start-NetxPowerShell { - param( - [Parameter(Mandatory = $true)][string]$File, - [string[]]$Arguments = @(), - [string]$WorkingDirectory = "", - [ValidateSet("Hidden", "Normal", "Minimized")] - [string]$WindowStyle = "Hidden", - [switch]$Wait = $false, - [switch]$PassThru = $false - ) - $parts = @( - "-NoProfile", - "-WindowStyle", $WindowStyle, - "-ExecutionPolicy", "Bypass", - "-File", $File - ) + $Arguments - $sp = @{ - FilePath = "powershell.exe" - ArgumentList = (ConvertTo-NetxProcessArgumentString -Arguments $parts) - WindowStyle = $WindowStyle - } - if ($WorkingDirectory) { $sp.WorkingDirectory = $WorkingDirectory } - if ($Wait) { $sp.Wait = $true } - if ($PassThru -or $Wait) { $sp.PassThru = $true } - return Start-Process @sp -} - -function Get-NetxSystemPython { - # Prefer a real interpreter; skip the WindowsApps Store stub. - $candidates = @() - foreach ($cmd in @("python", "python3")) { - $c = Get-Command $cmd -ErrorAction SilentlyContinue - if ($c -and $c.Source -and ($c.Source -notmatch '\\WindowsApps\\')) { - $candidates += $c.Source - } - } - $pyLauncher = Get-Command "py" -ErrorAction SilentlyContinue - if ($pyLauncher -and $pyLauncher.Source -and ($pyLauncher.Source -notmatch '\\WindowsApps\\')) { - try { - $resolved = (& $pyLauncher.Source -3 -c "import sys; print(sys.executable)" 2>$null | Out-String).Trim() - if ($resolved -and (Test-Path -LiteralPath $resolved)) { - $candidates += $resolved - } - } catch {} - } - foreach ($p in @( - "$env:LOCALAPPDATA\Python\pythoncore-3.14-64\python.exe", - "$env:LOCALAPPDATA\Programs\Python\Python312\python.exe", - "$env:LOCALAPPDATA\Programs\Python\Python311\python.exe", - "$env:ProgramFiles\Python312\python.exe", - "$env:ProgramFiles\Python311\python.exe" - )) { - if ($p -and (Test-Path $p)) { $candidates += $p } - } - foreach ($p in ($candidates | Select-Object -Unique)) { - try { - $v = & $p -c "import sys; print('%d.%d'%sys.version_info[:2])" 2>$null - if ($v -match '^(3\.(1[1-9]|[2-9]\d))$') { return $p } - } catch {} - } - return $null -} - -function Get-NetxBundledPythonRoot { - param([Parameter(Mandatory = $true)][string]$ProgramRoot) - return Join-Path $ProgramRoot "python\runtime" -} - -function Repair-NetxShippedVenv { - param([Parameter(Mandatory = $true)][string]$ProgramRoot) - $cfgPath = Join-Path $ProgramRoot ".venv\pyvenv.cfg" - $rtRoot = Get-NetxBundledPythonRoot -ProgramRoot $ProgramRoot - $rtPy = Join-Path $rtRoot "python.exe" - if (-not (Test-Path -LiteralPath $rtPy)) { return $false } - if (-not (Test-Path -LiteralPath $cfgPath)) { return $false } - - $rtRootAbs = (Resolve-Path -LiteralPath $rtRoot).Path - $rtPyAbs = (Resolve-Path -LiteralPath $rtPy).Path - $ver = "3.14.0" - try { - $verOut = & $rtPyAbs -c "import sys; print('%d.%d.%d' % sys.version_info[:3])" 2>$null - if ($verOut) { $ver = ($verOut | Out-String).Trim() } - } catch {} - - $lines = @( - "home = $rtRootAbs", - "include-system-site-packages = false", - "version = $ver", - "executable = $rtPyAbs" - ) - Set-Content -LiteralPath $cfgPath -Value ($lines -join "`r`n") -Encoding ascii - return $true -} - -function Test-NetxVenvRunnable { - param([Parameter(Mandatory = $true)][string]$VenvPython) - if (-not (Test-Path -LiteralPath $VenvPython)) { return $false } - - $outFile = Join-Path $env:TEMP ("netx-venv-out-" + [Guid]::NewGuid().ToString("n") + ".txt") - $errFile = Join-Path $env:TEMP ("netx-venv-err-" + [Guid]::NewGuid().ToString("n") + ".txt") - try { - $psi = New-Object System.Diagnostics.ProcessStartInfo - $psi.FileName = $VenvPython - $psi.Arguments = '-c "import encodings, sys; sys.exit(0 if sys.prefix else 1)"' - $psi.UseShellExecute = $false - $psi.RedirectStandardOutput = $true - $psi.RedirectStandardError = $true - $psi.CreateNoWindow = $true - $p = [Diagnostics.Process]::Start($psi) - $stderr = $p.StandardError.ReadToEnd() - $null = $p.StandardOutput.ReadToEnd() - $p.WaitForExit() - if ($stderr -match 'did not find executable|No Python at|Fatal Python error') { - return $false - } - if ($p.ExitCode -ne 0) { return $false } - return $true - } finally { - Remove-Item -LiteralPath $outFile, $errFile -Force -ErrorAction SilentlyContinue - } -} - -function Ensure-NetxVenv { - param( - [Parameter(Mandatory = $true)][string]$ProgramRoot, - [switch]$ForcePip = $false - ) - $venvPy = Join-Path $ProgramRoot ".venv\Scripts\python.exe" - $req = Join-Path $ProgramRoot "requirements.txt" - $rtPy = Join-Path (Get-NetxBundledPythonRoot -ProgramRoot $ProgramRoot) "python.exe" - - if (Test-Path -LiteralPath $rtPy) { - $null = Repair-NetxShippedVenv -ProgramRoot $ProgramRoot - } - - if ((Test-Path -LiteralPath $venvPy) -and -not $ForcePip) { - if (Test-NetxVenvRunnable -VenvPython $venvPy) { - return $venvPy - } - Write-Host "[WARN] Shipped .venv exists but Python cannot start (broken pyvenv.cfg or missing runtime)." -ForegroundColor Yellow - if (Test-Path -LiteralPath $rtPy) { - $null = Repair-NetxShippedVenv -ProgramRoot $ProgramRoot - if (Test-NetxVenvRunnable -VenvPython $venvPy) { - Write-Host "==> Repaired .venv to use bundled python/runtime" -ForegroundColor Green - return $venvPy - } - } - if (-not $ForcePip) { - throw "venv_not_runnable: reinstall NetX Setup (ships python/runtime + .venv) or run repair as admin" - } - } - if (-not (Test-Path $venvPy)) { - $py = Get-NetxSystemPython - if (-not $py) { - throw "python_not_found: install Python 3.11+ (or ship Setup built with -CreateVenv)" - } - Write-Host "==> Creating .venv with $py" - $venvDir = Join-Path $ProgramRoot ".venv" - try { - & $py -m venv $venvDir - } catch { - throw "venv_create_failed: cannot write $venvDir (need admin / ship -CreateVenv). $($_.Exception.Message)" - } - if (-not (Test-Path $venvPy)) { - throw "venv_create_failed: missing $venvPy (Program Files may be read-only without elevation)" - } - $ForcePip = $true - } - if ($ForcePip) { - if (-not (Test-Path $req)) { throw "requirements_missing: $req" } - Write-Host "==> pip install -r requirements.txt" - & $venvPy -m pip install --upgrade pip - & $venvPy -m pip install -r $req - if ($LASTEXITCODE -ne 0) { throw "pip_install_failed" } - } - return $venvPy -} - -function Stop-NetxTrayProcesses { - param([string]$ProgramRoot = "") - $hits = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object { - $_.CommandLine -and $_.CommandLine -match 'netx_tray\.ps1' - }) - if ($ProgramRoot) { - $esc = [regex]::Escape($ProgramRoot) - $hits = @($hits | Where-Object { $_.CommandLine -match $esc -or $_.CommandLine -match 'netx_tray\.ps1' }) - } - foreach ($p in $hits) { - try { - Stop-Process -Id $p.ProcessId -Force -ErrorAction Stop - Write-Host "Stopped NetX tray PID=$($p.ProcessId)" - } catch {} - } -} - function ConvertTo-NetxSqlLiteral([string]$Value) { # Single-quote escaping for PostgreSQL string literals. return ($Value -replace "'", "''") diff --git a/packaging/build_release.ps1 b/packaging/build_release.ps1 index d737114..a1f1437 100644 --- a/packaging/build_release.ps1 +++ b/packaging/build_release.ps1 @@ -1,11 +1,10 @@ -param( +param( [string]$Version = "", [string]$OutDir = "", [switch]$SkipWebBuild = $false, [switch]$SkipPostgresDownload = $false, [switch]$SkipZip = $false, - # Default on: offline Setup must ship .venv so target PCs never pip-install. - [switch]$CreateVenv = $true + [switch]$CreateVenv = $false ) $ErrorActionPreference = "Stop" @@ -104,7 +103,7 @@ 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", "uninstall_prepare.ps1", "uninstall_delete_data.ps1", "sign_release.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 @@ -160,7 +159,7 @@ 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", "NetX-Stop.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 @@ -168,50 +167,15 @@ foreach ($cmdName in @("NetX-FirstRun.cmd", "NetX-Start.cmd", "NetX-Tray.cmd", " } if ($CreateVenv) { - Write-Host "==> Creating portable python/runtime + .venv (must not reference build-machine paths)" - $pyPath = Get-NetxSystemPython - if (-not $pyPath) { throw "python_not_found_for_venv: need Python 3.11+ (not WindowsApps stub)" } - Write-Host " Build Python: $pyPath" - - $prefix = (& $pyPath -c "import sys; print(sys.base_prefix)" 2>$null | Out-String).Trim() - if (-not $prefix -or -not (Test-Path -LiteralPath $prefix)) { - throw "python_base_prefix_not_found: $prefix" - } - $rtDir = Join-Path $stage "python\runtime" - if (Test-Path $rtDir) { Remove-Item -Recurse -Force $rtDir } - New-Item -ItemType Directory -Path $rtDir -Force | Out-Null - - Write-Host "==> Copying Python stdlib/runtime to $rtDir (exclude base site-packages)" - $spEx = Join-Path $prefix "Lib\site-packages" - $robocopy = Join-Path $env:SystemRoot "System32\robocopy.exe" - if (-not (Test-Path -LiteralPath $robocopy)) { throw "robocopy_not_found" } - & $robocopy $prefix $rtDir /E /XD $spEx __pycache__ /XF *.pyc /NFL /NDL /NJH /NJS /nc /ns /np | Out-Null - if ($LASTEXITCODE -ge 8) { throw "robocopy_python_runtime_failed: exit $LASTEXITCODE" } - - $rtPy = Join-Path $rtDir "python.exe" - if (-not (Test-Path -LiteralPath $rtPy)) { throw "python_runtime_missing: $rtPy" } - - $venvDir = Join-Path $stage ".venv" - if (Test-Path $venvDir) { Remove-Item -Recurse -Force $venvDir } - Write-Host "==> Creating .venv from shipped runtime (--copies)" - & $rtPy -m venv $venvDir --copies - if ($LASTEXITCODE -ne 0) { throw "venv_create_failed" } - - $venvPy = Join-Path $venvDir "Scripts\python.exe" - $null = Repair-NetxShippedVenv -ProgramRoot $stage - if (-not (Test-NetxVenvRunnable -VenvPython $venvPy)) { - throw "venv_not_runnable_after_build: check python/runtime copy" - } - + Write-Host "==> Creating .venv in stage (requires Python 3.11+ on PATH)" + $py = (Get-Command python -ErrorAction SilentlyContinue) + if (-not $py) { throw "python_not_found_for_venv" } + & $py.Source -m venv (Join-Path $stage ".venv") + $venvPy = Join-Path $stage ".venv\Scripts\python.exe" & $venvPy -m pip install --upgrade pip & $venvPy -m pip install -r (Join-Path $stage "requirements.txt") if ($LASTEXITCODE -ne 0) { throw "pip_install_failed" } - $null = Repair-NetxShippedVenv -ProgramRoot $stage - if (-not (Test-NetxVenvRunnable -VenvPython $venvPy)) { - throw "venv_not_runnable_after_pip" - } - Write-Host "==> Slimming .venv (drop tests / __pycache__ / *.pyc)" $site = Join-Path $stage ".venv\Lib\site-packages" if (Test-Path $site) { diff --git a/packaging/cmd/NetX-FirstRun.cmd b/packaging/cmd/NetX-FirstRun.cmd index 63fa9e5..c07282e 100644 --- a/packaging/cmd/NetX-FirstRun.cmd +++ b/packaging/cmd/NetX-FirstRun.cmd @@ -3,26 +3,5 @@ setlocal set "ROOT=%~dp0" if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%" cd /d "%ROOT%" -title NetX — Reconfigure database -echo. -echo NetX database reconfigure / 重新配置数据库 -echo Prefer the installer wizard for first-time setup. -echo 首次安装请用安装向导选择内置或外置数据库。 -echo This window is for repair / reconfigure only. -echo 本窗口仅用于修复或重新配置。 -echo. -powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%ROOT%\packaging\setup_first_run.ps1" -ProgramRoot "%ROOT%" -DataRoot "%ProgramData%\NetX" -set "EC=%ERRORLEVEL%" -if not "%EC%"=="0" ( - echo. - echo Setup failed / 配置失败 exit=%EC% - pause - exit /b %EC% -) -echo. -echo Starting NetX tray... / 正在启动托盘... -start "" powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "%ROOT%\packaging\netx_tray.ps1" -ProgramRoot "%ROOT%" -DataRoot "%ProgramData%\NetX" -StartOnLaunch -echo. -echo Done. You can close this window. / 完成,可关闭本窗口。 -pause -exit /b 0 +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 index b489f7d..67f3675 100644 --- a/packaging/cmd/NetX-Start.cmd +++ b/packaging/cmd/NetX-Start.cmd @@ -3,5 +3,5 @@ setlocal set "ROOT=%~dp0" if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%" cd /d "%ROOT%" -start "" powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "%ROOT%\packaging\launch_shortcut.ps1" -Action start -ProgramRoot "%ROOT%" -DataRoot "%ProgramData%\NetX" -exit /b 0 +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-Stop.cmd b/packaging/cmd/NetX-Stop.cmd deleted file mode 100644 index 7722e38..0000000 --- a/packaging/cmd/NetX-Stop.cmd +++ /dev/null @@ -1,7 +0,0 @@ -@echo off -setlocal -set "ROOT=%~dp0" -if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%" -cd /d "%ROOT%" -start "" powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "%ROOT%\packaging\launch_shortcut.ps1" -Action stop -ProgramRoot "%ROOT%" -DataRoot "%ProgramData%\NetX" -exit /b 0 diff --git a/packaging/cmd/NetX-Tray.cmd b/packaging/cmd/NetX-Tray.cmd index 7cdd5af..262f21e 100644 --- a/packaging/cmd/NetX-Tray.cmd +++ b/packaging/cmd/NetX-Tray.cmd @@ -3,5 +3,5 @@ setlocal set "ROOT=%~dp0" if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%" cd /d "%ROOT%" -start "" powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "%ROOT%\packaging\launch_shortcut.ps1" -Action tray -ProgramRoot "%ROOT%" -DataRoot "%ProgramData%\NetX" -StartOnLaunch -exit /b 0 +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 335ce0c..5eb1683 100644 --- a/packaging/installer/netx.iss +++ b/packaging/installer/netx.iss @@ -1,11 +1,10 @@ ; NetX Windows installer (Inno Setup 6+) ; Compile after: packaging\build_release.ps1 ; ISCC.exe packaging\installer\netx.iss -; Encoding: UTF-8 with BOM (required for Chinese CustomMessages) #define MyAppName "NetX" #ifndef MyAppVersion - #define MyAppVersion "0.4.6" + #define MyAppVersion "0.4.5" #endif #define MyAppPublisher "NetX" #define MyAppURL "https://github.com/hansjone/netx" @@ -26,10 +25,7 @@ DefaultGroupName=NetX DisableProgramGroupPage=yes LicenseFile={#StageDir}\LICENSE OutputDir=..\release -#ifndef OutputBaseFilename - #define OutputBaseFilename "NetX-Setup-" + MyAppVersion -#endif -OutputBaseFilename={#OutputBaseFilename} +OutputBaseFilename=NetX-Setup-{#MyAppVersion} SetupIconFile={#IconFile} UninstallDisplayIcon={app}\packaging\assets\netx.ico Compression=lzma2 @@ -38,10 +34,8 @@ WizardStyle=modern ArchitecturesAllowed=x64compatible ArchitecturesInstallIn64BitMode=x64compatible PrivilegesRequired=admin -; Data lives under {commonappdata}\NetX - not overwritten by upgrades. -; Do NOT use CloseApplications=yes — it can freeze/beep on the Tasks/Ready -; pages while scanning locked NetX tray/service processes with no visible dialog. -CloseApplications=no +; Data lives under {commonappdata}\NetX 鈥?not overwritten by upgrades +CloseApplications=yes ShowLanguageDialog=yes [Languages] @@ -50,82 +44,26 @@ Name: "chinesesimplified"; MessagesFile: "Languages\ChineseSimplified.isl" [CustomMessages] english.CreateDesktopIcon=Create a desktop shortcut -english.SetupOptions=After install: +english.SetupOptions=Setup options: +english.RunFirstSetup=Run first-time setup after install (bundled database, offline) 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) -english.UninstallDeleteData=Also delete NetX data (%ProgramData%\NetX)?%n%nYes = remove database, settings, and secrets%nNo = keep data for a later reinstall -english.DbPageCaption=Database -english.DbPageDescription=Choose built-in or external PostgreSQL. Optional credential key: paste from an existing install, or leave empty to auto-generate. -english.DbBundled=Built-in PostgreSQL (portable, offline) -english.DbExternal=External PostgreSQL (existing server) -english.DbHost=Host -english.DbPort=Port -english.DbUser=User -english.DbPassword=Password -english.DbName=Database -english.CredKey=Credential key -english.CredKeyHint=NETX_CREDENTIAL_SECRET_KEY (optional — empty = auto-generate) -english.DbFieldsRequired=Please fill in host, port, user, password, and database name. -english.DbTesting=Testing PostgreSQL connection, please wait… -english.DbTestFailed=Cannot connect to PostgreSQL with these settings.%n%n%1%n%nFix the settings and try again. -english.DbTestExtractFailed=Could not extract PostgreSQL client tools for connection test. -english.DbSilentExternalMissing=Silent install with external DB requires /DbHost /DbUser /DbPassword /DbName (optional /DbPort). -english.DbApplyFailed=Database configuration failed after file install (exit %1).%n%nLog: %2%n%nFix the problem, then use Start Menu → Reconfigure database — or reinstall. -english.DbApplyExecFailed=Could not start database configuration script. -english.DbBundledMissing=Built-in PostgreSQL files are missing from the install folder:%n%1%n%nThe Setup package is incomplete. Re-download NetX-Setup and install again. -english.DbEnvMissing=Database configuration did not write %ProgramData%\NetX\.env. -english.ReconfigureDb=Reconfigure database -chinesesimplified.CreateDesktopIcon=创建桌面快捷方式 -chinesesimplified.SetupOptions=安装完成后: -chinesesimplified.StartTrayNow=立即启动 NetX 托盘 -chinesesimplified.EnableAutostart=开机时自动启动 NetX 托盘 -chinesesimplified.EnableAutoUpdate=启用每日静默自动更新(以后需要联网) -chinesesimplified.InstallService=安装为 Windows 服务(使用已捆绑的 WinSW,可离线) -chinesesimplified.UninstallDeleteData=是否同时删除 NetX 数据(%ProgramData%\NetX)?%n%n是 = 删除数据库、配置和密钥%n否 = 保留数据以便以后重装 -chinesesimplified.DbPageCaption=数据库 -chinesesimplified.DbPageDescription=选择内置或外置 PostgreSQL。可选粘贴已有 NETX_CREDENTIAL_SECRET_KEY;留空则自动生成。 -chinesesimplified.DbBundled=内置 PostgreSQL(便携,可离线) -chinesesimplified.DbExternal=外置 PostgreSQL(已有服务器) -chinesesimplified.DbHost=主机 -chinesesimplified.DbPort=端口 -chinesesimplified.DbUser=用户 -chinesesimplified.DbPassword=密码 -chinesesimplified.DbName=数据库名 -chinesesimplified.CredKey=凭据密钥 -chinesesimplified.CredKeyHint=NETX_CREDENTIAL_SECRET_KEY(可选,留空=自动生成) -chinesesimplified.DbFieldsRequired=请填写主机、端口、用户、密码和数据库名。 -chinesesimplified.DbTesting=正在测试 PostgreSQL 连接,请稍候… -chinesesimplified.DbTestFailed=无法用当前设置连接 PostgreSQL。%n%n%1%n%n请改正后重试。 -chinesesimplified.DbTestExtractFailed=无法解压 PostgreSQL 客户端以测试连接。 -chinesesimplified.DbSilentExternalMissing=静默安装外置库需要参数 /DbHost /DbUser /DbPassword /DbName(可选 /DbPort)。 -chinesesimplified.DbApplyFailed=文件安装后数据库自动配置失败(退出码 %1)。%n%n日志: %2%n%n请处理后使用开始菜单 → 重新配置数据库,或重新安装。 -chinesesimplified.DbApplyExecFailed=无法启动数据库配置脚本。 -chinesesimplified.DbBundledMissing=安装目录中缺少内置 PostgreSQL 文件:%n%1%n%n安装包不完整,请重新下载 NetX-Setup 后再装。 -chinesesimplified.DbEnvMissing=数据库配置未写入 %ProgramData%\NetX\.env。 -chinesesimplified.ReconfigureDb=重新配置数据库 - +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" -; Connection-test tools (wizard Next / silent PrepareToInstall only) -Source: "test_pg_conn.ps1"; Flags: dontcopy -Source: "{#StageDir}\postgres\pgsql\bin\psql.exe"; Flags: dontcopy -Source: "{#StageDir}\postgres\pgsql\bin\libpq.dll"; Flags: dontcopy -Source: "{#StageDir}\postgres\pgsql\bin\libssl-3-x64.dll"; Flags: dontcopy -Source: "{#StageDir}\postgres\pgsql\bin\libcrypto-3-x64.dll"; Flags: dontcopy -Source: "{#StageDir}\postgres\pgsql\bin\libintl-9.dll"; Flags: dontcopy -Source: "{#StageDir}\postgres\pgsql\bin\libiconv-2.dll"; Flags: dontcopy -Source: "{#StageDir}\postgres\pgsql\bin\libwinpthread-1.dll"; Flags: dontcopy -Source: "{#StageDir}\postgres\pgsql\bin\zlib1.dll"; Flags: dontcopy -Source: "{#StageDir}\postgres\pgsql\bin\liblz4.dll"; Flags: dontcopy -Source: "{#StageDir}\postgres\pgsql\bin\libzstd.dll"; Flags: dontcopy -Source: "{#StageDir}\postgres\pgsql\bin\libxml2.dll"; Flags: dontcopy -Source: "{#StageDir}\postgres\pgsql\bin\libxslt.dll"; Flags: dontcopy [Dirs] Name: "{commonappdata}\NetX" @@ -135,737 +73,31 @@ Name: "{commonappdata}\NetX\data\runtime" Name: "{commonappdata}\NetX\pgdata" Name: "{commonappdata}\NetX\backups" -[InstallDelete] -; Remove legacy desktop shortcuts from older Setup builds (keep a single NetX icon). -Type: files; Name: "{commondesktop}\NetX Start.lnk" -Type: files; Name: "{commondesktop}\NetX First-time setup.lnk" -Type: files; Name: "{commondesktop}\首次配置(内置或外置数据库).lnk" - [Icons] ; 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: "{app}\NetX-Stop.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}\{cm:ReconfigureDb}"; Filename: "{app}\NetX-FirstRun.cmd"; 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" -; One desktop shortcut only (tray = start + tray UI). Start Menu keeps Start/Stop entries. +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] -Filename: "powershell.exe"; Parameters: "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File ""{app}\packaging\netx_tray.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"" -StartOnLaunch"; WorkingDir: "{app}"; Description: "{cm:StartTrayNow}"; Flags: postinstall runhidden nowait skipifsilent; Check: NetxShouldStartTray -Filename: "powershell.exe"; Parameters: "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File ""{app}\packaging\install_autostart.ps1"" -ProgramRoot ""{app}"""; WorkingDir: "{app}"; Description: "{cm:EnableAutostart}"; Flags: postinstall runhidden skipifsilent unchecked -Filename: "powershell.exe"; Parameters: "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File ""{app}\packaging\install_update_task.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}"; Description: "{cm:EnableAutoUpdate}"; Flags: postinstall runhidden skipifsilent unchecked -Filename: "powershell.exe"; Parameters: "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File ""{app}\packaging\install_service.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"" -Start"; WorkingDir: "{app}"; Description: "{cm:InstallService}"; Flags: postinstall runhidden skipifsilent unchecked - -; Stop runtime BEFORE files are deleted. Do not also Exec prepare from -; InitializeUninstall (that previously raced/killed unins000 or hung the UI). -[UninstallRun] -Filename: "powershell.exe"; Parameters: "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File ""{app}\packaging\uninstall_prepare.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}"; RunOnceId: "StopNetX"; Flags: runhidden waituntilterminated +; First-run is NonInteractive + bundled DB so offline machines succeed without prompts or downloads. +Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\setup_first_run.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"" -NonInteractive -DbMode bundled"; WorkingDir: "{app}"; Flags: postinstall skipifsilent; Tasks: firstrun +Filename: "powershell.exe"; Parameters: "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File ""{app}\packaging\netx_tray.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"" -StartOnLaunch"; WorkingDir: "{app}"; Description: "{cm:StartTrayNow}"; Flags: postinstall nowait skipifsilent unchecked +Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\install_autostart.ps1"" -ProgramRoot ""{app}"""; WorkingDir: "{app}"; Description: "{cm:EnableAutostart}"; Flags: postinstall skipifsilent unchecked +Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\install_update_task.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}"; Description: "{cm:EnableAutoUpdate}"; Flags: postinstall skipifsilent unchecked +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] -; Program files always removed. Data dir only when user chose Yes in InitializeUninstall. +; Do NOT delete {commonappdata}\NetX 鈥?preserves DB and secrets across reinstall Type: filesandordirs; Name: "{app}" -[Code] -var - GDeleteNetxData: Boolean; - GTempDeleteDataScript: String; - DbPage: TWizardPage; - RbBundled: TNewRadioButton; - RbExternal: TNewRadioButton; - LblHost: TNewStaticText; - LblPort: TNewStaticText; - LblUser: TNewStaticText; - LblPassword: TNewStaticText; - LblDbName: TNewStaticText; - EdHost: TNewEdit; - EdPort: TNewEdit; - EdUser: TNewEdit; - EdPassword: TNewEdit; - EdDbName: TNewEdit; - LblCredKey: TNewStaticText; - LblCredHint: TNewStaticText; - EdCredKey: TNewEdit; - GDbMode: String; - GDbUrl: String; - GDbHost: String; - GDbPort: String; - GDbUser: String; - GDbPassword: String; - GDbName: String; - GCredKey: String; - GSkipDbPage: Boolean; - GDbConfigured: Boolean; - GPgToolsReady: Boolean; - GDbConnOk: Boolean; - GNextBtnCaption: String; - -function EnvHasDbMode(): Boolean; -var - Lines: TArrayOfString; - i: Integer; - Line: String; -begin - Result := False; - if not LoadStringsFromFile(ExpandConstant('{commonappdata}\NetX\.env'), Lines) then - Exit; - for i := 0 to GetArrayLength(Lines) - 1 do - begin - Line := Trim(Lines[i]); - if Pos('NETX_DB_MODE=', Line) = 1 then - begin - if Length(Line) > Length('NETX_DB_MODE=') then - begin - Result := True; - Exit; - end; - end; - end; -end; - -function EnvModeIsExternal(): Boolean; -var - Lines: TArrayOfString; - i: Integer; - Line: String; -begin - Result := False; - if not LoadStringsFromFile(ExpandConstant('{commonappdata}\NetX\.env'), Lines) then - Exit; - for i := 0 to GetArrayLength(Lines) - 1 do - begin - Line := Trim(Lines[i]); - if CompareText(Line, 'NETX_DB_MODE=external') = 0 then - begin - Result := True; - Exit; - end; - end; -end; - -function NetxShouldStartTray(): Boolean; -begin - Result := GDbConfigured or - FileExists(ExpandConstant('{commonappdata}\NetX\.env')); -end; - -procedure UpdateDbFieldState(); -var - Ext: Boolean; -begin - Ext := RbExternal.Checked; - LblHost.Enabled := Ext; - LblPort.Enabled := Ext; - LblUser.Enabled := Ext; - LblPassword.Enabled := Ext; - LblDbName.Enabled := Ext; - EdHost.Enabled := Ext; - EdPort.Enabled := Ext; - EdUser.Enabled := Ext; - EdPassword.Enabled := Ext; - EdDbName.Enabled := Ext; -end; - -procedure DbModeClick(Sender: TObject); -begin - UpdateDbFieldState(); -end; - -function ExtractPgClientTools(): Boolean; -begin - if GPgToolsReady and FileExists(ExpandConstant('{tmp}\psql.exe')) and - FileExists(ExpandConstant('{tmp}\test_pg_conn.ps1')) then - begin - Result := True; - Exit; - end; - Result := False; - try - ExtractTemporaryFile('test_pg_conn.ps1'); - ExtractTemporaryFile('psql.exe'); - ExtractTemporaryFile('libpq.dll'); - ExtractTemporaryFile('libssl-3-x64.dll'); - ExtractTemporaryFile('libcrypto-3-x64.dll'); - ExtractTemporaryFile('libintl-9.dll'); - ExtractTemporaryFile('libiconv-2.dll'); - ExtractTemporaryFile('libwinpthread-1.dll'); - ExtractTemporaryFile('zlib1.dll'); - ExtractTemporaryFile('liblz4.dll'); - ExtractTemporaryFile('libzstd.dll'); - ExtractTemporaryFile('libxml2.dll'); - ExtractTemporaryFile('libxslt.dll'); - Result := FileExists(ExpandConstant('{tmp}\psql.exe')) and - FileExists(ExpandConstant('{tmp}\test_pg_conn.ps1')); - GPgToolsReady := Result; - except - Result := False; - GPgToolsReady := False; - end; -end; - -procedure SetWizardBusy(const Busy: Boolean; const StatusText: String); -begin - if Busy then - begin - if GNextBtnCaption = '' then - GNextBtnCaption := WizardForm.NextButton.Caption; - WizardForm.NextButton.Caption := StatusText; - WizardForm.NextButton.Enabled := False; - WizardForm.BackButton.Enabled := False; - WizardForm.CancelButton.Enabled := False; - end - else - begin - if GNextBtnCaption <> '' then - WizardForm.NextButton.Caption := GNextBtnCaption; - WizardForm.NextButton.Enabled := True; - WizardForm.BackButton.Enabled := True; - WizardForm.CancelButton.Enabled := True; - end; - WizardForm.Refresh; -end; - -function TestExternalDb(const Host, Port, User, Password, DbName: String; var ErrMsg: String): Boolean; -var - ResultCode: Integer; - UrlFile: String; - ErrFile: String; - PwFile: String; - Params: String; - Lines: TArrayOfString; - i: Integer; -begin - Result := False; - ErrMsg := ''; - if not ExtractPgClientTools() then - begin - ErrMsg := ExpandConstant('{cm:DbTestExtractFailed}'); - Exit; - end; - - UrlFile := ExpandConstant('{tmp}\netx_db_url.txt'); - ErrFile := ExpandConstant('{tmp}\netx_db_test_err.txt'); - PwFile := ExpandConstant('{tmp}\netx_db_pw.txt'); - DeleteFile(UrlFile); - DeleteFile(ErrFile); - SaveStringToFile(PwFile, Password, False); - - Params := - '-NoProfile -ExecutionPolicy Bypass -File "' + ExpandConstant('{tmp}\test_pg_conn.ps1') + '"' + - ' -PgHost "' + Host + '"' + - ' -Port ' + Port + - ' -User "' + User + '"' + - ' -PasswordFile "' + PwFile + '"' + - ' -Database "' + DbName + '"' + - ' -PsqlPath "' + ExpandConstant('{tmp}\psql.exe') + '"' + - ' -OutUrlFile "' + UrlFile + '"' + - ' -OutErrFile "' + ErrFile + '"'; - - if not Exec('powershell.exe', Params, ExpandConstant('{tmp}'), SW_HIDE, ewWaitUntilTerminated, ResultCode) then - begin - ErrMsg := 'powershell_exec_failed'; - DeleteFile(PwFile); - Exit; - end; - DeleteFile(PwFile); - if ResultCode <> 0 then - begin - ErrMsg := 'psql/test exit ' + IntToStr(ResultCode); - if LoadStringsFromFile(ErrFile, Lines) then - begin - ErrMsg := ''; - for i := 0 to GetArrayLength(Lines) - 1 do - begin - if ErrMsg <> '' then - ErrMsg := ErrMsg + #13#10; - ErrMsg := ErrMsg + Lines[i]; - end; - if ErrMsg = '' then - ErrMsg := 'psql/test exit ' + IntToStr(ResultCode); - end; - Exit; - end; - if not LoadStringsFromFile(UrlFile, Lines) or (GetArrayLength(Lines) < 1) or (Trim(Lines[0]) = '') then - begin - ErrMsg := 'url_file_missing'; - Exit; - end; - GDbUrl := Trim(Lines[0]); - Result := True; -end; - -function SaveExternalFieldsFromControls(var ErrMsg: String): Boolean; -begin - Result := False; - GDbHost := Trim(EdHost.Text); - GDbPort := Trim(EdPort.Text); - GDbUser := Trim(EdUser.Text); - GDbPassword := EdPassword.Text; - GDbName := Trim(EdDbName.Text); - GDbConnOk := False; - GDbUrl := ''; - if (GDbHost = '') or (GDbPort = '') or (GDbUser = '') or - (GDbPassword = '') or (GDbName = '') then - begin - ErrMsg := ExpandConstant('{cm:DbFieldsRequired}'); - Exit; - end; - GDbMode := 'external'; - Result := True; -end; - -function ValidateExternalDbSaved(var ErrMsg: String): Boolean; -begin - Result := False; - if GDbConnOk and (GDbUrl <> '') then - begin - Result := True; - Exit; - end; - if not TestExternalDb(GDbHost, GDbPort, GDbUser, GDbPassword, GDbName, ErrMsg) then - begin - if ErrMsg = '' then - ErrMsg := 'connection_failed'; - ErrMsg := FmtMessage(ExpandConstant('{cm:DbTestFailed}'), [ErrMsg]); - GDbConnOk := False; - Exit; - end; - GDbConnOk := True; - Result := True; -end; - -function ApplySilentDbParams(var ErrMsg: String): Boolean; -var - Mode, Host, Port, User, Password, DbName: String; -begin - Result := False; - GCredKey := Trim(ExpandConstant('{param:CredentialSecretKey|}')); - Mode := LowerCase(ExpandConstant('{param:DbMode|bundled}')); - if Mode = '' then - Mode := 'bundled'; - if Mode = 'bundled' then - begin - GDbMode := 'bundled'; - GDbUrl := ''; - Result := True; - Exit; - end; - if Mode <> 'external' then - begin - ErrMsg := 'invalid DbMode (use bundled or external)'; - Exit; - end; - Host := ExpandConstant('{param:DbHost|}'); - Port := ExpandConstant('{param:DbPort|5432}'); - User := ExpandConstant('{param:DbUser|}'); - Password := ExpandConstant('{param:DbPassword|}'); - DbName := ExpandConstant('{param:DbName|}'); - if (Host = '') or (User = '') or (Password = '') or (DbName = '') then - begin - ErrMsg := ExpandConstant('{cm:DbSilentExternalMissing}'); - Exit; - end; - if not TestExternalDb(Host, Port, User, Password, DbName, ErrMsg) then - begin - if ErrMsg = '' then - ErrMsg := 'connection_failed'; - ErrMsg := FmtMessage(ExpandConstant('{cm:DbTestFailed}'), [ErrMsg]); - Exit; - end; - GDbMode := 'external'; - Result := True; -end; - -procedure CaptureCredKeyFromControls(); -begin - if EdCredKey <> nil then - GCredKey := Trim(EdCredKey.Text) - else - GCredKey := ''; -end; - -procedure InitializeWizard(); -var - TopPos: Integer; -begin - GDbMode := 'bundled'; - GDbUrl := ''; - GDbHost := ''; - GDbPort := '5432'; - GDbUser := ''; - GDbPassword := ''; - GDbName := ''; - GCredKey := ''; - GDbConfigured := False; - GPgToolsReady := False; - GDbConnOk := False; - { Always show DB page so choosing built-in always re-runs auto-configure. - Skipping when .env exists caused upgrades/reinstalls to keep a broken or - external config and still launch tray → confusing "first-time" prompts. } - GSkipDbPage := False; - - DbPage := CreateCustomPage(wpSelectDir, - ExpandConstant('{cm:DbPageCaption}'), - ExpandConstant('{cm:DbPageDescription}')); - - RbBundled := TNewRadioButton.Create(DbPage); - RbBundled.Parent := DbPage.Surface; - RbBundled.Caption := ExpandConstant('{cm:DbBundled}'); - RbBundled.Checked := True; - RbBundled.Top := ScaleY(8); - RbBundled.Left := ScaleX(0); - RbBundled.Width := DbPage.SurfaceWidth; - RbBundled.OnClick := @DbModeClick; - - RbExternal := TNewRadioButton.Create(DbPage); - RbExternal.Parent := DbPage.Surface; - RbExternal.Caption := ExpandConstant('{cm:DbExternal}'); - RbExternal.Top := RbBundled.Top + ScaleY(28); - RbExternal.Left := ScaleX(0); - RbExternal.Width := DbPage.SurfaceWidth; - RbExternal.OnClick := @DbModeClick; - - TopPos := RbExternal.Top + ScaleY(36); - - LblHost := TNewStaticText.Create(DbPage); - LblHost.Parent := DbPage.Surface; - LblHost.Caption := ExpandConstant('{cm:DbHost}'); - LblHost.Top := TopPos; - LblHost.Left := ScaleX(16); - - EdHost := TNewEdit.Create(DbPage); - EdHost.Parent := DbPage.Surface; - EdHost.Top := TopPos; - EdHost.Left := ScaleX(120); - EdHost.Width := ScaleX(220); - EdHost.Text := '127.0.0.1'; - - TopPos := TopPos + ScaleY(28); - LblPort := TNewStaticText.Create(DbPage); - LblPort.Parent := DbPage.Surface; - LblPort.Caption := ExpandConstant('{cm:DbPort}'); - LblPort.Top := TopPos; - LblPort.Left := ScaleX(16); - - EdPort := TNewEdit.Create(DbPage); - EdPort.Parent := DbPage.Surface; - EdPort.Top := TopPos; - EdPort.Left := ScaleX(120); - EdPort.Width := ScaleX(80); - EdPort.Text := '5432'; - - TopPos := TopPos + ScaleY(28); - LblUser := TNewStaticText.Create(DbPage); - LblUser.Parent := DbPage.Surface; - LblUser.Caption := ExpandConstant('{cm:DbUser}'); - LblUser.Top := TopPos; - LblUser.Left := ScaleX(16); - - EdUser := TNewEdit.Create(DbPage); - EdUser.Parent := DbPage.Surface; - EdUser.Top := TopPos; - EdUser.Left := ScaleX(120); - EdUser.Width := ScaleX(220); - EdUser.Text := 'netx'; - - TopPos := TopPos + ScaleY(28); - LblPassword := TNewStaticText.Create(DbPage); - LblPassword.Parent := DbPage.Surface; - LblPassword.Caption := ExpandConstant('{cm:DbPassword}'); - LblPassword.Top := TopPos; - LblPassword.Left := ScaleX(16); - - EdPassword := TNewEdit.Create(DbPage); - EdPassword.Parent := DbPage.Surface; - EdPassword.Top := TopPos; - EdPassword.Left := ScaleX(120); - EdPassword.Width := ScaleX(220); - EdPassword.PasswordChar := '*'; - - TopPos := TopPos + ScaleY(28); - LblDbName := TNewStaticText.Create(DbPage); - LblDbName.Parent := DbPage.Surface; - LblDbName.Caption := ExpandConstant('{cm:DbName}'); - LblDbName.Top := TopPos; - LblDbName.Left := ScaleX(16); - - EdDbName := TNewEdit.Create(DbPage); - EdDbName.Parent := DbPage.Surface; - EdDbName.Top := TopPos; - EdDbName.Left := ScaleX(120); - EdDbName.Width := ScaleX(220); - EdDbName.Text := 'netx'; - - TopPos := TopPos + ScaleY(36); - LblCredKey := TNewStaticText.Create(DbPage); - LblCredKey.Parent := DbPage.Surface; - LblCredKey.Caption := ExpandConstant('{cm:CredKey}'); - LblCredKey.Top := TopPos; - LblCredKey.Left := ScaleX(0); - - EdCredKey := TNewEdit.Create(DbPage); - EdCredKey.Parent := DbPage.Surface; - EdCredKey.Top := TopPos; - EdCredKey.Left := ScaleX(120); - EdCredKey.Width := DbPage.SurfaceWidth - ScaleX(120); - EdCredKey.Text := ''; - - TopPos := TopPos + ScaleY(24); - LblCredHint := TNewStaticText.Create(DbPage); - LblCredHint.Parent := DbPage.Surface; - LblCredHint.Caption := ExpandConstant('{cm:CredKeyHint}'); - LblCredHint.Top := TopPos; - LblCredHint.Left := ScaleX(120); - LblCredHint.Width := DbPage.SurfaceWidth - ScaleX(120); - LblCredHint.AutoSize := False; - - UpdateDbFieldState(); - - { Prefill from existing ProgramData\.env when present. } - if EnvModeIsExternal() then - begin - RbExternal.Checked := True; - UpdateDbFieldState(); - end - else if EnvHasDbMode() then - begin - RbBundled.Checked := True; - UpdateDbFieldState(); - end; - - if WizardSilent then - begin - { Params applied in PrepareToInstall } - end; -end; - -function ShouldSkipPage(PageID: Integer): Boolean; -begin - Result := False; - if (DbPage <> nil) and (PageID = DbPage.ID) then - Result := GSkipDbPage; -end; - -function NextButtonClick(CurPageID: Integer): Boolean; -var - ErrMsg: String; -begin - Result := True; - { Only the DB page runs validation. Other pages must always proceed - (Tasks/Ready used to appear "dead" when CloseApplications hung). } - if (DbPage = nil) or (CurPageID <> DbPage.ID) or GSkipDbPage then - Exit; - - CaptureCredKeyFromControls(); - if RbBundled.Checked then - begin - GDbMode := 'bundled'; - GDbUrl := ''; - GDbConnOk := True; - Result := True; - Exit; - end; - - if not SaveExternalFieldsFromControls(ErrMsg) then - begin - MsgBox(ErrMsg, mbError, MB_OK); - Result := False; - Exit; - end; - - { Connection test here so bad settings never reach Tasks/Ready. - Show busy state — ExtractTemporaryFile + psql can take a few seconds. } - SetWizardBusy(True, ExpandConstant('{cm:DbTesting}')); - try - Result := ValidateExternalDbSaved(ErrMsg); - finally - SetWizardBusy(False, ''); - end; - if not Result then - MsgBox(ErrMsg, mbError, MB_OK); -end; - -function PrepareToInstall(var NeedsRestart: Boolean): String; -var - ErrMsg: String; -begin - Result := ''; - NeedsRestart := False; - if GSkipDbPage then - Exit; - - if WizardSilent then - begin - if not ApplySilentDbParams(ErrMsg) then - Result := ErrMsg; - Exit; - end; - - if GDbMode = '' then - GDbMode := 'bundled'; - - { Re-check only if user never got a successful test (should be rare). } - if (GDbMode = 'external') and (not GDbConnOk) then - begin - if not ValidateExternalDbSaved(ErrMsg) then - Result := ErrMsg; - end; -end; - -function ApplyDatabaseConfig(): Boolean; -var - ResultCode: Integer; - Params: String; - UrlFile: String; - KeyFile: String; - LogFile: String; - InitDb: String; - EnvFile: String; - Wrapper: String; - WrapBody: String; -begin - Result := False; - GDbConfigured := False; - if GDbMode = '' then - GDbMode := 'bundled'; - - InitDb := ExpandConstant('{app}\postgres\pgsql\bin\initdb.exe'); - if (GDbMode = 'bundled') and (not FileExists(InitDb)) then - begin - MsgBox(FmtMessage(ExpandConstant('{cm:DbBundledMissing}'), [InitDb]), mbError, MB_OK); - Exit; - end; - - ForceDirectories(ExpandConstant('{commonappdata}\NetX\data\runtime')); - LogFile := ExpandConstant('{commonappdata}\NetX\data\runtime\setup_first_run.log'); - Wrapper := ExpandConstant('{tmp}\netx_apply_db.ps1'); - - WrapBody := - '$ErrorActionPreference = ''Stop''' + #13#10 + - '$log = ''' + LogFile + '''' + #13#10 + - 'Start-Transcript -Path $log -Force | Out-Null' + #13#10 + - 'try {' + #13#10 + - ' & ''' + ExpandConstant('{app}\packaging\setup_first_run.ps1') + '''' + - ' -ProgramRoot ''' + ExpandConstant('{app}') + '''' + - ' -DataRoot ''' + ExpandConstant('{commonappdata}\NetX') + '''' + - ' -NonInteractive -DbMode ' + GDbMode; - - if GDbMode = 'external' then - begin - UrlFile := ExpandConstant('{tmp}\netx_db_url.txt'); - if GDbUrl <> '' then - SaveStringToFile(UrlFile, GDbUrl, False); - WrapBody := WrapBody + - ' -ExternalDatabaseUrlFile ''' + UrlFile + ''' -SkipExternalProbe'; - end; - - if GCredKey <> '' then - begin - KeyFile := ExpandConstant('{tmp}\netx_cred_key.txt'); - SaveStringToFile(KeyFile, GCredKey, False); - WrapBody := WrapBody + ' -CredentialSecretKeyFile ''' + KeyFile + ''''; - end; - - WrapBody := WrapBody + #13#10 + - ' if ($null -ne $LASTEXITCODE -and $LASTEXITCODE -ne 0) { exit $LASTEXITCODE }' + #13#10 + - '} catch {' + #13#10 + - ' Write-Error $_' + #13#10 + - ' exit 1' + #13#10 + - '} finally {' + #13#10 + - ' Stop-Transcript | Out-Null' + #13#10 + - '}' + #13#10; - - SaveStringToFile(Wrapper, WrapBody, False); - Params := '-NoProfile -ExecutionPolicy Bypass -File "' + Wrapper + '"'; - - if not Exec( - 'powershell.exe', Params, ExpandConstant('{app}'), - SW_HIDE, ewWaitUntilTerminated, ResultCode) then - begin - MsgBox(ExpandConstant('{cm:DbApplyExecFailed}'), mbError, MB_OK); - Exit; - end; - if ResultCode <> 0 then - begin - MsgBox(FmtMessage(ExpandConstant('{cm:DbApplyFailed}'), [IntToStr(ResultCode), LogFile]), mbError, MB_OK); - Exit; - end; - - EnvFile := ExpandConstant('{commonappdata}\NetX\.env'); - if not FileExists(EnvFile) then - begin - MsgBox(ExpandConstant('{cm:DbEnvMissing}'), mbError, MB_OK); - Exit; - end; - - GDbConfigured := True; - Result := True; -end; - -function InitializeUninstall(): Boolean; -var - DeleteSrc: String; -begin - Result := True; - GDeleteNetxData := False; - GTempDeleteDataScript := ExpandConstant('{tmp}\netx_uninstall_delete_data.ps1'); - - // Copy data-wipe helper to TEMP before program files are removed. - DeleteSrc := ExpandConstant('{app}\packaging\uninstall_delete_data.ps1'); - if FileExists(DeleteSrc) then - CopyFile(DeleteSrc, GTempDeleteDataScript, False); - - if UninstallSilent then - begin - // Settings often uses QuietUninstallString; keep data by default. - GDeleteNetxData := False; - end - else - begin - GDeleteNetxData := - MsgBox(ExpandConstant('{cm:UninstallDeleteData}'), mbConfirmation, MB_YESNO) = IDYES; - end; -end; - -procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); -var - ResultCode: Integer; - DataRoot: String; -begin - if (CurUninstallStep = usPostUninstall) and GDeleteNetxData then - begin - DataRoot := ExpandConstant('{commonappdata}\NetX'); - if FileExists(GTempDeleteDataScript) then - begin - Exec( - 'powershell.exe', - '-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + - GTempDeleteDataScript + '" -DataRoot "' + DataRoot + '"', - ExpandConstant('{tmp}'), - SW_HIDE, - ewWaitUntilTerminated, - ResultCode - ); - end; - if DirExists(DataRoot) then - DelTree(DataRoot, True, True, True); - end; -end; - -procedure CurStepChanged(CurStep: TSetupStep); -var - UninstallKey: String; -begin - if CurStep = ssPostInstall then - begin - ApplyDatabaseConfig(); - - UninstallKey := - 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{A7E3C2D1-9F40-4B8E-9C1A-NETXWIN64001}_is1'; - RegDeleteValue(HKLM, UninstallKey, 'QuietUninstallString'); - end; -end; diff --git a/packaging/installer/test_pg_conn.ps1 b/packaging/installer/test_pg_conn.ps1 deleted file mode 100644 index efc8323..0000000 --- a/packaging/installer/test_pg_conn.ps1 +++ /dev/null @@ -1,104 +0,0 @@ -param( - [Parameter(Mandatory = $true)][string]$PgHost, - [int]$Port = 5432, - [Parameter(Mandatory = $true)][string]$User, - [string]$Password = "", - [string]$PasswordFile = "", - [Parameter(Mandatory = $true)][string]$Database, - [Parameter(Mandatory = $true)][string]$PsqlPath, - [string]$OutUrlFile = "", - [string]$OutErrFile = "" -) - -$ErrorActionPreference = "Stop" - -function Write-ErrFile([string]$Message) { - if ($OutErrFile) { - Set-Content -LiteralPath $OutErrFile -Value $Message -Encoding utf8 - } - [Console]::Error.WriteLine($Message) -} - -if (-not (Test-Path -LiteralPath $PsqlPath)) { - Write-ErrFile "psql_not_found: $PsqlPath" - exit 2 -} -if ($PasswordFile) { - if (-not (Test-Path -LiteralPath $PasswordFile)) { - Write-ErrFile "password_file_missing" - exit 3 - } - $Password = [IO.File]::ReadAllText($PasswordFile).TrimEnd("`r", "`n") -} -if ([string]::IsNullOrWhiteSpace($PgHost)) { - Write-ErrFile "host_required" - exit 3 -} -if ([string]::IsNullOrWhiteSpace($User) -or [string]::IsNullOrWhiteSpace($Database)) { - Write-ErrFile "user_and_database_required" - exit 3 -} -if ([string]::IsNullOrWhiteSpace($Password)) { - Write-ErrFile "password_required" - exit 3 -} -if ($Port -lt 1 -or $Port -gt 65535) { - Write-ErrFile "invalid_port: $Port" - exit 3 -} - -$encUser = [uri]::EscapeDataString($User) -$encPw = [uri]::EscapeDataString($Password) -$encDb = [uri]::EscapeDataString($Database) -$url = "postgresql+psycopg://${encUser}:${encPw}@${PgHost}:${Port}/${encDb}" - -$psqlDir = Split-Path -Parent $PsqlPath -$prevPath = $env:PATH -$prevPw = $env:PGPASSWORD -try { - $env:PATH = "$psqlDir;$env:PATH" - $env:PGPASSWORD = $Password - $psi = New-Object System.Diagnostics.ProcessStartInfo - $psi.FileName = $PsqlPath - $psi.Arguments = "-h `"$PgHost`" -p $Port -U `"$User`" -d `"$Database`" -v ON_ERROR_STOP=1 -t -A -c `"SELECT 1`"" - $psi.UseShellExecute = $false - $psi.RedirectStandardOutput = $true - $psi.RedirectStandardError = $true - $psi.CreateNoWindow = $true - $psi.WorkingDirectory = $psqlDir - $p = [Diagnostics.Process]::Start($psi) - $stdout = $p.StandardOutput.ReadToEnd() - $stderr = $p.StandardError.ReadToEnd() - $p.WaitForExit() - if ($p.ExitCode -ne 0) { - $msg = (($stderr + "`n" + $stdout).Trim()) - if (-not $msg) { $msg = "psql_exit_$($p.ExitCode)" } - Write-ErrFile "connection_failed: $msg" - exit 1 - } - if (($stdout.Trim()) -notmatch '1') { - Write-ErrFile "unexpected_result: $($stdout.Trim())" - exit 1 - } -} catch { - Write-ErrFile ("connection_failed: " + $_.Exception.Message) - exit 1 -} finally { - $env:PATH = $prevPath - if ($null -eq $prevPw) { - Remove-Item Env:PGPASSWORD -ErrorAction SilentlyContinue - } else { - $env:PGPASSWORD = $prevPw - } -} - -if ($OutUrlFile) { - $dir = Split-Path -Parent $OutUrlFile - if ($dir -and -not (Test-Path -LiteralPath $dir)) { - New-Item -ItemType Directory -Path $dir -Force | Out-Null - } - Set-Content -LiteralPath $OutUrlFile -Value $url -Encoding ascii -NoNewline -} - -Write-Output "ok" -exit 0 diff --git a/packaging/launch_shortcut.ps1 b/packaging/launch_shortcut.ps1 index f4ae945..5a43966 100644 --- a/packaging/launch_shortcut.ps1 +++ b/packaging/launch_shortcut.ps1 @@ -7,7 +7,7 @@ ) # Visible entrypoint for Start Menu / desktop shortcuts. -# Nested PowerShell runs are hidden; only failures show a message box. +# Shows a message box on failure (explorer-launched PowerShell otherwise flashes and exits). $ErrorActionPreference = "Stop" . "$PSScriptRoot\_common.ps1" @@ -42,82 +42,63 @@ function Show-NetxError([string]$Msg) { ) } -function Invoke-NetxHiddenPs1 { - param( - [string]$File, - [string[]]$ExtraArgs = @(), - [switch]$NoWait = $false - ) - return Start-NetxPowerShell -File $File ` - -Arguments (@("-ProgramRoot", $prog, "-DataRoot", $data) + $ExtraArgs) ` - -WorkingDirectory $prog -WindowStyle Hidden -PassThru -Wait:(-not $NoWait) -} - function Ensure-NetxEnv { $envPath = Join-Path $data ".env" if (Test-Path $envPath) { return } - $cmd = Join-Path $prog "NetX-FirstRun.cmd" - throw (T ` - "NetX is not configured yet (missing $envPath).`nRe-run Setup and choose built-in or external DB,`nor Start Menu → Reconfigure database:`n$cmd" ` - "尚未完成数据库配置(缺少 $envPath)。`n请重新运行安装向导选择内置/外置库,`n或开始菜单 → 重新配置数据库:`n$cmd") + Write-LaunchLog "missing .env — running setup_first_run -NonInteractive -DbMode bundled" + $setup = Join-Path $PSScriptRoot "setup_first_run.ps1" + & 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.`nDouble-click: $cmd`nOr: Start Menu → All Programs → NetX → First-time setup`nLog: $log" ` + "首次配置失败。`n请双击: $cmd`n或: 开始菜单 → 所有程序 → NetX → First-time setup`n日志: $log") + } } try { Write-LaunchLog "action=$Action prog=$prog data=$data" switch ($Action) { "setup" { - # Visible console so user can pick bundled vs external DB. - $p = Start-NetxPowerShell -File (Join-Path $PSScriptRoot "setup_first_run.ps1") ` - -Arguments @("-ProgramRoot", $prog, "-DataRoot", $data) ` - -WorkingDirectory $prog -WindowStyle Normal -Wait -PassThru - if ($p.ExitCode -ne 0) { throw "setup_exit_$($p.ExitCode)" } - if (Test-Path (Join-Path $data ".env")) { - Start-NetxPowerShell -File (Join-Path $PSScriptRoot "netx_tray.ps1") ` - -Arguments @("-ProgramRoot", $prog, "-DataRoot", $data, "-StartOnLaunch") ` - -WorkingDirectory $prog -WindowStyle Hidden | Out-Null - } + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "setup_first_run.ps1") ` + -ProgramRoot $prog -DataRoot $data + if ($LASTEXITCODE -ne 0) { throw "setup_exit_$LASTEXITCODE" } } "stop" { - $p = Invoke-NetxHiddenPs1 -File (Join-Path $PSScriptRoot "stop_netx_app.ps1") - if ($null -ne $p.ExitCode -and $p.ExitCode -ne 0) { throw "stop_exit_$($p.ExitCode)" } + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "stop_netx_app.ps1") ` + -ProgramRoot $prog -DataRoot $data } "update" { Ensure-NetxEnv - # Update UI may need a visible window for prompts. - $p = Start-NetxPowerShell -File (Join-Path $PSScriptRoot "check_update.ps1") ` - -Arguments @("-ProgramRoot", $prog, "-DataRoot", $data) ` - -WorkingDirectory $prog -WindowStyle Normal -Wait -PassThru - if ($null -ne $p.ExitCode -and $p.ExitCode -ne 0 -and $p.ExitCode -ne 10) { - throw "update_exit_$($p.ExitCode)" - } + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "check_update.ps1") ` + -ProgramRoot $prog -DataRoot $data } "start" { Ensure-NetxEnv - $hostBind = "127.0.0.1" - $port = 8890 - $envPath = Join-Path $data ".env" - 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 {} } - } - $p = Invoke-NetxHiddenPs1 -File (Join-Path $PSScriptRoot "start_netx_app.ps1") -ExtraArgs @("-SkipBrowser") - if ($p.ExitCode -ne 0) { throw "start_exit_$($p.ExitCode)" } - if (Wait-NetxApiHealthy -HostName $hostBind -Port $port -TimeoutSec 180) { - try { Start-Process "http://${hostBind}:${port}/" } catch {} - } else { - throw (T ` - "NetX started but /health not ready within 180s.`nSee: $logDir\netx.err.log" ` - "NetX 已启动但 /health 在 180 秒内未就绪。`n请查看: $logDir\netx.err.log") - } + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "start_netx_app.ps1") ` + -ProgramRoot $prog -DataRoot $data + if ($LASTEXITCODE -ne 0) { throw "start_exit_$LASTEXITCODE" } } "tray" { Ensure-NetxEnv - $extra = @("-ProgramRoot", $prog, "-DataRoot", $data) - if ($StartOnLaunch) { $extra += "-StartOnLaunch" } - # Detach tray; do not leave a success MessageBox (keeps a console open). - Start-NetxPowerShell -File (Join-Path $PSScriptRoot "netx_tray.ps1") ` - -Arguments $extra -WorkingDirectory $prog -WindowStyle Hidden | Out-Null + $trayArgs = @( + "-NoProfile", "-ExecutionPolicy", "Bypass", + "-File", (Join-Path $PSScriptRoot "netx_tray.ps1"), + "-ProgramRoot", $prog, "-DataRoot", $data + ) + if ($StartOnLaunch) { $trayArgs += "-StartOnLaunch" } + # Detach tray; keep this process short so Start Menu returns quickly. + Start-Process -FilePath "powershell.exe" -ArgumentList $trayArgs -WorkingDirectory $prog -WindowStyle Hidden + Start-Sleep -Milliseconds 800 + [void][System.Windows.Forms.MessageBox]::Show( + (T ` + "NetX tray started.`nLook for the NetX icon in the system tray (click ^ if hidden).`nUI: http://127.0.0.1:8890/" ` + "NetX 托盘已启动。`n请在任务栏右下角找 NetX 图标(被藏起来时点 ^)。`n界面: http://127.0.0.1:8890/"), + "NetX", + [System.Windows.Forms.MessageBoxButtons]::OK, + [System.Windows.Forms.MessageBoxIcon]::Information + ) } } Write-LaunchLog "action=$Action ok" diff --git a/packaging/netx_tray.ps1 b/packaging/netx_tray.ps1 index 769352f..e822fa7 100644 --- a/packaging/netx_tray.ps1 +++ b/packaging/netx_tray.ps1 @@ -43,8 +43,8 @@ function T([string]$En, [string]$Zh) { if (-not (Test-Path $envPath)) { [void][System.Windows.Forms.MessageBox]::Show( (T ` - "NetX is not configured yet (missing $envPath).`nRe-run the installer and choose built-in or external DB,`nor use Start Menu → NetX → Reconfigure database." ` - "尚未完成数据库配置(缺少 $envPath)。`n请重新运行安装向导并选择内置或外置数据库,`n或使用「开始菜单 → NetX → 重新配置数据库」。"), + "First-time setup incomplete (missing $envPath).`nRun Start Menu → NetX → First-time setup." ` + "尚未完成首次配置(缺少 $envPath)。`n请运行「开始菜单 → NetX → First-time setup」。"), "NetX", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Warning @@ -52,57 +52,17 @@ if (-not (Test-Path $envPath)) { exit 1 } -# Only one tray icon per machine session (desktop + postinstall + autostart can race). -$script:netxTrayMutex = $null -try { - $createdNew = $false - $script:netxTrayMutex = New-Object System.Threading.Mutex($true, "Local\NetX.WinTray.SingleInstance", [ref]$createdNew) - if (-not $createdNew) { - if (Test-NetxApiHealthy -HostName $hostBind -Port $port -TimeoutSec 2) { - try { Start-Process $uiUrl } catch {} - } elseif ($StartOnLaunch) { - Start-NetxPowerShell -File (Join-Path $PSScriptRoot "start_netx_app.ps1") ` - -Arguments @("-ProgramRoot", $prog, "-DataRoot", $data, "-SkipBrowser") ` - -WorkingDirectory $prog -WindowStyle Hidden | Out-Null - } - exit 0 - } -} catch { - # If mutex fails, continue with a tray (better than no UI control). -} - $startPs1 = Join-Path $PSScriptRoot "start_netx_app.ps1" $stopPs1 = Join-Path $PSScriptRoot "stop_netx_app.ps1" $checkPs1 = Join-Path $PSScriptRoot "check_update.ps1" function Invoke-NetxScript { - param( - [string]$File, - [string[]]$ExtraArgs = @(), - [switch]$Wait = $false - ) - return Start-NetxPowerShell -File $File -Arguments (@("-ProgramRoot", $prog, "-DataRoot", $data) + $ExtraArgs) ` - -WorkingDirectory $prog -WindowStyle Hidden -PassThru -Wait:$Wait -} - -function Open-NetxUiWhenReady { - param([int]$TimeoutSec = 180) - $notify.ShowBalloonTip( - 5000, - "NetX", - (T "Starting… first run may take a minute." "正在启动…首次迁移可能需要一分钟。"), - [System.Windows.Forms.ToolTipIcon]::Info - ) - if (Wait-NetxApiHealthy -HostName $hostBind -Port $port -TimeoutSec $TimeoutSec) { - try { Start-Process $uiUrl } catch {} - return - } - $notify.ShowBalloonTip( - 8000, - "NetX", - (T "UI not ready yet. Use tray → Open UI when the icon appears, or check data\runtime\netx.err.log." "界面尚未就绪。就绪后请用托盘「打开界面」,或查看 data\runtime\netx.err.log。"), - [System.Windows.Forms.ToolTipIcon]::Warning - ) + param([string]$File, [string[]]$ExtraArgs = @()) + $args = @( + "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $File, + "-ProgramRoot", $prog, "-DataRoot", $data + ) + $ExtraArgs + Start-Process -FilePath "powershell.exe" -ArgumentList $args -WorkingDirectory $prog -WindowStyle Hidden } $form = New-Object System.Windows.Forms.Form @@ -133,30 +93,23 @@ $miOpen.add_Click({ Start-Process $uiUrl }) $miStart = $menu.Items.Add((T "Start NetX" "启动 NetX")) $miStart.add_Click({ - Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser") | Out-Null - Open-NetxUiWhenReady + $notify.ShowBalloonTip(3000, "NetX", (T "Starting…" "正在启动…"), [System.Windows.Forms.ToolTipIcon]::Info) + Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser") }) -$miStop = $menu.Items.Add((T "Stop NetX (keep tray)" "停止 NetX(保留托盘)")) +$miStop = $menu.Items.Add((T "Stop NetX" "停止 NetX")) $miStop.add_Click({ $notify.ShowBalloonTip(3000, "NetX", (T "Stopping…" "正在停止…"), [System.Windows.Forms.ToolTipIcon]::Info) - Invoke-NetxScript -File $stopPs1 | Out-Null -}) - -$miStopExit = $menu.Items.Add((T "Stop NetX and exit tray" "停止 NetX 并退出托盘")) -$miStopExit.add_Click({ - $notify.ShowBalloonTip(3000, "NetX", (T "Stopping…" "正在停止…"), [System.Windows.Forms.ToolTipIcon]::Info) - Invoke-NetxScript -File $stopPs1 -Wait | Out-Null - $notify.Visible = $false - $form.Close() - [System.Windows.Forms.Application]::Exit() + Invoke-NetxScript -File $stopPs1 }) $miUpdate = $menu.Items.Add((T "Check for updates" "检查更新")) $miUpdate.add_Click({ try { - $p = Start-NetxPowerShell -File $checkPs1 -Arguments @("-ProgramRoot", $prog, "-DataRoot", $data) ` - -WorkingDirectory $prog -WindowStyle Normal -Wait -PassThru + $p = Start-Process -FilePath "powershell.exe" -ArgumentList @( + "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $checkPs1, + "-ProgramRoot", $prog, "-DataRoot", $data + ) -Wait -PassThru -WindowStyle Normal if ($p.ExitCode -eq 10) { $ans = [System.Windows.Forms.MessageBox]::Show( (T "A newer NetX is available. Download and apply now?" "发现新版本,是否立即下载并更新?"), @@ -165,8 +118,10 @@ $miUpdate.add_Click({ [System.Windows.Forms.MessageBoxIcon]::Question ) if ($ans -eq [System.Windows.Forms.DialogResult]::Yes) { - Start-NetxPowerShell -File $checkPs1 -Arguments @("-ProgramRoot", $prog, "-DataRoot", $data, "-Apply") ` - -WorkingDirectory $prog -WindowStyle Normal | Out-Null + Start-Process -FilePath "powershell.exe" -ArgumentList @( + "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $checkPs1, + "-ProgramRoot", $prog, "-DataRoot", $data, "-Apply" + ) -WorkingDirectory $prog } } elseif ($p.ExitCode -eq 0) { [System.Windows.Forms.MessageBox]::Show((T "NetX is up to date ($ver)." "已是最新版本 ($ver)。"), (T "NetX update" "NetX 更新")) @@ -191,49 +146,20 @@ $notify.add_DoubleClick({ Start-Process $uiUrl }) $form.add_Shown({ if ($AutoUpdate) { $notify.ShowBalloonTip(4000, "NetX", (T "Checking for updates…" "正在检查更新…"), [System.Windows.Forms.ToolTipIcon]::Info) - Start-NetxPowerShell -File $checkPs1 ` - -Arguments @("-ProgramRoot", $prog, "-DataRoot", $data, "-Apply", "-Quiet", "-AutoOnly") ` - -WorkingDirectory $prog -WindowStyle Hidden -Wait | Out-Null + Start-Process -FilePath "powershell.exe" -ArgumentList @( + "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $checkPs1, + "-ProgramRoot", $prog, "-DataRoot", $data, "-Apply", "-Quiet", "-AutoOnly" + ) -Wait -WindowStyle Hidden } elseif ($CheckUpdateOnLaunch) { - Start-NetxPowerShell -File $checkPs1 ` - -Arguments @("-ProgramRoot", $prog, "-DataRoot", $data, "-Quiet") ` - -WorkingDirectory $prog -WindowStyle Hidden | Out-Null + Start-Process -FilePath "powershell.exe" -ArgumentList @( + "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $checkPs1, + "-ProgramRoot", $prog, "-DataRoot", $data, "-Quiet" + ) -WindowStyle Hidden } if ($StartOnLaunch) { - # Detach start so the tray message pump stays alive; poll /health before browser. - Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser") | Out-Null - $script:netxUiWaitTicks = 0 - $script:netxUiWaitMax = 360 # 360 * 500ms = 180s - $script:netxUiTimer = New-Object System.Windows.Forms.Timer - $script:netxUiTimer.Interval = 500 - $script:netxUiTimer.add_Tick({ - $script:netxUiWaitTicks++ - if (Test-NetxApiHealthy -HostName $hostBind -Port $port -TimeoutSec 2) { - $script:netxUiTimer.Stop() - $script:netxUiTimer.Dispose() - try { Start-Process $uiUrl } catch {} - return - } - if ($script:netxUiWaitTicks -eq 1) { - $notify.ShowBalloonTip( - 5000, - "NetX", - (T "Starting… first run may take a minute." "正在启动…首次迁移可能需要一分钟。"), - [System.Windows.Forms.ToolTipIcon]::Info - ) - } - if ($script:netxUiWaitTicks -ge $script:netxUiWaitMax) { - $script:netxUiTimer.Stop() - $script:netxUiTimer.Dispose() - $notify.ShowBalloonTip( - 8000, - "NetX", - (T "UI not ready yet. Use tray → Open UI later, or check data\runtime\netx.err.log." "界面尚未就绪。请稍后用托盘「打开界面」,或查看 data\runtime\netx.err.log。"), - [System.Windows.Forms.ToolTipIcon]::Warning - ) - } - }) - $script:netxUiTimer.Start() + Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser") + Start-Sleep -Seconds 2 + try { Start-Process $uiUrl } catch {} } }) diff --git a/packaging/setup_first_run.ps1 b/packaging/setup_first_run.ps1 index c865723..6a73e65 100644 --- a/packaging/setup_first_run.ps1 +++ b/packaging/setup_first_run.ps1 @@ -4,13 +4,9 @@ [string]$ProgramRoot = "", [string]$DataRoot = "", [string]$ExternalDatabaseUrl = "", - [string]$ExternalDatabaseUrlFile = "", - [string]$CredentialSecretKey = "", - [string]$CredentialSecretKeyFile = "", [string]$BundledPassword = "", [int]$BundledPort = 15432, - [switch]$NonInteractive = $false, - [switch]$SkipExternalProbe = $false + [switch]$NonInteractive = $false ) $ErrorActionPreference = "Stop" @@ -43,10 +39,9 @@ if (-not $DbMode) { } } else { Write-Host "" - Write-Host (T "Choose database mode:" "选择数据库模式:") -ForegroundColor Cyan - Write-Host (T " 1) bundled — NetX portable PostgreSQL (offline / default)" " 1) bundled — NetX 内置便携 PostgreSQL(可离线,默认)") - Write-Host (T " 2) external — existing PostgreSQL (you provide connection URL)" " 2) external — 已有外置 PostgreSQL(需填写连接串)") - Write-Host "" + Write-Host (T "Choose database mode:" "选择数据库模式:") + Write-Host (T " 1) bundled — use NetX portable PostgreSQL (default for new installs)" " 1) bundled — 使用 NetX 内置便携 PostgreSQL(新安装默认)") + Write-Host (T " 2) external — connect to an existing PostgreSQL (Linux / already deployed)" " 2) external — 连接已有 PostgreSQL") $choice = Read-Host (T "Enter 1 or 2" "请输入 1 或 2") if ($choice -eq "2") { $DbMode = "external" } else { $DbMode = "bundled" } } @@ -72,35 +67,6 @@ if (Test-Path (Join-Path $distAbs "index.html")) { $dataEnv = Get-NetxDataEnvMap -DataRoot $data foreach ($k in $dataEnv.Keys) { $values[$k] = $dataEnv[$k] } -# Fernet key for managed-NE password encryption (required to save SSH passwords). -# Priority: -CredentialSecretKey(File) > interactive prompt > existing .env > generate. -if ($CredentialSecretKeyFile) { - if (-not (Test-Path -LiteralPath $CredentialSecretKeyFile)) { - throw "credential_secret_key_file_missing: $CredentialSecretKeyFile" - } - $CredentialSecretKey = [IO.File]::ReadAllText($CredentialSecretKeyFile).Trim() -} -if (-not $CredentialSecretKey -and -not $NonInteractive) { - Write-Host "" - Write-Host (T ` - "Optional: paste NETX_CREDENTIAL_SECRET_KEY from an existing install" ` - "可选: 粘贴已有安装的 NETX_CREDENTIAL_SECRET_KEY(便于沿用已加密凭据)") -ForegroundColor Cyan - Write-Host (T ` - "Leave empty to keep existing .env key, or auto-generate if none." ` - "留空则保留已有 .env 中的密钥;若没有则自动生成。") - $CredentialSecretKey = (Read-Host "NETX_CREDENTIAL_SECRET_KEY").Trim() -} -if ($CredentialSecretKey) { - $values["NETX_CREDENTIAL_SECRET_KEY"] = $CredentialSecretKey.Trim() - Write-Host "==> Using provided NETX_CREDENTIAL_SECRET_KEY" -ForegroundColor Green -} elseif ($existing.ContainsKey("NETX_CREDENTIAL_SECRET_KEY") -and $existing["NETX_CREDENTIAL_SECRET_KEY"]) { - $values["NETX_CREDENTIAL_SECRET_KEY"] = $existing["NETX_CREDENTIAL_SECRET_KEY"] - Write-Host "==> Keeping existing NETX_CREDENTIAL_SECRET_KEY from .env" -ForegroundColor Green -} else { - $values["NETX_CREDENTIAL_SECRET_KEY"] = New-NetxFernetKey - Write-Host "==> Generated NETX_CREDENTIAL_SECRET_KEY (saved in .env)" -ForegroundColor Green -} - if ($DbMode -eq "bundled") { $pgsql = Join-Path $prog "postgres\pgsql" if (-not (Test-Path (Join-Path $pgsql "bin\initdb.exe"))) { @@ -223,107 +189,23 @@ if ($DbMode -eq "bundled") { } Write-Host "==> Bundled Postgres ready on 127.0.0.1:$BundledPort" -ForegroundColor Green } else { - if ($ExternalDatabaseUrlFile) { - if (-not (Test-Path -LiteralPath $ExternalDatabaseUrlFile)) { - throw "external_url_file_missing: $ExternalDatabaseUrlFile" - } - $ExternalDatabaseUrl = [IO.File]::ReadAllText($ExternalDatabaseUrlFile).Trim() - } if (-not $ExternalDatabaseUrl) { if ($existing.ContainsKey("NETX_DATABASE_URL") -and $existing["NETX_DATABASE_URL"]) { $ExternalDatabaseUrl = $existing["NETX_DATABASE_URL"] } elseif ($NonInteractive) { throw "external_url_required" } else { - Write-Host "" - Write-Host (T ` - "Enter PostgreSQL URL (database/role must already exist):" ` - "请输入 PostgreSQL 连接串(库和用户需事先建好):") -ForegroundColor Cyan - Write-Host " postgresql+psycopg://USER:PASSWORD@HOST:5432/DBNAME" - Write-Host (T ` - "Example: postgresql+psycopg://netx:secret@10.0.0.8:5432/netx" ` - "示例: postgresql+psycopg://netx:secret@10.0.0.8:5432/netx") - $ExternalDatabaseUrl = Read-Host "NETX_DATABASE_URL" + $ExternalDatabaseUrl = Read-Host "NETX_DATABASE_URL (postgresql+psycopg://user:pass@host:5432/netx)" } } if (-not $ExternalDatabaseUrl) { throw "external_url_required" } - $ExternalDatabaseUrl = $ExternalDatabaseUrl.Trim() $values["NETX_DATABASE_URL"] = $ExternalDatabaseUrl - Write-Host "==> External Postgres configured" -ForegroundColor Green - - if (-not $SkipExternalProbe) { - # Probe with bundled psql when available (wizard already tested; this covers CLI reconfigure). - $psqlProbe = Join-Path $prog "postgres\pgsql\bin\psql.exe" - $testHelper = Join-Path $PSScriptRoot "installer\test_pg_conn.ps1" - if (-not (Test-Path $testHelper)) { - $testHelper = Join-Path $PSScriptRoot "test_pg_conn.ps1" - } - if ((Test-Path $psqlProbe) -and ($ExternalDatabaseUrl -match '^(?:postgresql(?:\+psycopg)?|postgres)://([^:]+):([^@]*)@([^:/]+):?(\d+)?/([^?\s]+)')) { - $u = [uri]::UnescapeDataString($Matches[1]) - $pw = [uri]::UnescapeDataString($Matches[2]) - $h = $Matches[3] - $po = if ($Matches[4]) { [int]$Matches[4] } else { 5432 } - $dbn = [uri]::UnescapeDataString($Matches[5]) - if (Test-Path $testHelper) { - Write-Host "==> Probing external PostgreSQL..." - & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $testHelper ` - -PgHost $h -Port $po -User $u -Password $pw -Database $dbn -PsqlPath $psqlProbe - if ($LASTEXITCODE -ne 0) { - throw "external_db_probe_failed" - } - Write-Host "==> External PostgreSQL OK" -ForegroundColor Green - } else { - $env:PGPASSWORD = $pw - try { - $ping = & $psqlProbe -h $h -p $po -U $u -d $dbn -t -A -c "SELECT 1" 2>&1 - if ($LASTEXITCODE -ne 0 -or (($ping | Out-String).Trim()) -notmatch '1') { - throw "external_db_probe_failed: $ping" - } - } finally { - Remove-Item Env:PGPASSWORD -ErrorAction SilentlyContinue - } - } - } - } + Write-Host "==> External Postgres configured (ensure role/db exist; see scripts\init_pg.ps1)" -ForegroundColor Green } Write-DotEnvValue -Path $envPath -Values $values Write-Host "" Write-Host "Wrote $envPath" -ForegroundColor Green - -# Official Setup ships python/runtime + .venv (build_release -CreateVenv). -$venvPy = Join-Path $prog ".venv\Scripts\python.exe" -try { - $null = Ensure-NetxVenv -ProgramRoot $prog - Write-Host "==> Bundled Python venv OK: $venvPy" -ForegroundColor Green -} catch { - if (Test-Path $venvPy) { - throw - } - Write-Host "==> Bundled .venv missing — creating one (Setup should normally ship it)." -ForegroundColor Yellow - Write-Host "[WARN] $($_.Exception.Message)" -ForegroundColor Yellow - Write-Host " Install a Setup built with: packaging\build_release.ps1 -CreateVenv" -ForegroundColor Yellow -} - -Write-Host "" -Write-Host (T "Setup complete." "首次配置完成。") -ForegroundColor Green -if (-not $NonInteractive) { - Write-Host (T ` - "Next: Start Menu → NetX Tray (or press Y below)." ` - "下一步: 开始菜单 → NetX 托盘 (或在下方按 Y 立即启动)。") - $ans = Read-Host (T "Start NetX tray now? [Y/n]" "现在启动 NetX 托盘?[Y/n]") - if ($ans -notmatch '^[Nn]') { - try { - Start-NetxPowerShell -File (Join-Path $PSScriptRoot "netx_tray.ps1") ` - -Arguments @("-ProgramRoot", $prog, "-DataRoot", $data, "-StartOnLaunch") ` - -WorkingDirectory $prog -WindowStyle Hidden | Out-Null - Write-Host (T "Tray started." "托盘已启动。") -ForegroundColor Green - } catch { - Write-Host "[WARN] $($_.Exception.Message)" -ForegroundColor Yellow - } - } -} else { - Write-Host "Next: .\packaging\start_netx_app.ps1 or NetX-Tray.cmd" -} +Write-Host "Next: .\packaging\start_netx_app.ps1" diff --git a/packaging/start_netx_app.ps1 b/packaging/start_netx_app.ps1 index e56c792..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, @@ -96,13 +96,15 @@ if ($mode -eq "bundled") { } } -try { - $null = Ensure-NetxVenv -ProgramRoot $prog -} catch { - Write-Host "[ERR] $($_.Exception.Message)" -ForegroundColor Red - Write-Host " Tip: run Start Menu → NetX → First-time setup once as Administrator," -ForegroundColor Yellow - Write-Host " or install a Setup built with packaging\\build_release.ps1 -CreateVenv." -ForegroundColor Yellow - exit 1 +$venvPy = Join-Path $prog ".venv\Scripts\python.exe" +if (-not (Test-Path $venvPy)) { + Write-Host "==> Creating .venv (one-time)" + $pyCmd = Get-Command python -ErrorAction SilentlyContinue + if (-not $pyCmd) { throw "python_not_found: install Python 3.11+ and re-run" } + & $pyCmd.Source -m venv (Join-Path $prog ".venv") + & $venvPy -m pip install --upgrade pip + & $venvPy -m pip install -r (Join-Path $prog "requirements.txt") + if ($LASTEXITCODE -ne 0) { throw "pip_install_failed" } } if (-not (Test-NetxTcpPortFree -HostName $hostBind -Port $port)) { @@ -118,22 +120,18 @@ if (-not (Test-Path $startScript)) { throw "start_netx.ps1 not found at $startScript" } -$startExtra = @( +$psArgs = @( + "-ExecutionPolicy", "Bypass", "-File", $startScript, "-SkipInstall", "-Background", "-BindHost", $hostBind, "-Port", "$port" ) -if ($InlineSchedulers) { $startExtra += "-InlineSchedulers" } +if ($InlineSchedulers) { $psArgs += "-InlineSchedulers" } Write-Host "==> Starting NetX (API + workers; UI via API on :$port)" -$startProc = Start-NetxPowerShell -File $startScript -Arguments $startExtra ` - -WorkingDirectory $prog -WindowStyle Hidden -Wait -PassThru -if ($startProc.ExitCode -ne 0) { exit $startProc.ExitCode } +& powershell @psArgs +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $url = "http://${hostBind}:${port}/" -if (-not (Wait-NetxApiHealthy -HostName $hostBind -Port $port -TimeoutSec 30)) { - Write-Host "[ERR] NetX process exited 0 but /health not ready: $url" -ForegroundColor Red - exit 1 -} Write-Host "==> Open: $url" -ForegroundColor Green if (-not $SkipBrowser) { try { Start-Process $url } catch {} diff --git a/packaging/stop_netx_app.ps1 b/packaging/stop_netx_app.ps1 index 5da0525..4d04f19 100644 --- a/packaging/stop_netx_app.ps1 +++ b/packaging/stop_netx_app.ps1 @@ -1,8 +1,7 @@ -param( +param( [string]$ProgramRoot = "", [string]$DataRoot = "", - [switch]$KeepPostgres = $false, - [switch]$KillTray = $false + [switch]$KeepPostgres = $false ) $ErrorActionPreference = "Continue" @@ -22,17 +21,11 @@ if (-not (Test-Path $stopScript)) { } if (Test-Path $stopScript) { Write-Host "==> Stopping NetX processes" - Start-NetxPowerShell -File $stopScript -Arguments @("-Force") ` - -WorkingDirectory $prog -WindowStyle Hidden -Wait | Out-Null + & powershell -ExecutionPolicy Bypass -File $stopScript -Force } else { Write-Host "[WARN] stop_netx.ps1 not found" } -if ($KillTray) { - Write-Host "==> Stopping NetX tray icons" - Stop-NetxTrayProcesses -ProgramRoot $prog -} - $mode = Get-DbMode -EnvMap $map if ($mode -eq "bundled" -and -not $KeepPostgres) { $pgBinCandidates = @( diff --git a/packaging/uninstall_delete_data.ps1 b/packaging/uninstall_delete_data.ps1 deleted file mode 100644 index 707d0cd..0000000 --- a/packaging/uninstall_delete_data.ps1 +++ /dev/null @@ -1,53 +0,0 @@ -param( - [string]$DataRoot = "" -) - -# Post-uninstall optional data wipe with retries (handles briefly locked runtime logs). - -$ErrorActionPreference = "Continue" -if (-not $DataRoot) { - $DataRoot = Join-Path ([Environment]::GetFolderPath("CommonApplicationData")) "NetX" -} - -if (-not (Test-Path -LiteralPath $DataRoot)) { - Write-Host "==> Data root already gone: $DataRoot" - exit 0 -} - -Write-Host "==> Deleting data root: $DataRoot" - -# Kill anything still holding files under data root. -Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object { - $_.CommandLine -and ($_.CommandLine.IndexOf($DataRoot, [StringComparison]::OrdinalIgnoreCase) -ge 0) -} | ForEach-Object { - try { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } catch {} - try { & taskkill.exe /F /PID $_.ProcessId 2>$null | Out-Null } catch {} -} - -$ok = $false -for ($i = 1; $i -le 5; $i++) { - try { - cmd /c "rmdir /s /q `"$DataRoot`"" - } catch {} - if (-not (Test-Path -LiteralPath $DataRoot)) { - $ok = $true - break - } - Start-Sleep -Seconds 1 -} - -if (-not $ok -and (Test-Path -LiteralPath $DataRoot)) { - try { - takeown /F $DataRoot /R /D Y | Out-Null - icacls $DataRoot /grant Administrators:F /T | Out-Null - cmd /c "rmdir /s /q `"$DataRoot`"" - } catch {} -} - -if (Test-Path -LiteralPath $DataRoot) { - Write-Host "[WARN] Could not fully delete $DataRoot" - exit 1 -} - -Write-Host "==> Data root deleted" -exit 0 diff --git a/packaging/uninstall_prepare.ps1 b/packaging/uninstall_prepare.ps1 deleted file mode 100644 index 05e581b..0000000 --- a/packaging/uninstall_prepare.ps1 +++ /dev/null @@ -1,71 +0,0 @@ -param( - [string]$ProgramRoot = "", - [string]$DataRoot = "" -) - -# Fast, non-blocking uninstall prep for Inno [UninstallRun]. -# Must return within a few seconds and never kill unins000.exe / _unins.tmp. - -$ErrorActionPreference = "Continue" - -$prog = if ($ProgramRoot) { $ProgramRoot } else { "C:\Program Files\NetX" } -$data = if ($DataRoot) { $DataRoot } else { Join-Path $env:ProgramData "NetX" } -$prog = ($prog -replace '/', '\').TrimEnd('\') -$data = ($data -replace '/', '\').TrimEnd('\') - -function Test-Protected([string]$Name, [string]$Cmd) { - if ($Name -match '^(unins\d*|_unins\.tmp|setup)\.exe$') { return $true } - if ($Cmd -match 'unins\d*\.exe|_unins\.tmp|uninstall_prepare\.ps1|uninstall_delete_data\.ps1') { return $true } - return $false -} - -function Stop-Pid([int]$Id, [string]$Label) { - if ($Id -le 4 -or $Id -eq $PID) { return } - Write-Host "stop $Id $Label" - try { Stop-Process -Id $Id -Force -ErrorAction SilentlyContinue } catch {} -} - -Write-Host "==> uninstall_prepare fast-stop prog=$prog" - -$patterns = @( - 'netx_tray\.ps1', - 'netx_api\.(main|worker|biz_state_worker)', - 'start_netx_app\.ps1', - 'stop_netx_app\.ps1', - 'launch_shortcut\.ps1', - [regex]::Escape((Join-Path $prog '.venv\Scripts\python.exe')), - [regex]::Escape((Join-Path $prog 'postgres\pgsql\bin')) -) - -Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | ForEach-Object { - $cl = [string]$_.CommandLine - $name = [string]$_.Name - if (-not $cl) { return } - if (Test-Protected -Name $name -Cmd $cl) { return } - foreach ($pat in $patterns) { - if ($cl -match $pat) { - Stop-Pid -Id ([int]$_.ProcessId) -Label $name - break - } - } -} - -# Best-effort postgres stop (no hang: immediate + ignore) -$pgCtl = Join-Path $prog "postgres\pgsql\bin\pg_ctl.exe" -$pgData = Join-Path $data "pgdata" -if ((Test-Path $pgCtl) -and (Test-Path $pgData)) { - try { - $p = Start-Process -FilePath $pgCtl -ArgumentList @('-D', $pgData, 'stop', '-m', 'immediate') ` - -WindowStyle Hidden -PassThru - if (-not $p.WaitForExit(5000)) { - try { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue } catch {} - } - } catch {} -} - -try { - Remove-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" -Name "NetX" -ErrorAction SilentlyContinue -} catch {} - -Write-Host "==> uninstall_prepare done" -exit 0 diff --git a/pyproject.toml b/pyproject.toml index 3e59513..8c54492 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "netx-ops" -version = "0.4.6" +version = "0.4.5" description = "netx operations tool: alarm-centric workflows with REST API and stdio MCP" readme = "README.md" requires-python = ">=3.11" diff --git a/scripts/start_netx.ps1 b/scripts/start_netx.ps1 index d1ba8ea..7053271 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, @@ -23,14 +23,6 @@ if ($Backgtound -and -not $Background) { $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 @@ -54,17 +46,13 @@ $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)) { +$venvPython = Join-Path $projectRoot ".venv\\Scripts\\python.exe" +if (-not (Test-Path $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" - } +} +if (-not (Test-Path $venvPython)) { + throw "failed_to_create_venv" } $pythonExe = $venvPython @@ -272,11 +260,7 @@ if ($Background) { 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 {} - } + $healthWaitSec = 45 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