mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 03:10:46 +08:00
Release 0.4.5: installer DB wizard, credential key, lean offline Setup.
Choose bundled or external PostgreSQL in the Setup wizard with connection validation; optional NETX_CREDENTIAL_SECRET_KEY; single desktop shortcut; faster uninstall and tray startup for offline Windows packages. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
86dfd41f44
commit
97393cd407
20 changed files with 1531 additions and 146 deletions
|
|
@ -15,6 +15,7 @@ from .schema_patches import (
|
||||||
apply_topology_schema_safety_net,
|
apply_topology_schema_safety_net,
|
||||||
run_alembic_upgrade_to_head,
|
run_alembic_upgrade_to_head,
|
||||||
)
|
)
|
||||||
|
from .ne_crypto import ensure_credential_secret_key
|
||||||
from .security_bootstrap import assert_secure_defaults_or_exit
|
from .security_bootstrap import assert_secure_defaults_or_exit
|
||||||
from .ume_runtime import start_api_sideband_threads, start_device_schedulers
|
from .ume_runtime import start_api_sideband_threads, start_device_schedulers
|
||||||
import netx_api.ume_support as ume_support
|
import netx_api.ume_support as ume_support
|
||||||
|
|
@ -42,6 +43,8 @@ def _configure_ume_diag_logging() -> None:
|
||||||
def run_api_startup() -> None:
|
def run_api_startup() -> None:
|
||||||
"""Full API boot sequence previously inlined in ``main.on_startup``."""
|
"""Full API boot sequence previously inlined in ``main.on_startup``."""
|
||||||
assert_secure_defaults_or_exit()
|
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()
|
_configure_ume_diag_logging()
|
||||||
_log.info(
|
_log.info(
|
||||||
"startup: ne_exec_policy_enabled=%s",
|
"startup: ne_exec_policy_enabled=%s",
|
||||||
|
|
|
||||||
|
|
@ -80,8 +80,9 @@ class Settings(BaseSettings):
|
||||||
ume_topo_nodes_path: str = "/restconf/data/zte-resources-module:TopoNodes"
|
ume_topo_nodes_path: str = "/restconf/data/zte-resources-module:TopoNodes"
|
||||||
ume_topological_links_path: str = "/restconf/data/zte-resources-module:TopologicalLinks"
|
ume_topological_links_path: str = "/restconf/data/zte-resources-module:TopologicalLinks"
|
||||||
ume_sync_alarms_history_every_hours: int = 24
|
ume_sync_alarms_history_every_hours: int = 24
|
||||||
# Managed NE credentials (Fernet key; generate with cryptography.fernet.Fernet.generate_key())
|
# Managed NE credentials (Fernet). Empty = auto-generate & persist to credential_secret_file.
|
||||||
credential_secret_key: str = ""
|
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).
|
# Shared-server worker caps (sized for multi-operator use; raise if bastion/DB allow).
|
||||||
ne_connect_max_workers: int = 8
|
ne_connect_max_workers: int = 8
|
||||||
ne_connect_timeout_sec: int = 30
|
ne_connect_timeout_sec: int = 30
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,73 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from cryptography.fernet import Fernet, InvalidToken
|
from cryptography.fernet import Fernet, InvalidToken
|
||||||
|
|
||||||
from .config import settings
|
from .config import settings
|
||||||
|
|
||||||
|
_log = logging.getLogger("netx.crypto")
|
||||||
|
_cached_credential_key: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class CredentialCryptoError(RuntimeError):
|
class CredentialCryptoError(RuntimeError):
|
||||||
pass
|
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:
|
def _fernet() -> Fernet:
|
||||||
key = str(settings.credential_secret_key or "").strip()
|
key = ensure_credential_secret_key()
|
||||||
if not key:
|
|
||||||
raise CredentialCryptoError("credential_secret_key_not_configured")
|
|
||||||
try:
|
try:
|
||||||
return Fernet(key.encode("ascii"))
|
return Fernet(key.encode("ascii"))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|
@ -37,4 +92,7 @@ def decrypt_secret(value: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def credentials_configured() -> bool:
|
def credentials_configured() -> bool:
|
||||||
return bool(str(settings.credential_secret_key or "").strip())
|
try:
|
||||||
|
return bool(ensure_credential_secret_key())
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ This directory builds a Windows deliverable with:
|
||||||
|
|
||||||
**Installer:** English + 简体中文 (language dialog). Icons use `packaging/assets/netx.ico`.
|
**Installer:** English + 简体中文 (language dialog). Icons use `packaging/assets/netx.ico`.
|
||||||
|
|
||||||
**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.
|
**Offline:** Setup ships portable PostgreSQL, `.venv` (when built with `-CreateVenv`), 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.
|
||||||
|
|
||||||
**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.
|
**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,10 +54,14 @@ Override version in the `.iss` or edit `#define MyAppVersion`.
|
||||||
### Setup.exe
|
### Setup.exe
|
||||||
|
|
||||||
1. Run `NetX-Setup-x.y.z.exe` (admin).
|
1. Run `NetX-Setup-x.y.z.exe` (admin).
|
||||||
2. Files → `%ProgramFiles%\NetX\` (program root).
|
2. On the **Database** page: choose built-in (offline) or external PostgreSQL (host/port/user/password/db; connection must succeed to continue).
|
||||||
3. Data → `%ProgramData%\NetX\` (`.env`, `pgdata`, spool, secrets).
|
3. Files → `%ProgramFiles%\NetX\` (program root).
|
||||||
4. Optional post-install task runs `setup_first_run.ps1` (choose bundled vs external DB).
|
4. Data → `%ProgramData%\NetX\` (`.env`, `pgdata`, spool, secrets). Post-install writes `.env` automatically.
|
||||||
5. Start menu: **Start NetX** / **Stop NetX** / **Open NetX UI**.
|
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.
|
||||||
|
|
||||||
### Zip (portable)
|
### Zip (portable)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# Shared path helpers for Windows packaging scripts.
|
# Shared path helpers for Windows packaging scripts.
|
||||||
# Dot-source: . "$PSScriptRoot\_common.ps1"
|
# Dot-source: . "$PSScriptRoot\_common.ps1"
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
@ -147,6 +147,7 @@ function Get-NetxDataEnvMap {
|
||||||
return @{
|
return @{
|
||||||
"NETX_AUTH_MCP_TOKEN_FILE" = (ConvertTo-NetxFsPath (Join-Path $d "data\auth\mcp_token"))
|
"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_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_SCHEDULER_HEARTBEAT_PATH" = (ConvertTo-NetxFsPath (Join-Path $d "data\runtime\scheduler_heartbeat.json"))
|
||||||
"NETX_RUN_DIR" = (ConvertTo-NetxFsPath (Join-Path $d "data\runtime"))
|
"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_BIZ_STATE_SPOOL_DIR" = (ConvertTo-NetxFsPath (Join-Path $d "data\biz_state_spool"))
|
||||||
|
|
@ -156,6 +157,18 @@ 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 {
|
function Ensure-NetxDataDirectories {
|
||||||
param([string]$DataRoot)
|
param([string]$DataRoot)
|
||||||
$subs = @(
|
$subs = @(
|
||||||
|
|
@ -201,6 +214,146 @@ 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", "py")) {
|
||||||
|
$c = Get-Command $cmd -ErrorAction SilentlyContinue
|
||||||
|
if ($c -and $c.Source -and ($c.Source -notmatch '\\WindowsApps\\')) {
|
||||||
|
$candidates += $c.Source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 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"
|
||||||
|
if ((Test-Path $venvPy) -and -not $ForcePip) {
|
||||||
|
return $venvPy
|
||||||
|
}
|
||||||
|
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) {
|
function ConvertTo-NetxSqlLiteral([string]$Value) {
|
||||||
# Single-quote escaping for PostgreSQL string literals.
|
# Single-quote escaping for PostgreSQL string literals.
|
||||||
return ($Value -replace "'", "''")
|
return ($Value -replace "'", "''")
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
param(
|
param(
|
||||||
[string]$Version = "",
|
[string]$Version = "",
|
||||||
[string]$OutDir = "",
|
[string]$OutDir = "",
|
||||||
[switch]$SkipWebBuild = $false,
|
[switch]$SkipWebBuild = $false,
|
||||||
[switch]$SkipPostgresDownload = $false,
|
[switch]$SkipPostgresDownload = $false,
|
||||||
[switch]$SkipZip = $false,
|
[switch]$SkipZip = $false,
|
||||||
[switch]$CreateVenv = $false
|
# Default on: offline Setup must ship .venv so target PCs never pip-install.
|
||||||
|
[switch]$CreateVenv = $true
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
@ -103,7 +104,7 @@ foreach ($name in @(
|
||||||
"download_postgres.ps1", "setup_first_run.ps1", "start_netx_app.ps1",
|
"download_postgres.ps1", "setup_first_run.ps1", "start_netx_app.ps1",
|
||||||
"stop_netx_app.ps1", "update_netx.ps1", "check_update.ps1",
|
"stop_netx_app.ps1", "update_netx.ps1", "check_update.ps1",
|
||||||
"launch_shortcut.ps1", "netx_tray.ps1", "install_autostart.ps1", "install_service.ps1",
|
"launch_shortcut.ps1", "netx_tray.ps1", "install_autostart.ps1", "install_service.ps1",
|
||||||
"service_run.ps1", "install_update_task.ps1", "sign_release.ps1",
|
"service_run.ps1", "install_update_task.ps1", "uninstall_prepare.ps1", "uninstall_delete_data.ps1", "sign_release.ps1",
|
||||||
"build_release.ps1", "publish_release.ps1", "publish_forgejo_release.ps1", "README.md", "manifest.example.json"
|
"build_release.ps1", "publish_release.ps1", "publish_forgejo_release.ps1", "README.md", "manifest.example.json"
|
||||||
)) {
|
)) {
|
||||||
$src = Join-Path $PSScriptRoot $name
|
$src = Join-Path $PSScriptRoot $name
|
||||||
|
|
@ -159,7 +160,7 @@ Set-Content -Path (Join-Path $stage ".portable") -Value "1" -Encoding ascii
|
||||||
|
|
||||||
# Root .cmd launchers (Explorer / Start Menu friendly; ASCII-only).
|
# Root .cmd launchers (Explorer / Start Menu friendly; ASCII-only).
|
||||||
$cmdSrc = Join-Path $PSScriptRoot "cmd"
|
$cmdSrc = Join-Path $PSScriptRoot "cmd"
|
||||||
foreach ($cmdName in @("NetX-FirstRun.cmd", "NetX-Start.cmd", "NetX-Tray.cmd")) {
|
foreach ($cmdName in @("NetX-FirstRun.cmd", "NetX-Start.cmd", "NetX-Tray.cmd", "NetX-Stop.cmd")) {
|
||||||
$c = Join-Path $cmdSrc $cmdName
|
$c = Join-Path $cmdSrc $cmdName
|
||||||
if (Test-Path $c) {
|
if (Test-Path $c) {
|
||||||
Copy-Item -Path $c -Destination (Join-Path $stage $cmdName) -Force
|
Copy-Item -Path $c -Destination (Join-Path $stage $cmdName) -Force
|
||||||
|
|
@ -167,10 +168,11 @@ foreach ($cmdName in @("NetX-FirstRun.cmd", "NetX-Start.cmd", "NetX-Tray.cmd"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($CreateVenv) {
|
if ($CreateVenv) {
|
||||||
Write-Host "==> Creating .venv in stage (requires Python 3.11+ on PATH)"
|
Write-Host "==> Creating .venv in stage (ships with Setup; target should NOT pip-install)"
|
||||||
$py = (Get-Command python -ErrorAction SilentlyContinue)
|
$pyPath = Get-NetxSystemPython
|
||||||
if (-not $py) { throw "python_not_found_for_venv" }
|
if (-not $pyPath) { throw "python_not_found_for_venv: need Python 3.11+ (not WindowsApps stub)" }
|
||||||
& $py.Source -m venv (Join-Path $stage ".venv")
|
Write-Host " Using: $pyPath"
|
||||||
|
& $pyPath -m venv (Join-Path $stage ".venv")
|
||||||
$venvPy = Join-Path $stage ".venv\Scripts\python.exe"
|
$venvPy = Join-Path $stage ".venv\Scripts\python.exe"
|
||||||
& $venvPy -m pip install --upgrade pip
|
& $venvPy -m pip install --upgrade pip
|
||||||
& $venvPy -m pip install -r (Join-Path $stage "requirements.txt")
|
& $venvPy -m pip install -r (Join-Path $stage "requirements.txt")
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,26 @@ setlocal
|
||||||
set "ROOT=%~dp0"
|
set "ROOT=%~dp0"
|
||||||
if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%"
|
if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%"
|
||||||
cd /d "%ROOT%"
|
cd /d "%ROOT%"
|
||||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%ROOT%\packaging\launch_shortcut.ps1" -Action setup -ProgramRoot "%ROOT%" -DataRoot "%ProgramData%\NetX"
|
title NetX — Reconfigure database
|
||||||
exit /b %ERRORLEVEL%
|
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
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,5 @@ setlocal
|
||||||
set "ROOT=%~dp0"
|
set "ROOT=%~dp0"
|
||||||
if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%"
|
if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%"
|
||||||
cd /d "%ROOT%"
|
cd /d "%ROOT%"
|
||||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%ROOT%\packaging\launch_shortcut.ps1" -Action start -ProgramRoot "%ROOT%" -DataRoot "%ProgramData%\NetX"
|
start "" powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "%ROOT%\packaging\launch_shortcut.ps1" -Action start -ProgramRoot "%ROOT%" -DataRoot "%ProgramData%\NetX"
|
||||||
exit /b %ERRORLEVEL%
|
exit /b 0
|
||||||
|
|
|
||||||
7
packaging/cmd/NetX-Stop.cmd
Normal file
7
packaging/cmd/NetX-Stop.cmd
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
@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
|
||||||
|
|
@ -3,5 +3,5 @@ setlocal
|
||||||
set "ROOT=%~dp0"
|
set "ROOT=%~dp0"
|
||||||
if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%"
|
if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%"
|
||||||
cd /d "%ROOT%"
|
cd /d "%ROOT%"
|
||||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%ROOT%\packaging\launch_shortcut.ps1" -Action tray -ProgramRoot "%ROOT%" -DataRoot "%ProgramData%\NetX" -StartOnLaunch
|
start "" powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "%ROOT%\packaging\launch_shortcut.ps1" -Action tray -ProgramRoot "%ROOT%" -DataRoot "%ProgramData%\NetX" -StartOnLaunch
|
||||||
exit /b %ERRORLEVEL%
|
exit /b 0
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
; NetX Windows installer (Inno Setup 6+)
|
; NetX Windows installer (Inno Setup 6+)
|
||||||
; Compile after: packaging\build_release.ps1
|
; Compile after: packaging\build_release.ps1
|
||||||
; ISCC.exe packaging\installer\netx.iss
|
; ISCC.exe packaging\installer\netx.iss
|
||||||
|
; Encoding: UTF-8 with BOM (required for Chinese CustomMessages)
|
||||||
|
|
||||||
#define MyAppName "NetX"
|
#define MyAppName "NetX"
|
||||||
#ifndef MyAppVersion
|
#ifndef MyAppVersion
|
||||||
|
|
@ -25,7 +26,10 @@ DefaultGroupName=NetX
|
||||||
DisableProgramGroupPage=yes
|
DisableProgramGroupPage=yes
|
||||||
LicenseFile={#StageDir}\LICENSE
|
LicenseFile={#StageDir}\LICENSE
|
||||||
OutputDir=..\release
|
OutputDir=..\release
|
||||||
OutputBaseFilename=NetX-Setup-{#MyAppVersion}
|
#ifndef OutputBaseFilename
|
||||||
|
#define OutputBaseFilename "NetX-Setup-" + MyAppVersion
|
||||||
|
#endif
|
||||||
|
OutputBaseFilename={#OutputBaseFilename}
|
||||||
SetupIconFile={#IconFile}
|
SetupIconFile={#IconFile}
|
||||||
UninstallDisplayIcon={app}\packaging\assets\netx.ico
|
UninstallDisplayIcon={app}\packaging\assets\netx.ico
|
||||||
Compression=lzma2
|
Compression=lzma2
|
||||||
|
|
@ -34,8 +38,10 @@ WizardStyle=modern
|
||||||
ArchitecturesAllowed=x64compatible
|
ArchitecturesAllowed=x64compatible
|
||||||
ArchitecturesInstallIn64BitMode=x64compatible
|
ArchitecturesInstallIn64BitMode=x64compatible
|
||||||
PrivilegesRequired=admin
|
PrivilegesRequired=admin
|
||||||
; Data lives under {commonappdata}\NetX 鈥?not overwritten by upgrades
|
; Data lives under {commonappdata}\NetX - not overwritten by upgrades.
|
||||||
CloseApplications=yes
|
; 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
|
||||||
ShowLanguageDialog=yes
|
ShowLanguageDialog=yes
|
||||||
|
|
||||||
[Languages]
|
[Languages]
|
||||||
|
|
@ -44,26 +50,76 @@ Name: "chinesesimplified"; MessagesFile: "Languages\ChineseSimplified.isl"
|
||||||
|
|
||||||
[CustomMessages]
|
[CustomMessages]
|
||||||
english.CreateDesktopIcon=Create a desktop shortcut
|
english.CreateDesktopIcon=Create a desktop shortcut
|
||||||
english.SetupOptions=Setup options:
|
english.SetupOptions=After install:
|
||||||
english.RunFirstSetup=Run first-time setup after install (bundled database, offline)
|
|
||||||
english.StartTrayNow=Start NetX tray now
|
english.StartTrayNow=Start NetX tray now
|
||||||
english.EnableAutostart=Start NetX tray at Windows logon
|
english.EnableAutostart=Start NetX tray at Windows logon
|
||||||
english.EnableAutoUpdate=Enable daily silent auto-update (needs network later)
|
english.EnableAutoUpdate=Enable daily silent auto-update (needs network later)
|
||||||
english.InstallService=Install as Windows Service (uses bundled WinSW, offline)
|
english.InstallService=Install as Windows Service (uses bundled WinSW, offline)
|
||||||
chinesesimplified.CreateDesktopIcon=鍒涘缓妗岄潰蹇嵎鏂瑰紡
|
english.UninstallDeleteData=Also delete NetX data (%ProgramData%\NetX)?%n%nYes = remove database, settings, and secrets%nNo = keep data for a later reinstall
|
||||||
chinesesimplified.SetupOptions=瀹夎閫夐」:
|
english.DbPageCaption=Database
|
||||||
chinesesimplified.RunFirstSetup=瀹夎鍚庤繍琛岄娆¢厤缃紙鍐呯疆鏁版嵁搴擄紝鍙绾匡級
|
english.DbPageDescription=Choose built-in or external PostgreSQL. Optional credential key: paste from an existing install, or leave empty to auto-generate.
|
||||||
chinesesimplified.StartTrayNow=绔嬪嵆鍚姩 NetX 鎵樼洏
|
english.DbBundled=Built-in PostgreSQL (portable, offline)
|
||||||
chinesesimplified.EnableAutostart=寮€鏈烘椂鑷姩鍚姩 NetX 鎵樼洏
|
english.DbExternal=External PostgreSQL (existing server)
|
||||||
chinesesimplified.EnableAutoUpdate=鍚敤姣忔棩闈欓粯鑷姩鏇存柊锛堜互鍚庨渶瑕佽仈缃戯級
|
english.DbHost=Host
|
||||||
chinesesimplified.InstallService=瀹夎涓?Windows 鏈嶅姟锛堜娇鐢ㄥ凡鎹嗙粦鐨?WinSW锛屽彲绂荤嚎锛?
|
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). See %ProgramData%\NetX or run Start Menu → Reconfigure database.
|
||||||
|
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)。请查看 %ProgramData%\NetX,或运行开始菜单 → 重新配置数据库。
|
||||||
|
chinesesimplified.ReconfigureDb=重新配置数据库
|
||||||
|
|
||||||
[Tasks]
|
[Tasks]
|
||||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce
|
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce
|
||||||
Name: "firstrun"; Description: "{cm:RunFirstSetup}"; GroupDescription: "{cm:SetupOptions}"; Flags: checkedonce
|
|
||||||
|
|
||||||
[Files]
|
[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"
|
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]
|
[Dirs]
|
||||||
Name: "{commonappdata}\NetX"
|
Name: "{commonappdata}\NetX"
|
||||||
|
|
@ -73,31 +129,656 @@ Name: "{commonappdata}\NetX\data\runtime"
|
||||||
Name: "{commonappdata}\NetX\pgdata"
|
Name: "{commonappdata}\NetX\pgdata"
|
||||||
Name: "{commonappdata}\NetX\backups"
|
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]
|
[Icons]
|
||||||
; Prefer .cmd targets so Server 2012 / classic Start Menu shows normal program entries.
|
; 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}\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}\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}\Stop NetX"; Filename: "{app}\NetX-Stop.cmd"; 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}\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}\Open NetX UI"; Filename: "http://127.0.0.1:8890/"; 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}\{cm:ReconfigureDb}"; 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}\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 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: "{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: "{commondesktop}\NetX First-time setup"; Filename: "{app}\NetX-FirstRun.cmd"; 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 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
|
Name: "{commondesktop}\NetX"; Filename: "{app}\NetX-Tray.cmd"; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico"; Tasks: desktopicon
|
||||||
|
|
||||||
[Run]
|
[Run]
|
||||||
; First-run is NonInteractive + bundled DB so offline machines succeed without prompts or downloads.
|
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
|
||||||
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\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\netx_tray.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"" -StartOnLaunch"; WorkingDir: "{app}"; Description: "{cm:StartTrayNow}"; Flags: postinstall nowait 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: "-ExecutionPolicy Bypass -File ""{app}\packaging\install_autostart.ps1"" -ProgramRoot ""{app}"""; WorkingDir: "{app}"; Description: "{cm:EnableAutostart}"; Flags: postinstall 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
|
||||||
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
|
; 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
|
||||||
|
|
||||||
[UninstallDelete]
|
[UninstallDelete]
|
||||||
; Do NOT delete {commonappdata}\NetX 鈥?preserves DB and secrets across reinstall
|
; Program files always removed. Data dir only when user chose Yes in InitializeUninstall.
|
||||||
Type: filesandordirs; Name: "{app}"
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
GSkipDbPage := EnvHasDbMode();
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
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;
|
||||||
|
begin
|
||||||
|
Result := True;
|
||||||
|
if GSkipDbPage then
|
||||||
|
Exit;
|
||||||
|
|
||||||
|
Params :=
|
||||||
|
'-NoProfile -ExecutionPolicy Bypass -File "' +
|
||||||
|
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);
|
||||||
|
Params := Params + ' -ExternalDatabaseUrlFile "' + UrlFile + '" -SkipExternalProbe';
|
||||||
|
end;
|
||||||
|
|
||||||
|
if GCredKey <> '' then
|
||||||
|
begin
|
||||||
|
KeyFile := ExpandConstant('{tmp}\netx_cred_key.txt');
|
||||||
|
SaveStringToFile(KeyFile, GCredKey, False);
|
||||||
|
Params := Params + ' -CredentialSecretKeyFile "' + KeyFile + '"';
|
||||||
|
end;
|
||||||
|
|
||||||
|
if not Exec(
|
||||||
|
'powershell.exe', Params, ExpandConstant('{app}'),
|
||||||
|
SW_HIDE, ewWaitUntilTerminated, ResultCode) then
|
||||||
|
begin
|
||||||
|
Result := False;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
if ResultCode <> 0 then
|
||||||
|
begin
|
||||||
|
MsgBox(FmtMessage(ExpandConstant('{cm:DbApplyFailed}'), [IntToStr(ResultCode)]), mbError, MB_OK);
|
||||||
|
Result := False;
|
||||||
|
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;
|
||||||
|
|
|
||||||
104
packaging/installer/test_pg_conn.ps1
Normal file
104
packaging/installer/test_pg_conn.ps1
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
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
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
# Visible entrypoint for Start Menu / desktop shortcuts.
|
# Visible entrypoint for Start Menu / desktop shortcuts.
|
||||||
# Shows a message box on failure (explorer-launched PowerShell otherwise flashes and exits).
|
# Nested PowerShell runs are hidden; only failures show a message box.
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
. "$PSScriptRoot\_common.ps1"
|
. "$PSScriptRoot\_common.ps1"
|
||||||
|
|
@ -42,63 +42,83 @@ 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 {
|
function Ensure-NetxEnv {
|
||||||
$envPath = Join-Path $data ".env"
|
$envPath = Join-Path $data ".env"
|
||||||
if (Test-Path $envPath) { return }
|
if (Test-Path $envPath) { return }
|
||||||
Write-LaunchLog "missing .env — running setup_first_run -NonInteractive -DbMode bundled"
|
# Do not silently force bundled DB — external-DB users must run interactive First-time setup.
|
||||||
$setup = Join-Path $PSScriptRoot "setup_first_run.ps1"
|
$cmd = Join-Path $prog "NetX-FirstRun.cmd"
|
||||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $setup `
|
throw (T `
|
||||||
-ProgramRoot $prog -DataRoot $data -NonInteractive -DbMode bundled
|
"First-time setup not done (missing $envPath).`nDouble-click desktop/Start Menu: First-time setup`nOr run: $cmd`nThere you can choose built-in or external PostgreSQL." `
|
||||||
if ($LASTEXITCODE -ne 0 -or -not (Test-Path $envPath)) {
|
"尚未完成首次配置(缺少 $envPath)。`n请双击桌面/开始菜单的「首次配置」`n或运行: $cmd`n在向导里可选内置或外置 PostgreSQL。")
|
||||||
$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 {
|
try {
|
||||||
Write-LaunchLog "action=$Action prog=$prog data=$data"
|
Write-LaunchLog "action=$Action prog=$prog data=$data"
|
||||||
switch ($Action) {
|
switch ($Action) {
|
||||||
"setup" {
|
"setup" {
|
||||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "setup_first_run.ps1") `
|
# Visible console so user can pick bundled vs external DB.
|
||||||
-ProgramRoot $prog -DataRoot $data
|
$p = Start-NetxPowerShell -File (Join-Path $PSScriptRoot "setup_first_run.ps1") `
|
||||||
if ($LASTEXITCODE -ne 0) { throw "setup_exit_$LASTEXITCODE" }
|
-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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"stop" {
|
"stop" {
|
||||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "stop_netx_app.ps1") `
|
$p = Invoke-NetxHiddenPs1 -File (Join-Path $PSScriptRoot "stop_netx_app.ps1")
|
||||||
-ProgramRoot $prog -DataRoot $data
|
if ($null -ne $p.ExitCode -and $p.ExitCode -ne 0) { throw "stop_exit_$($p.ExitCode)" }
|
||||||
}
|
}
|
||||||
"update" {
|
"update" {
|
||||||
Ensure-NetxEnv
|
Ensure-NetxEnv
|
||||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "check_update.ps1") `
|
# Update UI may need a visible window for prompts.
|
||||||
-ProgramRoot $prog -DataRoot $data
|
$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)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"start" {
|
"start" {
|
||||||
Ensure-NetxEnv
|
Ensure-NetxEnv
|
||||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "start_netx_app.ps1") `
|
$hostBind = "127.0.0.1"
|
||||||
-ProgramRoot $prog -DataRoot $data
|
$port = 8890
|
||||||
if ($LASTEXITCODE -ne 0) { throw "start_exit_$LASTEXITCODE" }
|
$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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"tray" {
|
"tray" {
|
||||||
Ensure-NetxEnv
|
Ensure-NetxEnv
|
||||||
$trayArgs = @(
|
$extra = @("-ProgramRoot", $prog, "-DataRoot", $data)
|
||||||
"-NoProfile", "-ExecutionPolicy", "Bypass",
|
if ($StartOnLaunch) { $extra += "-StartOnLaunch" }
|
||||||
"-File", (Join-Path $PSScriptRoot "netx_tray.ps1"),
|
# Detach tray; do not leave a success MessageBox (keeps a console open).
|
||||||
"-ProgramRoot", $prog, "-DataRoot", $data
|
Start-NetxPowerShell -File (Join-Path $PSScriptRoot "netx_tray.ps1") `
|
||||||
)
|
-Arguments $extra -WorkingDirectory $prog -WindowStyle Hidden | Out-Null
|
||||||
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"
|
Write-LaunchLog "action=$Action ok"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
param(
|
param(
|
||||||
[string]$ProgramRoot = "",
|
[string]$ProgramRoot = "",
|
||||||
[string]$DataRoot = "",
|
[string]$DataRoot = "",
|
||||||
[switch]$StartOnLaunch = $true,
|
[switch]$StartOnLaunch = $true,
|
||||||
|
|
@ -52,17 +52,57 @@ if (-not (Test-Path $envPath)) {
|
||||||
exit 1
|
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"
|
$startPs1 = Join-Path $PSScriptRoot "start_netx_app.ps1"
|
||||||
$stopPs1 = Join-Path $PSScriptRoot "stop_netx_app.ps1"
|
$stopPs1 = Join-Path $PSScriptRoot "stop_netx_app.ps1"
|
||||||
$checkPs1 = Join-Path $PSScriptRoot "check_update.ps1"
|
$checkPs1 = Join-Path $PSScriptRoot "check_update.ps1"
|
||||||
|
|
||||||
function Invoke-NetxScript {
|
function Invoke-NetxScript {
|
||||||
param([string]$File, [string[]]$ExtraArgs = @())
|
param(
|
||||||
$args = @(
|
[string]$File,
|
||||||
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $File,
|
[string[]]$ExtraArgs = @(),
|
||||||
"-ProgramRoot", $prog, "-DataRoot", $data
|
[switch]$Wait = $false
|
||||||
) + $ExtraArgs
|
)
|
||||||
Start-Process -FilePath "powershell.exe" -ArgumentList $args -WorkingDirectory $prog -WindowStyle Hidden
|
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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
$form = New-Object System.Windows.Forms.Form
|
$form = New-Object System.Windows.Forms.Form
|
||||||
|
|
@ -93,23 +133,30 @@ $miOpen.add_Click({ Start-Process $uiUrl })
|
||||||
|
|
||||||
$miStart = $menu.Items.Add((T "Start NetX" "启动 NetX"))
|
$miStart = $menu.Items.Add((T "Start NetX" "启动 NetX"))
|
||||||
$miStart.add_Click({
|
$miStart.add_Click({
|
||||||
$notify.ShowBalloonTip(3000, "NetX", (T "Starting…" "正在启动…"), [System.Windows.Forms.ToolTipIcon]::Info)
|
Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser") | Out-Null
|
||||||
Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser")
|
Open-NetxUiWhenReady
|
||||||
})
|
})
|
||||||
|
|
||||||
$miStop = $menu.Items.Add((T "Stop NetX" "停止 NetX"))
|
$miStop = $menu.Items.Add((T "Stop NetX (keep tray)" "停止 NetX(保留托盘)"))
|
||||||
$miStop.add_Click({
|
$miStop.add_Click({
|
||||||
$notify.ShowBalloonTip(3000, "NetX", (T "Stopping…" "正在停止…"), [System.Windows.Forms.ToolTipIcon]::Info)
|
$notify.ShowBalloonTip(3000, "NetX", (T "Stopping…" "正在停止…"), [System.Windows.Forms.ToolTipIcon]::Info)
|
||||||
Invoke-NetxScript -File $stopPs1
|
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()
|
||||||
})
|
})
|
||||||
|
|
||||||
$miUpdate = $menu.Items.Add((T "Check for updates" "检查更新"))
|
$miUpdate = $menu.Items.Add((T "Check for updates" "检查更新"))
|
||||||
$miUpdate.add_Click({
|
$miUpdate.add_Click({
|
||||||
try {
|
try {
|
||||||
$p = Start-Process -FilePath "powershell.exe" -ArgumentList @(
|
$p = Start-NetxPowerShell -File $checkPs1 -Arguments @("-ProgramRoot", $prog, "-DataRoot", $data) `
|
||||||
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $checkPs1,
|
-WorkingDirectory $prog -WindowStyle Normal -Wait -PassThru
|
||||||
"-ProgramRoot", $prog, "-DataRoot", $data
|
|
||||||
) -Wait -PassThru -WindowStyle Normal
|
|
||||||
if ($p.ExitCode -eq 10) {
|
if ($p.ExitCode -eq 10) {
|
||||||
$ans = [System.Windows.Forms.MessageBox]::Show(
|
$ans = [System.Windows.Forms.MessageBox]::Show(
|
||||||
(T "A newer NetX is available. Download and apply now?" "发现新版本,是否立即下载并更新?"),
|
(T "A newer NetX is available. Download and apply now?" "发现新版本,是否立即下载并更新?"),
|
||||||
|
|
@ -118,10 +165,8 @@ $miUpdate.add_Click({
|
||||||
[System.Windows.Forms.MessageBoxIcon]::Question
|
[System.Windows.Forms.MessageBoxIcon]::Question
|
||||||
)
|
)
|
||||||
if ($ans -eq [System.Windows.Forms.DialogResult]::Yes) {
|
if ($ans -eq [System.Windows.Forms.DialogResult]::Yes) {
|
||||||
Start-Process -FilePath "powershell.exe" -ArgumentList @(
|
Start-NetxPowerShell -File $checkPs1 -Arguments @("-ProgramRoot", $prog, "-DataRoot", $data, "-Apply") `
|
||||||
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $checkPs1,
|
-WorkingDirectory $prog -WindowStyle Normal | Out-Null
|
||||||
"-ProgramRoot", $prog, "-DataRoot", $data, "-Apply"
|
|
||||||
) -WorkingDirectory $prog
|
|
||||||
}
|
}
|
||||||
} elseif ($p.ExitCode -eq 0) {
|
} elseif ($p.ExitCode -eq 0) {
|
||||||
[System.Windows.Forms.MessageBox]::Show((T "NetX is up to date ($ver)." "已是最新版本 ($ver)。"), (T "NetX update" "NetX 更新"))
|
[System.Windows.Forms.MessageBox]::Show((T "NetX is up to date ($ver)." "已是最新版本 ($ver)。"), (T "NetX update" "NetX 更新"))
|
||||||
|
|
@ -146,20 +191,49 @@ $notify.add_DoubleClick({ Start-Process $uiUrl })
|
||||||
$form.add_Shown({
|
$form.add_Shown({
|
||||||
if ($AutoUpdate) {
|
if ($AutoUpdate) {
|
||||||
$notify.ShowBalloonTip(4000, "NetX", (T "Checking for updates…" "正在检查更新…"), [System.Windows.Forms.ToolTipIcon]::Info)
|
$notify.ShowBalloonTip(4000, "NetX", (T "Checking for updates…" "正在检查更新…"), [System.Windows.Forms.ToolTipIcon]::Info)
|
||||||
Start-Process -FilePath "powershell.exe" -ArgumentList @(
|
Start-NetxPowerShell -File $checkPs1 `
|
||||||
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $checkPs1,
|
-Arguments @("-ProgramRoot", $prog, "-DataRoot", $data, "-Apply", "-Quiet", "-AutoOnly") `
|
||||||
"-ProgramRoot", $prog, "-DataRoot", $data, "-Apply", "-Quiet", "-AutoOnly"
|
-WorkingDirectory $prog -WindowStyle Hidden -Wait | Out-Null
|
||||||
) -Wait -WindowStyle Hidden
|
|
||||||
} elseif ($CheckUpdateOnLaunch) {
|
} elseif ($CheckUpdateOnLaunch) {
|
||||||
Start-Process -FilePath "powershell.exe" -ArgumentList @(
|
Start-NetxPowerShell -File $checkPs1 `
|
||||||
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $checkPs1,
|
-Arguments @("-ProgramRoot", $prog, "-DataRoot", $data, "-Quiet") `
|
||||||
"-ProgramRoot", $prog, "-DataRoot", $data, "-Quiet"
|
-WorkingDirectory $prog -WindowStyle Hidden | Out-Null
|
||||||
) -WindowStyle Hidden
|
|
||||||
}
|
}
|
||||||
if ($StartOnLaunch) {
|
if ($StartOnLaunch) {
|
||||||
Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser")
|
# Detach start so the tray message pump stays alive; poll /health before browser.
|
||||||
Start-Sleep -Seconds 2
|
Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser") | Out-Null
|
||||||
try { Start-Process $uiUrl } catch {}
|
$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()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,13 @@
|
||||||
[string]$ProgramRoot = "",
|
[string]$ProgramRoot = "",
|
||||||
[string]$DataRoot = "",
|
[string]$DataRoot = "",
|
||||||
[string]$ExternalDatabaseUrl = "",
|
[string]$ExternalDatabaseUrl = "",
|
||||||
|
[string]$ExternalDatabaseUrlFile = "",
|
||||||
|
[string]$CredentialSecretKey = "",
|
||||||
|
[string]$CredentialSecretKeyFile = "",
|
||||||
[string]$BundledPassword = "",
|
[string]$BundledPassword = "",
|
||||||
[int]$BundledPort = 15432,
|
[int]$BundledPort = 15432,
|
||||||
[switch]$NonInteractive = $false
|
[switch]$NonInteractive = $false,
|
||||||
|
[switch]$SkipExternalProbe = $false
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
@ -39,9 +43,10 @@ if (-not $DbMode) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host (T "Choose database mode:" "选择数据库模式:")
|
Write-Host (T "Choose database mode:" "选择数据库模式:") -ForegroundColor Cyan
|
||||||
Write-Host (T " 1) bundled — use NetX portable PostgreSQL (default for new installs)" " 1) bundled — 使用 NetX 内置便携 PostgreSQL(新安装默认)")
|
Write-Host (T " 1) bundled — NetX portable PostgreSQL (offline / default)" " 1) bundled — NetX 内置便携 PostgreSQL(可离线,默认)")
|
||||||
Write-Host (T " 2) external — connect to an existing PostgreSQL (Linux / already deployed)" " 2) external — 连接已有 PostgreSQL")
|
Write-Host (T " 2) external — existing PostgreSQL (you provide connection URL)" " 2) external — 已有外置 PostgreSQL(需填写连接串)")
|
||||||
|
Write-Host ""
|
||||||
$choice = Read-Host (T "Enter 1 or 2" "请输入 1 或 2")
|
$choice = Read-Host (T "Enter 1 or 2" "请输入 1 或 2")
|
||||||
if ($choice -eq "2") { $DbMode = "external" } else { $DbMode = "bundled" }
|
if ($choice -eq "2") { $DbMode = "external" } else { $DbMode = "bundled" }
|
||||||
}
|
}
|
||||||
|
|
@ -67,6 +72,35 @@ if (Test-Path (Join-Path $distAbs "index.html")) {
|
||||||
$dataEnv = Get-NetxDataEnvMap -DataRoot $data
|
$dataEnv = Get-NetxDataEnvMap -DataRoot $data
|
||||||
foreach ($k in $dataEnv.Keys) { $values[$k] = $dataEnv[$k] }
|
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") {
|
if ($DbMode -eq "bundled") {
|
||||||
$pgsql = Join-Path $prog "postgres\pgsql"
|
$pgsql = Join-Path $prog "postgres\pgsql"
|
||||||
if (-not (Test-Path (Join-Path $pgsql "bin\initdb.exe"))) {
|
if (-not (Test-Path (Join-Path $pgsql "bin\initdb.exe"))) {
|
||||||
|
|
@ -189,23 +223,109 @@ if ($DbMode -eq "bundled") {
|
||||||
}
|
}
|
||||||
Write-Host "==> Bundled Postgres ready on 127.0.0.1:$BundledPort" -ForegroundColor Green
|
Write-Host "==> Bundled Postgres ready on 127.0.0.1:$BundledPort" -ForegroundColor Green
|
||||||
} else {
|
} 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 (-not $ExternalDatabaseUrl) {
|
||||||
if ($existing.ContainsKey("NETX_DATABASE_URL") -and $existing["NETX_DATABASE_URL"]) {
|
if ($existing.ContainsKey("NETX_DATABASE_URL") -and $existing["NETX_DATABASE_URL"]) {
|
||||||
$ExternalDatabaseUrl = $existing["NETX_DATABASE_URL"]
|
$ExternalDatabaseUrl = $existing["NETX_DATABASE_URL"]
|
||||||
} elseif ($NonInteractive) {
|
} elseif ($NonInteractive) {
|
||||||
throw "external_url_required"
|
throw "external_url_required"
|
||||||
} else {
|
} else {
|
||||||
$ExternalDatabaseUrl = Read-Host "NETX_DATABASE_URL (postgresql+psycopg://user:pass@host:5432/netx)"
|
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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (-not $ExternalDatabaseUrl) {
|
if (-not $ExternalDatabaseUrl) {
|
||||||
throw "external_url_required"
|
throw "external_url_required"
|
||||||
}
|
}
|
||||||
|
$ExternalDatabaseUrl = $ExternalDatabaseUrl.Trim()
|
||||||
$values["NETX_DATABASE_URL"] = $ExternalDatabaseUrl
|
$values["NETX_DATABASE_URL"] = $ExternalDatabaseUrl
|
||||||
Write-Host "==> External Postgres configured (ensure role/db exist; see scripts\init_pg.ps1)" -ForegroundColor Green
|
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-DotEnvValue -Path $envPath -Values $values
|
Write-DotEnvValue -Path $envPath -Values $values
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Wrote $envPath" -ForegroundColor Green
|
Write-Host "Wrote $envPath" -ForegroundColor Green
|
||||||
Write-Host "Next: .\packaging\start_netx_app.ps1"
|
|
||||||
|
# Official Setup ships .venv (build_release -CreateVenv). Only bootstrap if missing
|
||||||
|
# (e.g. zip without venv). Prefer not to pip-install on the target machine.
|
||||||
|
$venvPy = Join-Path $prog ".venv\Scripts\python.exe"
|
||||||
|
if (Test-Path $venvPy) {
|
||||||
|
Write-Host "==> Using bundled Python venv: $venvPy" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "==> Bundled .venv missing — creating one (Setup should normally ship it)." -ForegroundColor Yellow
|
||||||
|
try {
|
||||||
|
$null = Ensure-NetxVenv -ProgramRoot $prog
|
||||||
|
Write-Host "==> Python venv ready: $venvPy" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
param(
|
param(
|
||||||
[string]$ProgramRoot = "",
|
[string]$ProgramRoot = "",
|
||||||
[string]$DataRoot = "",
|
[string]$DataRoot = "",
|
||||||
[switch]$SkipBrowser = $false,
|
[switch]$SkipBrowser = $false,
|
||||||
|
|
@ -96,15 +96,13 @@ if ($mode -eq "bundled") {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$venvPy = Join-Path $prog ".venv\Scripts\python.exe"
|
try {
|
||||||
if (-not (Test-Path $venvPy)) {
|
$null = Ensure-NetxVenv -ProgramRoot $prog
|
||||||
Write-Host "==> Creating .venv (one-time)"
|
} catch {
|
||||||
$pyCmd = Get-Command python -ErrorAction SilentlyContinue
|
Write-Host "[ERR] $($_.Exception.Message)" -ForegroundColor Red
|
||||||
if (-not $pyCmd) { throw "python_not_found: install Python 3.11+ and re-run" }
|
Write-Host " Tip: run Start Menu → NetX → First-time setup once as Administrator," -ForegroundColor Yellow
|
||||||
& $pyCmd.Source -m venv (Join-Path $prog ".venv")
|
Write-Host " or install a Setup built with packaging\\build_release.ps1 -CreateVenv." -ForegroundColor Yellow
|
||||||
& $venvPy -m pip install --upgrade pip
|
exit 1
|
||||||
& $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)) {
|
if (-not (Test-NetxTcpPortFree -HostName $hostBind -Port $port)) {
|
||||||
|
|
@ -120,18 +118,22 @@ if (-not (Test-Path $startScript)) {
|
||||||
throw "start_netx.ps1 not found at $startScript"
|
throw "start_netx.ps1 not found at $startScript"
|
||||||
}
|
}
|
||||||
|
|
||||||
$psArgs = @(
|
$startExtra = @(
|
||||||
"-ExecutionPolicy", "Bypass", "-File", $startScript,
|
|
||||||
"-SkipInstall", "-Background",
|
"-SkipInstall", "-Background",
|
||||||
"-BindHost", $hostBind, "-Port", "$port"
|
"-BindHost", $hostBind, "-Port", "$port"
|
||||||
)
|
)
|
||||||
if ($InlineSchedulers) { $psArgs += "-InlineSchedulers" }
|
if ($InlineSchedulers) { $startExtra += "-InlineSchedulers" }
|
||||||
|
|
||||||
Write-Host "==> Starting NetX (API + workers; UI via API on :$port)"
|
Write-Host "==> Starting NetX (API + workers; UI via API on :$port)"
|
||||||
& powershell @psArgs
|
$startProc = Start-NetxPowerShell -File $startScript -Arguments $startExtra `
|
||||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
-WorkingDirectory $prog -WindowStyle Hidden -Wait -PassThru
|
||||||
|
if ($startProc.ExitCode -ne 0) { exit $startProc.ExitCode }
|
||||||
|
|
||||||
$url = "http://${hostBind}:${port}/"
|
$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
|
Write-Host "==> Open: $url" -ForegroundColor Green
|
||||||
if (-not $SkipBrowser) {
|
if (-not $SkipBrowser) {
|
||||||
try { Start-Process $url } catch {}
|
try { Start-Process $url } catch {}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
param(
|
param(
|
||||||
[string]$ProgramRoot = "",
|
[string]$ProgramRoot = "",
|
||||||
[string]$DataRoot = "",
|
[string]$DataRoot = "",
|
||||||
[switch]$KeepPostgres = $false
|
[switch]$KeepPostgres = $false,
|
||||||
|
[switch]$KillTray = $false
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Continue"
|
$ErrorActionPreference = "Continue"
|
||||||
|
|
@ -21,11 +22,17 @@ if (-not (Test-Path $stopScript)) {
|
||||||
}
|
}
|
||||||
if (Test-Path $stopScript) {
|
if (Test-Path $stopScript) {
|
||||||
Write-Host "==> Stopping NetX processes"
|
Write-Host "==> Stopping NetX processes"
|
||||||
& powershell -ExecutionPolicy Bypass -File $stopScript -Force
|
Start-NetxPowerShell -File $stopScript -Arguments @("-Force") `
|
||||||
|
-WorkingDirectory $prog -WindowStyle Hidden -Wait | Out-Null
|
||||||
} else {
|
} else {
|
||||||
Write-Host "[WARN] stop_netx.ps1 not found"
|
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
|
$mode = Get-DbMode -EnvMap $map
|
||||||
if ($mode -eq "bundled" -and -not $KeepPostgres) {
|
if ($mode -eq "bundled" -and -not $KeepPostgres) {
|
||||||
$pgBinCandidates = @(
|
$pgBinCandidates = @(
|
||||||
|
|
|
||||||
53
packaging/uninstall_delete_data.ps1
Normal file
53
packaging/uninstall_delete_data.ps1
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
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
|
||||||
71
packaging/uninstall_prepare.ps1
Normal file
71
packaging/uninstall_prepare.ps1
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
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
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
param(
|
param(
|
||||||
[string]$BindHost = "127.0.0.1",
|
[string]$BindHost = "127.0.0.1",
|
||||||
[int]$Port = 8890,
|
[int]$Port = 8890,
|
||||||
[int]$WebPort = 5173,
|
[int]$WebPort = 5173,
|
||||||
|
|
@ -260,7 +260,11 @@ if ($Background) {
|
||||||
Show-LogTail -Path $logFile
|
Show-LogTail -Path $logFile
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
$healthWaitSec = 45
|
# Fresh installs run Alembic upgrades on first boot; 45s is often too short.
|
||||||
|
$healthWaitSec = 180
|
||||||
|
if ($env:NETX_START_HEALTH_WAIT_SEC) {
|
||||||
|
try { $healthWaitSec = [int]$env:NETX_START_HEALTH_WAIT_SEC } catch {}
|
||||||
|
}
|
||||||
if (-not (Test-NetxApiListening -HostName $BindHost -LocalPort $Port -WaitSec $healthWaitSec)) {
|
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
|
Write-Host "[ERR] Port $Port not listening /health not OK within ${healthWaitSec}s (process may be stuck on DB or schema migration)." -ForegroundColor Red
|
||||||
Show-LogTail -Path $errFile
|
Show-LogTail -Path $errFile
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue