mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 00:50:46 +08:00
Release 0.4.7: ProgramData ACL fix and tray menu polish.
Some checks failed
Release Windows / build (push) Has been cancelled
Some checks failed
Release Windows / build (push) Has been cancelled
Allow non-admin reconfigure of .env, harden setup/tray flows, and refresh the context menu with hover selection. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
5429c07342
commit
4e582dc4a5
6 changed files with 583 additions and 131 deletions
|
|
@ -1,4 +1,4 @@
|
|||
# Shared path helpers for Windows packaging scripts.
|
||||
# Shared path helpers for Windows packaging scripts.
|
||||
# Dot-source: . "$PSScriptRoot\_common.ps1"
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
|
@ -114,7 +114,33 @@ function Write-DotEnvValue {
|
|||
if (-not (Test-Path $dir)) {
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
}
|
||||
Set-Content -Path $Path -Value ($lines -join "`r`n") -Encoding utf8
|
||||
$text = ($lines -join "`r`n")
|
||||
try {
|
||||
Set-Content -LiteralPath $Path -Value $text -Encoding utf8
|
||||
} catch {
|
||||
# Admin-created .env is often Users:RX only — repair ACL then retry once.
|
||||
$root = $dir
|
||||
if ((Split-Path -Leaf $dir) -eq "NetX" -or $dir -match '\\NetX$') {
|
||||
$root = $dir
|
||||
} else {
|
||||
$parent = Split-Path -Parent $dir
|
||||
if ($parent -and ((Split-Path -Leaf $parent) -eq "NetX")) { $root = $parent }
|
||||
}
|
||||
$null = Ensure-NetxDataAcl -DataRoot $root -Quiet
|
||||
try {
|
||||
# Reset .env ACL explicitly if still blocked.
|
||||
$icacls = Join-Path $env:SystemRoot "System32\icacls.exe"
|
||||
if (Test-Path -LiteralPath $icacls) {
|
||||
& $icacls $Path /grant "*S-1-5-32-545:M" /C /Q 2>$null | Out-Null
|
||||
}
|
||||
Set-Content -LiteralPath $Path -Value $text -Encoding utf8
|
||||
} catch {
|
||||
throw (New-Object System.UnauthorizedAccessException(
|
||||
("access_denied: cannot write {0}. Run Start Menu → NetX → Reconfigure database as Administrator, or: icacls `"{1}`" /grant Users:(OI)(CI)M /T" -f $Path, $root),
|
||||
$_.Exception
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-DbMode {
|
||||
|
|
@ -169,8 +195,48 @@ function New-NetxFernetKey {
|
|||
return [Convert]::ToBase64String($bytes).Replace('+', '-').Replace('/', '_')
|
||||
}
|
||||
|
||||
function Ensure-NetxDataAcl {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$DataRoot,
|
||||
[switch]$Quiet = $false
|
||||
)
|
||||
# Installer creates %ProgramData%\NetX as Administrators. Tray / normal users must
|
||||
# be able to update .env and runtime files when reconfiguring DB.
|
||||
if (-not (Test-Path -LiteralPath $DataRoot)) { return $false }
|
||||
try {
|
||||
$acl = Get-Acl -LiteralPath $DataRoot
|
||||
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
|
||||
"BUILTIN\Users",
|
||||
"Modify",
|
||||
"ContainerInherit,ObjectInherit",
|
||||
"None",
|
||||
"Allow"
|
||||
)
|
||||
$acl.SetAccessRule($rule)
|
||||
Set-Acl -LiteralPath $DataRoot -AclObject $acl
|
||||
|
||||
# Also fix already-created files that only inherited RX for Users.
|
||||
$icacls = Join-Path $env:SystemRoot "System32\icacls.exe"
|
||||
if (Test-Path -LiteralPath $icacls) {
|
||||
& $icacls $DataRoot /grant "*S-1-5-32-545:(OI)(CI)M" /T /C /Q 2>$null | Out-Null
|
||||
}
|
||||
if (-not $Quiet) {
|
||||
Write-Host "==> Data ACL: BUILTIN\Users Modify on $DataRoot" -ForegroundColor DarkGray
|
||||
}
|
||||
return $true
|
||||
} catch {
|
||||
if (-not $Quiet) {
|
||||
Write-Host "[WARN] Ensure-NetxDataAcl: $($_.Exception.Message)" -ForegroundColor Yellow
|
||||
}
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Ensure-NetxDataDirectories {
|
||||
param([string]$DataRoot)
|
||||
if (-not (Test-Path -LiteralPath $DataRoot)) {
|
||||
New-Item -ItemType Directory -Path $DataRoot -Force | Out-Null
|
||||
}
|
||||
$subs = @(
|
||||
"data", "data\auth", "data\runtime", "data\biz_state_spool",
|
||||
"data\ne_collections", "data\ne_exec_jobs", "data\webcrt",
|
||||
|
|
@ -182,6 +248,8 @@ function Ensure-NetxDataDirectories {
|
|||
New-Item -ItemType Directory -Path $p -Force | Out-Null
|
||||
}
|
||||
}
|
||||
# Best-effort; succeeds when running elevated (installer / first-run as admin).
|
||||
$null = Ensure-NetxDataAcl -DataRoot $DataRoot -Quiet
|
||||
}
|
||||
|
||||
function Test-NetxTcpPortFree {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
; NetX Windows installer (Inno Setup 6+)
|
||||
; NetX Windows installer (Inno Setup 6+)
|
||||
; Compile after: packaging\build_release.ps1
|
||||
; ISCC.exe packaging\installer\netx.iss
|
||||
; Encoding: UTF-8 with BOM (required for Chinese CustomMessages)
|
||||
|
||||
#define MyAppName "NetX"
|
||||
#ifndef MyAppVersion
|
||||
#define MyAppVersion "0.4.6"
|
||||
#define MyAppVersion "0.4.7"
|
||||
#endif
|
||||
#define MyAppPublisher "NetX"
|
||||
#define MyAppURL "https://github.com/hansjone/netx"
|
||||
|
|
@ -39,7 +39,7 @@ ArchitecturesAllowed=x64compatible
|
|||
ArchitecturesInstallIn64BitMode=x64compatible
|
||||
PrivilegesRequired=admin
|
||||
; Data lives under {commonappdata}\NetX - not overwritten by upgrades.
|
||||
; Do NOT use CloseApplications=yes — it can freeze/beep on the Tasks/Ready
|
||||
; 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
|
||||
|
|
@ -66,45 +66,45 @@ english.DbUser=User
|
|||
english.DbPassword=Password
|
||||
english.DbName=Database
|
||||
english.CredKey=Credential key
|
||||
english.CredKeyHint=NETX_CREDENTIAL_SECRET_KEY (optional — empty = auto-generate)
|
||||
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.DbTesting=Testing PostgreSQL connection, please wait…
|
||||
english.DbTestFailed=Cannot connect to PostgreSQL with these settings.%n%n%1%n%nFix the settings and try again.
|
||||
english.DbTestExtractFailed=Could not extract PostgreSQL client tools for connection test.
|
||||
english.DbSilentExternalMissing=Silent install with external DB requires /DbHost /DbUser /DbPassword /DbName (optional /DbPort).
|
||||
english.DbApplyFailed=Database configuration failed after file install (exit %1).%n%nLog: %2%n%nFix the problem, then use Start Menu → Reconfigure database — or reinstall.
|
||||
english.DbApplyFailed=Database configuration failed after file install (exit %1).%n%nLog: %2%n%nFix the problem, then use Start Menu → Reconfigure database — or reinstall.
|
||||
english.DbApplyExecFailed=Could not start database configuration script.
|
||||
english.DbBundledMissing=Built-in PostgreSQL files are missing from the install folder:%n%1%n%nThe Setup package is incomplete. Re-download NetX-Setup and install again.
|
||||
english.DbEnvMissing=Database configuration did not write %ProgramData%\NetX\.env.
|
||||
english.ReconfigureDb=Reconfigure database
|
||||
chinesesimplified.CreateDesktopIcon=创建桌面快捷方式
|
||||
chinesesimplified.SetupOptions=安装完成后:
|
||||
chinesesimplified.StartTrayNow=立即启动 NetX 托盘
|
||||
chinesesimplified.EnableAutostart=开机时自动启动 NetX 托盘
|
||||
chinesesimplified.EnableAutoUpdate=启用每日静默自动更新(以后需要联网)
|
||||
chinesesimplified.InstallService=安装为 Windows 服务(使用已捆绑的 WinSW,可离线)
|
||||
chinesesimplified.UninstallDeleteData=是否同时删除 NetX 数据(%ProgramData%\NetX)?%n%n是 = 删除数据库、配置和密钥%n否 = 保留数据以便以后重装
|
||||
chinesesimplified.DbPageCaption=数据库
|
||||
chinesesimplified.DbPageDescription=选择内置或外置 PostgreSQL。可选粘贴已有 NETX_CREDENTIAL_SECRET_KEY;留空则自动生成。
|
||||
chinesesimplified.DbBundled=内置 PostgreSQL(便携,可离线)
|
||||
chinesesimplified.DbExternal=外置 PostgreSQL(已有服务器)
|
||||
chinesesimplified.DbHost=主机
|
||||
chinesesimplified.DbPort=端口
|
||||
chinesesimplified.DbUser=用户
|
||||
chinesesimplified.DbPassword=密码
|
||||
chinesesimplified.DbName=数据库名
|
||||
chinesesimplified.CredKey=凭据密钥
|
||||
chinesesimplified.CredKeyHint=NETX_CREDENTIAL_SECRET_KEY(可选,留空=自动生成)
|
||||
chinesesimplified.DbFieldsRequired=请填写主机、端口、用户、密码和数据库名。
|
||||
chinesesimplified.DbTesting=正在测试 PostgreSQL 连接,请稍候…
|
||||
chinesesimplified.DbTestFailed=无法用当前设置连接 PostgreSQL。%n%n%1%n%n请改正后重试。
|
||||
chinesesimplified.DbTestExtractFailed=无法解压 PostgreSQL 客户端以测试连接。
|
||||
chinesesimplified.DbSilentExternalMissing=静默安装外置库需要参数 /DbHost /DbUser /DbPassword /DbName(可选 /DbPort)。
|
||||
chinesesimplified.DbApplyFailed=文件安装后数据库自动配置失败(退出码 %1)。%n%n日志: %2%n%n请处理后使用开始菜单 → 重新配置数据库,或重新安装。
|
||||
chinesesimplified.DbApplyExecFailed=无法启动数据库配置脚本。
|
||||
chinesesimplified.DbBundledMissing=安装目录中缺少内置 PostgreSQL 文件:%n%1%n%n安装包不完整,请重新下载 NetX-Setup 后再装。
|
||||
chinesesimplified.DbEnvMissing=数据库配置未写入 %ProgramData%\NetX\.env。
|
||||
chinesesimplified.ReconfigureDb=重新配置数据库
|
||||
chinesesimplified.CreateDesktopIcon=创建桌面快捷方式
|
||||
chinesesimplified.SetupOptions=安装完成后:
|
||||
chinesesimplified.StartTrayNow=立即启动 NetX 托盘
|
||||
chinesesimplified.EnableAutostart=开机时自动启动 NetX 托盘
|
||||
chinesesimplified.EnableAutoUpdate=启用每日静默自动更新(以后需要联网)
|
||||
chinesesimplified.InstallService=安装为 Windows 服务(使用已捆绑的 WinSW,可离线)
|
||||
chinesesimplified.UninstallDeleteData=是否同时删除 NetX 数据(%ProgramData%\NetX)?%n%n是 = 删除数据库、配置和密钥%n否 = 保留数据以便以后重装
|
||||
chinesesimplified.DbPageCaption=数据库
|
||||
chinesesimplified.DbPageDescription=选择内置或外置 PostgreSQL。可选粘贴已有 NETX_CREDENTIAL_SECRET_KEY;留空则自动生成。
|
||||
chinesesimplified.DbBundled=内置 PostgreSQL(便携,可离线)
|
||||
chinesesimplified.DbExternal=外置 PostgreSQL(已有服务器)
|
||||
chinesesimplified.DbHost=主机
|
||||
chinesesimplified.DbPort=端口
|
||||
chinesesimplified.DbUser=用户
|
||||
chinesesimplified.DbPassword=密码
|
||||
chinesesimplified.DbName=数据库名
|
||||
chinesesimplified.CredKey=凭据密钥
|
||||
chinesesimplified.CredKeyHint=NETX_CREDENTIAL_SECRET_KEY(可选,留空=自动生成)
|
||||
chinesesimplified.DbFieldsRequired=请填写主机、端口、用户、密码和数据库名。
|
||||
chinesesimplified.DbTesting=正在测试 PostgreSQL 连接,请稍候…
|
||||
chinesesimplified.DbTestFailed=无法用当前设置连接 PostgreSQL。%n%n%1%n%n请改正后重试。
|
||||
chinesesimplified.DbTestExtractFailed=无法解压 PostgreSQL 客户端以测试连接。
|
||||
chinesesimplified.DbSilentExternalMissing=静默安装外置库需要参数 /DbHost /DbUser /DbPassword /DbName(可选 /DbPort)。
|
||||
chinesesimplified.DbApplyFailed=文件安装后数据库自动配置失败(退出码 %1)。%n%n日志: %2%n%n请处理后使用开始菜单 → 重新配置数据库,或重新安装。
|
||||
chinesesimplified.DbApplyExecFailed=无法启动数据库配置脚本。
|
||||
chinesesimplified.DbBundledMissing=安装目录中缺少内置 PostgreSQL 文件:%n%1%n%n安装包不完整,请重新下载 NetX-Setup 后再装。
|
||||
chinesesimplified.DbEnvMissing=数据库配置未写入 %ProgramData%\NetX\.env。
|
||||
chinesesimplified.ReconfigureDb=重新配置数据库
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce
|
||||
|
|
@ -128,18 +128,19 @@ Source: "{#StageDir}\postgres\pgsql\bin\libxml2.dll"; Flags: dontcopy
|
|||
Source: "{#StageDir}\postgres\pgsql\bin\libxslt.dll"; Flags: dontcopy
|
||||
|
||||
[Dirs]
|
||||
Name: "{commonappdata}\NetX"
|
||||
Name: "{commonappdata}\NetX\data"
|
||||
Name: "{commonappdata}\NetX\data\auth"
|
||||
Name: "{commonappdata}\NetX\data\runtime"
|
||||
Name: "{commonappdata}\NetX\pgdata"
|
||||
Name: "{commonappdata}\NetX\backups"
|
||||
; users-modify so tray / non-admin reconfigure can update .env and runtime files.
|
||||
Name: "{commonappdata}\NetX"; Permissions: users-modify
|
||||
Name: "{commonappdata}\NetX\data"; Permissions: users-modify
|
||||
Name: "{commonappdata}\NetX\data\auth"; Permissions: users-modify
|
||||
Name: "{commonappdata}\NetX\data\runtime"; Permissions: users-modify
|
||||
Name: "{commonappdata}\NetX\pgdata"; Permissions: users-modify
|
||||
Name: "{commonappdata}\NetX\backups"; Permissions: users-modify
|
||||
|
||||
[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"
|
||||
Type: files; Name: "{commondesktop}\首次配置(内置或外置数据库).lnk"
|
||||
|
||||
[Icons]
|
||||
; Prefer .cmd targets so Server 2012 / classic Start Menu shows normal program entries.
|
||||
|
|
@ -505,7 +506,7 @@ begin
|
|||
GDbConnOk := False;
|
||||
{ Always show DB page so choosing built-in always re-runs auto-configure.
|
||||
Skipping when .env exists caused upgrades/reinstalls to keep a broken or
|
||||
external config and still launch tray → confusing "first-time" prompts. }
|
||||
external config and still launch tray → confusing "first-time" prompts. }
|
||||
GSkipDbPage := False;
|
||||
|
||||
DbPage := CreateCustomPage(wpSelectDir,
|
||||
|
|
@ -678,7 +679,7 @@ begin
|
|||
end;
|
||||
|
||||
{ Connection test here so bad settings never reach Tasks/Ready.
|
||||
Show busy state — ExtractTemporaryFile + psql can take a few seconds. }
|
||||
Show busy state — ExtractTemporaryFile + psql can take a few seconds. }
|
||||
SetWizardBusy(True, ExpandConstant('{cm:DbTesting}'));
|
||||
try
|
||||
Result := ValidateExternalDbSaved(ErrMsg);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
{
|
||||
"channel": "stable",
|
||||
"latest": "0.4.5",
|
||||
"latest": "0.4.7",
|
||||
"min_compatible": "0.3.0",
|
||||
"notes_url": "https://github.com/hansjone/netx/releases/tag/v0.4.5",
|
||||
"notes_url": "https://github.com/hansjone/netx/releases/tag/v0.4.7",
|
||||
"windows": {
|
||||
"url": "https://github.com/hansjone/netx/releases/download/v0.4.5/NetX-0.4.5-win64.zip",
|
||||
"setup_url": "https://github.com/hansjone/netx/releases/download/v0.4.5/NetX-Setup-0.4.5.exe",
|
||||
"url": "https://github.com/hansjone/netx/releases/download/v0.4.7/NetX-0.4.7-win64.zip",
|
||||
"setup_url": "https://github.com/hansjone/netx/releases/download/v0.4.7/NetX-Setup-0.4.7.exe",
|
||||
"sha256": "",
|
||||
"size": 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@
|
|||
[switch]$AutoUpdate = $false
|
||||
)
|
||||
|
||||
# Simple system-tray controller for NetX (Windows packaged installs).
|
||||
# Start / Stop / Open UI / Check update / Exit
|
||||
# System-tray controller for NetX (Windows packaged installs).
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
. "$PSScriptRoot\_common.ps1"
|
||||
|
|
@ -15,6 +14,74 @@ $ErrorActionPreference = "Stop"
|
|||
Add-Type -AssemblyName System.Windows.Forms
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
|
||||
# Custom tray menu: hover/selected highlight + flat light chrome (default OS menu looks dead).
|
||||
if (-not ("NetxMenuRenderer" -as [type])) {
|
||||
Add-Type -ReferencedAssemblies System.Windows.Forms, System.Drawing -TypeDefinition @"
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Windows.Forms;
|
||||
|
||||
public sealed class NetxMenuColorTable : ProfessionalColorTable {
|
||||
static readonly Color Hover = Color.FromArgb(232, 240, 252);
|
||||
static readonly Color Press = Color.FromArgb(214, 228, 248);
|
||||
static readonly Color Border = Color.FromArgb(170, 198, 240);
|
||||
static readonly Color Edge = Color.FromArgb(210, 214, 220);
|
||||
static readonly Color Sep = Color.FromArgb(228, 230, 235);
|
||||
static readonly Color Bg = Color.FromArgb(252, 252, 253);
|
||||
|
||||
public override Color MenuItemSelected { get { return Hover; } }
|
||||
public override Color MenuItemSelectedGradientBegin { get { return Hover; } }
|
||||
public override Color MenuItemSelectedGradientEnd { get { return Hover; } }
|
||||
public override Color MenuItemBorder { get { return Border; } }
|
||||
public override Color MenuItemPressedGradientBegin { get { return Press; } }
|
||||
public override Color MenuItemPressedGradientMiddle { get { return Press; } }
|
||||
public override Color MenuItemPressedGradientEnd { get { return Press; } }
|
||||
public override Color MenuBorder { get { return Edge; } }
|
||||
public override Color ToolStripDropDownBackground { get { return Bg; } }
|
||||
public override Color ImageMarginGradientBegin { get { return Bg; } }
|
||||
public override Color ImageMarginGradientMiddle { get { return Bg; } }
|
||||
public override Color ImageMarginGradientEnd { get { return Bg; } }
|
||||
public override Color SeparatorDark { get { return Sep; } }
|
||||
public override Color SeparatorLight { get { return Sep; } }
|
||||
}
|
||||
|
||||
public sealed class NetxMenuRenderer : ToolStripProfessionalRenderer {
|
||||
public NetxMenuRenderer() : base(new NetxMenuColorTable()) {
|
||||
RoundedEdges = false;
|
||||
}
|
||||
|
||||
protected override void OnRenderMenuItemBackground(ToolStripItemRenderEventArgs e) {
|
||||
ToolStripMenuItem item = e.Item as ToolStripMenuItem;
|
||||
if (item == null || !item.Selected || !item.Enabled) {
|
||||
base.OnRenderMenuItemBackground(e);
|
||||
return;
|
||||
}
|
||||
Rectangle r = new Rectangle(2, 0, e.Item.Width - 4, e.Item.Height);
|
||||
using (SolidBrush brush = new SolidBrush(Color.FromArgb(232, 240, 252)))
|
||||
using (Pen pen = new Pen(Color.FromArgb(170, 198, 240))) {
|
||||
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
e.Graphics.FillRectangle(brush, r);
|
||||
e.Graphics.DrawRectangle(pen, r.X, r.Y, r.Width - 1, r.Height - 1);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnRenderToolStripBorder(ToolStripRenderEventArgs e) {
|
||||
Rectangle r = new Rectangle(0, 0, e.AffectedBounds.Width - 1, e.AffectedBounds.Height - 1);
|
||||
using (Pen pen = new Pen(Color.FromArgb(210, 214, 220))) {
|
||||
e.Graphics.DrawRectangle(pen, r);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnRenderSeparator(ToolStripSeparatorRenderEventArgs e) {
|
||||
int y = e.Item.Height / 2;
|
||||
using (Pen pen = new Pen(Color.FromArgb(228, 230, 235))) {
|
||||
e.Graphics.DrawLine(pen, 10, y, e.Item.Width - 10, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
"@
|
||||
}
|
||||
|
||||
$prog = Get-NetxProgramRoot -Override $ProgramRoot
|
||||
$data = Get-NetxDataRoot -ProgramRoot $prog -Override $DataRoot
|
||||
$hostBind = "127.0.0.1"
|
||||
|
|
@ -35,6 +102,12 @@ if ($autoVal -match '^(1|true|yes|on)$') {
|
|||
}
|
||||
$uiUrl = "http://${hostBind}:${port}/"
|
||||
$ver = Get-NetxVersion -ProgramRoot $prog
|
||||
$dbMode = Get-DbMode -EnvMap $map
|
||||
$dbModeLabel = if ($dbMode -eq "bundled") {
|
||||
if (([cultureinfo]::CurrentUICulture.Name -match '^(zh|zh-)')) { "内置库" } else { "built-in DB" }
|
||||
} else {
|
||||
if (([cultureinfo]::CurrentUICulture.Name -match '^(zh|zh-)')) { "外置库" } else { "external DB" }
|
||||
}
|
||||
$zh = ([cultureinfo]::CurrentUICulture.Name -match '^(zh|zh-)')
|
||||
function T([string]$En, [string]$Zh) {
|
||||
if ($zh) { return $Zh } else { return $En }
|
||||
|
|
@ -74,6 +147,7 @@ try {
|
|||
$startPs1 = Join-Path $PSScriptRoot "start_netx_app.ps1"
|
||||
$stopPs1 = Join-Path $PSScriptRoot "stop_netx_app.ps1"
|
||||
$checkPs1 = Join-Path $PSScriptRoot "check_update.ps1"
|
||||
$setupPs1 = Join-Path $PSScriptRoot "setup_first_run.ps1"
|
||||
|
||||
function Invoke-NetxScript {
|
||||
param(
|
||||
|
|
@ -85,6 +159,52 @@ function Invoke-NetxScript {
|
|||
-WorkingDirectory $prog -WindowStyle Hidden -PassThru -Wait:$Wait
|
||||
}
|
||||
|
||||
function Update-NetxTrayTip {
|
||||
$running = Test-NetxApiHealthy -HostName $hostBind -Port $port -TimeoutSec 1
|
||||
$state = if ($running) {
|
||||
(T "Running" "运行中")
|
||||
} else {
|
||||
(T "Stopped" "已停止")
|
||||
}
|
||||
$tip = "NetX $ver · $state · $dbModeLabel`n$uiUrl"
|
||||
if ($tip.Length -gt 63) {
|
||||
# NotifyIcon.Text max ~63 chars on older Windows.
|
||||
$tip = "NetX $ver · $state · $dbModeLabel"
|
||||
}
|
||||
$notify.Text = $tip
|
||||
}
|
||||
|
||||
function Close-NetxTrayMenu {
|
||||
# ContextMenuStrip stays modal if Click handlers block; close before dialogs/waits.
|
||||
try { $menu.Close() } catch {}
|
||||
try { [System.Windows.Forms.Application]::DoEvents() } catch {}
|
||||
}
|
||||
|
||||
function Start-NetxTrayDeferred {
|
||||
param([scriptblock]$Work)
|
||||
Close-NetxTrayMenu
|
||||
$timer = New-Object System.Windows.Forms.Timer
|
||||
$timer.Interval = 50
|
||||
$script:netxTrayDeferredWork = $Work
|
||||
$timer.add_Tick({
|
||||
$t = $script:netxTrayDeferredTimer
|
||||
try { if ($t) { $t.Stop(); $t.Dispose() } } catch {}
|
||||
$script:netxTrayDeferredTimer = $null
|
||||
$w = $script:netxTrayDeferredWork
|
||||
$script:netxTrayDeferredWork = $null
|
||||
if ($w) { & $w }
|
||||
})
|
||||
$script:netxTrayDeferredTimer = $timer
|
||||
$timer.Start()
|
||||
}
|
||||
|
||||
function Test-NetxSetupCancelled {
|
||||
param([AllowNull()][Nullable[int]]$ExitCode)
|
||||
if ($null -eq $ExitCode) { return $false }
|
||||
# STATUS_CONTROL_C_EXIT (0xC000013A): console closed / Ctrl+C — not a setup failure.
|
||||
return ([int]$ExitCode -eq -1073741510)
|
||||
}
|
||||
|
||||
function Open-NetxUiWhenReady {
|
||||
param([int]$TimeoutSec = 180)
|
||||
$notify.ShowBalloonTip(
|
||||
|
|
@ -95,14 +215,16 @@ function Open-NetxUiWhenReady {
|
|||
)
|
||||
if (Wait-NetxApiHealthy -HostName $hostBind -Port $port -TimeoutSec $TimeoutSec) {
|
||||
try { Start-Process $uiUrl } catch {}
|
||||
Update-NetxTrayTip
|
||||
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。"),
|
||||
(T "UI not ready yet. Use tray → Open UI when ready, or check data\runtime\netx.err.log." "界面尚未就绪。就绪后请用托盘「打开界面」,或查看 data\runtime\netx.err.log。"),
|
||||
[System.Windows.Forms.ToolTipIcon]::Warning
|
||||
)
|
||||
Update-NetxTrayTip
|
||||
}
|
||||
|
||||
$form = New-Object System.Windows.Forms.Form
|
||||
|
|
@ -113,7 +235,6 @@ $form.Visible = $false
|
|||
$form.Size = New-Object System.Drawing.Size(0, 0)
|
||||
|
||||
$notify = New-Object System.Windows.Forms.NotifyIcon
|
||||
$notify.Text = "NetX $ver"
|
||||
$notify.Visible = $true
|
||||
$iconPath = Join-Path $PSScriptRoot "assets\netx.ico"
|
||||
try {
|
||||
|
|
@ -125,68 +246,286 @@ try {
|
|||
} catch {
|
||||
try { $notify.Icon = [System.Drawing.SystemIcons]::Application } catch {}
|
||||
}
|
||||
Update-NetxTrayTip
|
||||
|
||||
function New-NetxMenuItem {
|
||||
param(
|
||||
[string]$Text,
|
||||
[switch]$Header,
|
||||
[switch]$Primary,
|
||||
[switch]$Muted
|
||||
)
|
||||
$item = New-Object System.Windows.Forms.ToolStripMenuItem
|
||||
$item.Text = $Text
|
||||
$item.AutoSize = $true
|
||||
$item.Padding = New-Object System.Windows.Forms.Padding(10, 5, 28, 5)
|
||||
$item.Margin = New-Object System.Windows.Forms.Padding(0)
|
||||
if ($Header) {
|
||||
$item.Enabled = $false
|
||||
$item.Font = New-Object System.Drawing.Font("Segoe UI", 9.0, [System.Drawing.FontStyle]::Bold)
|
||||
$item.ForeColor = [System.Drawing.Color]::FromArgb(55, 65, 80)
|
||||
$item.Padding = New-Object System.Windows.Forms.Padding(10, 6, 28, 2)
|
||||
} elseif ($Muted) {
|
||||
$item.Enabled = $false
|
||||
$item.Font = New-Object System.Drawing.Font("Segoe UI", 8.25)
|
||||
$item.ForeColor = [System.Drawing.Color]::FromArgb(120, 128, 140)
|
||||
$item.Padding = New-Object System.Windows.Forms.Padding(10, 1, 28, 6)
|
||||
} elseif ($Primary) {
|
||||
$item.Font = New-Object System.Drawing.Font("Segoe UI", 9.0, [System.Drawing.FontStyle]::Bold)
|
||||
$item.ForeColor = [System.Drawing.Color]::FromArgb(20, 40, 70)
|
||||
} else {
|
||||
$item.Font = New-Object System.Drawing.Font("Segoe UI", 9.0)
|
||||
$item.ForeColor = [System.Drawing.Color]::FromArgb(35, 40, 48)
|
||||
}
|
||||
return $item
|
||||
}
|
||||
|
||||
function New-NetxMenuSeparator {
|
||||
$sep = New-Object System.Windows.Forms.ToolStripSeparator
|
||||
$sep.Margin = New-Object System.Windows.Forms.Padding(0, 3, 0, 3)
|
||||
$sep.Padding = New-Object System.Windows.Forms.Padding(0)
|
||||
return $sep
|
||||
}
|
||||
|
||||
$menu = New-Object System.Windows.Forms.ContextMenuStrip
|
||||
$menu.ShowImageMargin = $false
|
||||
$menu.ShowCheckMargin = $false
|
||||
$menu.Font = New-Object System.Drawing.Font("Segoe UI", 9.0)
|
||||
$menu.BackColor = [System.Drawing.Color]::FromArgb(252, 252, 253)
|
||||
$menu.ForeColor = [System.Drawing.Color]::FromArgb(35, 40, 48)
|
||||
$menu.Padding = New-Object System.Windows.Forms.Padding(4, 4, 4, 4)
|
||||
$menu.Renderer = New-Object NetxMenuRenderer
|
||||
|
||||
$miOpen = $menu.Items.Add((T "Open UI ($uiUrl)" "打开界面 ($uiUrl)"))
|
||||
$miOpen.add_Click({ Start-Process $uiUrl })
|
||||
# --- header (info only) ---
|
||||
$miHeader = New-NetxMenuItem -Text "NetX $ver" -Header
|
||||
[void]$menu.Items.Add($miHeader)
|
||||
|
||||
$miStart = $menu.Items.Add((T "Start NetX" "启动 NetX"))
|
||||
$miSub = New-NetxMenuItem -Text "$dbModeLabel · ${hostBind}:$port" -Muted
|
||||
[void]$menu.Items.Add($miSub)
|
||||
|
||||
[void]$menu.Items.Add((New-NetxMenuSeparator))
|
||||
|
||||
# --- primary ---
|
||||
$miOpen = New-NetxMenuItem -Text (T "Open UI" "打开界面") -Primary
|
||||
$miOpen.add_Click({
|
||||
Start-NetxTrayDeferred {
|
||||
if (Test-NetxApiHealthy -HostName $hostBind -Port $port -TimeoutSec 2) {
|
||||
Start-Process $uiUrl
|
||||
} else {
|
||||
$notify.ShowBalloonTip(4000, "NetX", (T "NetX is not running. Starting…" "NetX 未运行,正在启动…"), [System.Windows.Forms.ToolTipIcon]::Info)
|
||||
Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser") | Out-Null
|
||||
Open-NetxUiWhenReady
|
||||
}
|
||||
}
|
||||
})
|
||||
[void]$menu.Items.Add($miOpen)
|
||||
|
||||
[void]$menu.Items.Add((New-NetxMenuSeparator))
|
||||
|
||||
# --- service control ---
|
||||
$miStart = New-NetxMenuItem -Text (T "Start" "启动")
|
||||
$miStart.add_Click({
|
||||
Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser") | Out-Null
|
||||
Open-NetxUiWhenReady
|
||||
Start-NetxTrayDeferred {
|
||||
Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser") | Out-Null
|
||||
Open-NetxUiWhenReady
|
||||
}
|
||||
})
|
||||
[void]$menu.Items.Add($miStart)
|
||||
|
||||
$miStop = $menu.Items.Add((T "Stop NetX (keep tray)" "停止 NetX(保留托盘)"))
|
||||
$miStop = New-NetxMenuItem -Text (T "Stop" "停止")
|
||||
$miStop.add_Click({
|
||||
$notify.ShowBalloonTip(3000, "NetX", (T "Stopping…" "正在停止…"), [System.Windows.Forms.ToolTipIcon]::Info)
|
||||
Invoke-NetxScript -File $stopPs1 | Out-Null
|
||||
Start-NetxTrayDeferred {
|
||||
$notify.ShowBalloonTip(3000, "NetX", (T "Stopping…" "正在停止…"), [System.Windows.Forms.ToolTipIcon]::Info)
|
||||
Invoke-NetxScript -File $stopPs1 | Out-Null
|
||||
Update-NetxTrayTip
|
||||
}
|
||||
})
|
||||
[void]$menu.Items.Add($miStop)
|
||||
|
||||
$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
|
||||
[void]$menu.Items.Add((New-NetxMenuSeparator))
|
||||
|
||||
# --- setup / update ---
|
||||
$miReconfig = New-NetxMenuItem -Text (T "Reconfigure database…" "重新配置数据库…")
|
||||
$miReconfig.add_Click({
|
||||
Start-NetxTrayDeferred {
|
||||
$ans = [System.Windows.Forms.MessageBox]::Show(
|
||||
(T `
|
||||
"NetX will stop while you reconfigure the database.`n`nChoose built-in or external PostgreSQL in the console window.`nContinue?" `
|
||||
"重新配置数据库时会先停止 NetX。`n`n请在随后的控制台窗口中选择内置或外置 PostgreSQL。`n是否继续?"),
|
||||
(T "Reconfigure database" "重新配置数据库"),
|
||||
[System.Windows.Forms.MessageBoxButtons]::YesNo,
|
||||
[System.Windows.Forms.MessageBoxIcon]::Question
|
||||
)
|
||||
if ($ans -ne [System.Windows.Forms.DialogResult]::Yes) { return }
|
||||
|
||||
$notify.ShowBalloonTip(3000, "NetX", (T "Stopping…" "正在停止…"), [System.Windows.Forms.ToolTipIcon]::Info)
|
||||
Invoke-NetxScript -File $stopPs1 -Wait | Out-Null
|
||||
|
||||
# ProgramData\.env is often admin-owned after Setup; repair ACL before reconfigure.
|
||||
$null = Ensure-NetxDataAcl -DataRoot $data -Quiet
|
||||
|
||||
try {
|
||||
$p = Start-NetxPowerShell -File $setupPs1 `
|
||||
-Arguments @("-ProgramRoot", $prog, "-DataRoot", $data) `
|
||||
-WorkingDirectory $prog -WindowStyle Normal -Wait -PassThru
|
||||
if (Test-NetxSetupCancelled -ExitCode $p.ExitCode) {
|
||||
$notify.ShowBalloonTip(
|
||||
4000, "NetX",
|
||||
(T "Database setup cancelled." "已取消数据库配置。"),
|
||||
[System.Windows.Forms.ToolTipIcon]::Info
|
||||
)
|
||||
Update-NetxTrayTip
|
||||
return
|
||||
}
|
||||
if ($null -ne $p.ExitCode -and $p.ExitCode -ne 0) {
|
||||
# Access denied → offer elevated re-run (UAC).
|
||||
$elev = [System.Windows.Forms.MessageBox]::Show(
|
||||
(T `
|
||||
"Database setup failed (exit $($p.ExitCode)).`n`nIf this was a permission error on %ProgramData%\NetX\.env, click Yes to retry as Administrator." `
|
||||
"数据库配置失败(退出码 $($p.ExitCode))。`n`n若是 %ProgramData%\NetX\.env 权限问题,点「是」将以管理员身份重试。"),
|
||||
"NetX",
|
||||
[System.Windows.Forms.MessageBoxButtons]::YesNo,
|
||||
[System.Windows.Forms.MessageBoxIcon]::Warning
|
||||
)
|
||||
if ($elev -eq [System.Windows.Forms.DialogResult]::Yes) {
|
||||
$arg = "-NoProfile -ExecutionPolicy Bypass -File `"$setupPs1`" -ProgramRoot `"$prog`" -DataRoot `"$data`""
|
||||
$ep = Start-Process -FilePath "powershell.exe" -ArgumentList $arg `
|
||||
-WorkingDirectory $prog -Verb RunAs -Wait -PassThru
|
||||
if (Test-NetxSetupCancelled -ExitCode $ep.ExitCode) {
|
||||
Update-NetxTrayTip
|
||||
return
|
||||
}
|
||||
if ($null -ne $ep.ExitCode -and $ep.ExitCode -ne 0) {
|
||||
[System.Windows.Forms.MessageBox]::Show(
|
||||
(T "Elevated database setup still failed (exit $($ep.ExitCode))." "管理员配置仍失败(退出码 $($ep.ExitCode))。"),
|
||||
"NetX",
|
||||
[System.Windows.Forms.MessageBoxButtons]::OK,
|
||||
[System.Windows.Forms.MessageBoxIcon]::Error
|
||||
)
|
||||
Update-NetxTrayTip
|
||||
return
|
||||
}
|
||||
} else {
|
||||
Update-NetxTrayTip
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
[System.Windows.Forms.MessageBox]::Show(
|
||||
(T "Database setup failed: $($_.Exception.Message)" "数据库配置失败:$($_.Exception.Message)"),
|
||||
"NetX",
|
||||
[System.Windows.Forms.MessageBoxButtons]::OK,
|
||||
[System.Windows.Forms.MessageBoxIcon]::Error
|
||||
)
|
||||
Update-NetxTrayTip
|
||||
return
|
||||
}
|
||||
|
||||
# Reload .env after reconfigure (host/port/mode may change).
|
||||
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 {} }
|
||||
$uiUrl = "http://${hostBind}:${port}/"
|
||||
$dbMode = Get-DbMode -EnvMap $map
|
||||
$dbModeLabel = if ($dbMode -eq "bundled") {
|
||||
(T "built-in DB" "内置库")
|
||||
} else {
|
||||
(T "external DB" "外置库")
|
||||
}
|
||||
$miSub.Text = "$dbModeLabel · ${hostBind}:$port"
|
||||
}
|
||||
|
||||
$restart = [System.Windows.Forms.MessageBox]::Show(
|
||||
(T "Database configuration saved.`nStart NetX now?" "数据库配置已保存。`n是否立即启动 NetX?"),
|
||||
"NetX",
|
||||
[System.Windows.Forms.MessageBoxButtons]::YesNo,
|
||||
[System.Windows.Forms.MessageBoxIcon]::Information
|
||||
)
|
||||
if ($restart -eq [System.Windows.Forms.DialogResult]::Yes) {
|
||||
Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser") | Out-Null
|
||||
Open-NetxUiWhenReady
|
||||
} else {
|
||||
Update-NetxTrayTip
|
||||
}
|
||||
}
|
||||
})
|
||||
[void]$menu.Items.Add($miReconfig)
|
||||
|
||||
$miUpdate = New-NetxMenuItem -Text (T "Check for updates…" "检查更新…")
|
||||
$miUpdate.add_Click({
|
||||
Start-NetxTrayDeferred {
|
||||
try {
|
||||
$p = Start-NetxPowerShell -File $checkPs1 -Arguments @("-ProgramRoot", $prog, "-DataRoot", $data) `
|
||||
-WorkingDirectory $prog -WindowStyle Normal -Wait -PassThru
|
||||
if (Test-NetxSetupCancelled -ExitCode $p.ExitCode) { return }
|
||||
if ($p.ExitCode -eq 10) {
|
||||
$ans = [System.Windows.Forms.MessageBox]::Show(
|
||||
(T "A newer NetX is available. Download and apply now?" "发现新版本,是否立即下载并更新?"),
|
||||
(T "NetX update" "NetX 更新"),
|
||||
[System.Windows.Forms.MessageBoxButtons]::YesNo,
|
||||
[System.Windows.Forms.MessageBoxIcon]::Question
|
||||
)
|
||||
if ($ans -eq [System.Windows.Forms.DialogResult]::Yes) {
|
||||
Start-NetxPowerShell -File $checkPs1 -Arguments @("-ProgramRoot", $prog, "-DataRoot", $data, "-Apply") `
|
||||
-WorkingDirectory $prog -WindowStyle Normal | Out-Null
|
||||
}
|
||||
} elseif ($p.ExitCode -eq 0) {
|
||||
[System.Windows.Forms.MessageBox]::Show(
|
||||
(T "NetX is up to date ($ver)." "已是最新版本 ($ver)。"),
|
||||
(T "NetX update" "NetX 更新")
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
[System.Windows.Forms.MessageBox]::Show(
|
||||
(T "Update check failed: $($_.Exception.Message)" "检查更新失败:$($_.Exception.Message)"),
|
||||
"NetX"
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
[void]$menu.Items.Add($miUpdate)
|
||||
|
||||
[void]$menu.Items.Add((New-NetxMenuSeparator))
|
||||
|
||||
# --- exit ---
|
||||
$miExit = New-NetxMenuItem -Text (T "Exit tray" "退出托盘")
|
||||
$miExit.ToolTipText = (T "Leave NetX services running" "NetX 服务继续运行")
|
||||
$miExit.add_Click({
|
||||
Close-NetxTrayMenu
|
||||
$notify.Visible = $false
|
||||
$form.Close()
|
||||
[System.Windows.Forms.Application]::Exit()
|
||||
})
|
||||
[void]$menu.Items.Add($miExit)
|
||||
|
||||
$miUpdate = $menu.Items.Add((T "Check for updates" "检查更新"))
|
||||
$miUpdate.add_Click({
|
||||
try {
|
||||
$p = Start-NetxPowerShell -File $checkPs1 -Arguments @("-ProgramRoot", $prog, "-DataRoot", $data) `
|
||||
-WorkingDirectory $prog -WindowStyle Normal -Wait -PassThru
|
||||
if ($p.ExitCode -eq 10) {
|
||||
$ans = [System.Windows.Forms.MessageBox]::Show(
|
||||
(T "A newer NetX is available. Download and apply now?" "发现新版本,是否立即下载并更新?"),
|
||||
(T "NetX update" "NetX 更新"),
|
||||
[System.Windows.Forms.MessageBoxButtons]::YesNo,
|
||||
[System.Windows.Forms.MessageBoxIcon]::Question
|
||||
)
|
||||
if ($ans -eq [System.Windows.Forms.DialogResult]::Yes) {
|
||||
Start-NetxPowerShell -File $checkPs1 -Arguments @("-ProgramRoot", $prog, "-DataRoot", $data, "-Apply") `
|
||||
-WorkingDirectory $prog -WindowStyle Normal | Out-Null
|
||||
}
|
||||
} elseif ($p.ExitCode -eq 0) {
|
||||
[System.Windows.Forms.MessageBox]::Show((T "NetX is up to date ($ver)." "已是最新版本 ($ver)。"), (T "NetX update" "NetX 更新"))
|
||||
}
|
||||
} catch {
|
||||
[System.Windows.Forms.MessageBox]::Show((T "Update check failed: $($_.Exception.Message)" "检查更新失败:$($_.Exception.Message)"), "NetX")
|
||||
$miStopExit = New-NetxMenuItem -Text (T "Stop and exit" "停止并退出")
|
||||
$miStopExit.add_Click({
|
||||
Start-NetxTrayDeferred {
|
||||
$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()
|
||||
}
|
||||
})
|
||||
[void]$menu.Items.Add($miStopExit)
|
||||
|
||||
$notify.ContextMenuStrip = $menu
|
||||
$notify.add_DoubleClick({
|
||||
if (Test-NetxApiHealthy -HostName $hostBind -Port $port -TimeoutSec 2) {
|
||||
Start-Process $uiUrl
|
||||
} else {
|
||||
Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser") | Out-Null
|
||||
Open-NetxUiWhenReady
|
||||
}
|
||||
})
|
||||
|
||||
[void]$menu.Items.Add("-")
|
||||
|
||||
$miExit = $menu.Items.Add((T "Exit tray (services keep running)" "退出托盘(服务继续运行)"))
|
||||
$miExit.add_Click({
|
||||
$notify.Visible = $false
|
||||
$form.Close()
|
||||
[System.Windows.Forms.Application]::Exit()
|
||||
})
|
||||
|
||||
$notify.ContextMenuStrip = $menu
|
||||
$notify.add_DoubleClick({ Start-Process $uiUrl })
|
||||
# Refresh tooltip periodically (running / stopped).
|
||||
$script:tipTimer = New-Object System.Windows.Forms.Timer
|
||||
$script:tipTimer.Interval = 5000
|
||||
$script:tipTimer.add_Tick({ Update-NetxTrayTip })
|
||||
$script:tipTimer.Start()
|
||||
|
||||
$form.add_Shown({
|
||||
if ($AutoUpdate) {
|
||||
|
|
@ -200,7 +539,6 @@ $form.add_Shown({
|
|||
-WorkingDirectory $prog -WindowStyle Hidden | Out-Null
|
||||
}
|
||||
if ($StartOnLaunch) {
|
||||
# Detach start so the tray message pump stays alive; poll /health before browser.
|
||||
Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser") | Out-Null
|
||||
$script:netxUiWaitTicks = 0
|
||||
$script:netxUiWaitMax = 360 # 360 * 500ms = 180s
|
||||
|
|
@ -212,6 +550,7 @@ $form.add_Shown({
|
|||
$script:netxUiTimer.Stop()
|
||||
$script:netxUiTimer.Dispose()
|
||||
try { Start-Process $uiUrl } catch {}
|
||||
Update-NetxTrayTip
|
||||
return
|
||||
}
|
||||
if ($script:netxUiWaitTicks -eq 1) {
|
||||
|
|
@ -231,6 +570,7 @@ $form.add_Shown({
|
|||
(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
|
||||
)
|
||||
Update-NetxTrayTip
|
||||
}
|
||||
})
|
||||
$script:netxUiTimer.Start()
|
||||
|
|
@ -238,6 +578,7 @@ $form.add_Shown({
|
|||
})
|
||||
|
||||
$form.add_FormClosing({
|
||||
try { $script:tipTimer.Stop(); $script:tipTimer.Dispose() } catch {}
|
||||
$notify.Visible = $false
|
||||
$notify.Dispose()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ function T([string]$En, [string]$Zh) {
|
|||
if ($zh) { return $Zh } else { return $En }
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
Write-Host ("==> " + (T "Program root:" "程序目录:") + " $prog")
|
||||
Write-Host ("==> " + (T "Data root:" "数据目录:") + " $data")
|
||||
Write-Host ("==> " + (T "Env file:" "环境文件:") + " $envPath")
|
||||
|
|
@ -72,34 +74,14 @@ if (Test-Path (Join-Path $distAbs "index.html")) {
|
|||
$dataEnv = Get-NetxDataEnvMap -DataRoot $data
|
||||
foreach ($k in $dataEnv.Keys) { $values[$k] = $dataEnv[$k] }
|
||||
|
||||
# Fernet key for managed-NE password encryption (required to save SSH passwords).
|
||||
# Priority: -CredentialSecretKey(File) > interactive prompt > existing .env > generate.
|
||||
# Preload credential key from file/CLI only; interactive prompt comes AFTER DB settings
|
||||
# so users are not left thinking the key alone finished setup.
|
||||
if ($CredentialSecretKeyFile) {
|
||||
if (-not (Test-Path -LiteralPath $CredentialSecretKeyFile)) {
|
||||
throw "credential_secret_key_file_missing: $CredentialSecretKeyFile"
|
||||
}
|
||||
$CredentialSecretKey = [IO.File]::ReadAllText($CredentialSecretKeyFile).Trim()
|
||||
}
|
||||
if (-not $CredentialSecretKey -and -not $NonInteractive) {
|
||||
Write-Host ""
|
||||
Write-Host (T `
|
||||
"Optional: paste NETX_CREDENTIAL_SECRET_KEY from an existing install" `
|
||||
"可选: 粘贴已有安装的 NETX_CREDENTIAL_SECRET_KEY(便于沿用已加密凭据)") -ForegroundColor Cyan
|
||||
Write-Host (T `
|
||||
"Leave empty to keep existing .env key, or auto-generate if none." `
|
||||
"留空则保留已有 .env 中的密钥;若没有则自动生成。")
|
||||
$CredentialSecretKey = (Read-Host "NETX_CREDENTIAL_SECRET_KEY").Trim()
|
||||
}
|
||||
if ($CredentialSecretKey) {
|
||||
$values["NETX_CREDENTIAL_SECRET_KEY"] = $CredentialSecretKey.Trim()
|
||||
Write-Host "==> Using provided NETX_CREDENTIAL_SECRET_KEY" -ForegroundColor Green
|
||||
} elseif ($existing.ContainsKey("NETX_CREDENTIAL_SECRET_KEY") -and $existing["NETX_CREDENTIAL_SECRET_KEY"]) {
|
||||
$values["NETX_CREDENTIAL_SECRET_KEY"] = $existing["NETX_CREDENTIAL_SECRET_KEY"]
|
||||
Write-Host "==> Keeping existing NETX_CREDENTIAL_SECRET_KEY from .env" -ForegroundColor Green
|
||||
} else {
|
||||
$values["NETX_CREDENTIAL_SECRET_KEY"] = New-NetxFernetKey
|
||||
Write-Host "==> Generated NETX_CREDENTIAL_SECRET_KEY (saved in .env)" -ForegroundColor Green
|
||||
}
|
||||
|
||||
if ($DbMode -eq "bundled") {
|
||||
$pgsql = Join-Path $prog "postgres\pgsql"
|
||||
|
|
@ -230,11 +212,14 @@ if ($DbMode -eq "bundled") {
|
|||
$ExternalDatabaseUrl = [IO.File]::ReadAllText($ExternalDatabaseUrlFile).Trim()
|
||||
}
|
||||
if (-not $ExternalDatabaseUrl) {
|
||||
if ($existing.ContainsKey("NETX_DATABASE_URL") -and $existing["NETX_DATABASE_URL"]) {
|
||||
$ExternalDatabaseUrl = $existing["NETX_DATABASE_URL"]
|
||||
} elseif ($NonInteractive) {
|
||||
throw "external_url_required"
|
||||
if ($NonInteractive) {
|
||||
if ($existing.ContainsKey("NETX_DATABASE_URL") -and $existing["NETX_DATABASE_URL"]) {
|
||||
$ExternalDatabaseUrl = $existing["NETX_DATABASE_URL"]
|
||||
} else {
|
||||
throw "external_url_required"
|
||||
}
|
||||
} else {
|
||||
# Interactive: always ask. Empty = keep existing URL (never silent).
|
||||
Write-Host ""
|
||||
Write-Host (T `
|
||||
"Enter PostgreSQL URL (database/role must already exist):" `
|
||||
|
|
@ -243,7 +228,21 @@ if ($DbMode -eq "bundled") {
|
|||
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 ($existing.ContainsKey("NETX_DATABASE_URL") -and $existing["NETX_DATABASE_URL"]) {
|
||||
Write-Host (T `
|
||||
"Current: $($existing['NETX_DATABASE_URL'])" `
|
||||
"当前: $($existing['NETX_DATABASE_URL'])") -ForegroundColor DarkGray
|
||||
Write-Host (T `
|
||||
"Press Enter to keep the current URL, or paste a new one." `
|
||||
"直接回车 = 保留当前连接串;或粘贴新的连接串。")
|
||||
}
|
||||
$entered = (Read-Host "NETX_DATABASE_URL").Trim()
|
||||
if ($entered) {
|
||||
$ExternalDatabaseUrl = $entered
|
||||
} elseif ($existing.ContainsKey("NETX_DATABASE_URL") -and $existing["NETX_DATABASE_URL"]) {
|
||||
$ExternalDatabaseUrl = $existing["NETX_DATABASE_URL"]
|
||||
Write-Host (T "==> Keeping existing NETX_DATABASE_URL" "==> 保留已有 NETX_DATABASE_URL") -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $ExternalDatabaseUrl) {
|
||||
|
|
@ -266,29 +265,61 @@ if ($DbMode -eq "bundled") {
|
|||
$h = $Matches[3]
|
||||
$po = if ($Matches[4]) { [int]$Matches[4] } else { 5432 }
|
||||
$dbn = [uri]::UnescapeDataString($Matches[5])
|
||||
Write-Host "==> Probing external PostgreSQL ${h}:${po}/${dbn} ..."
|
||||
if (Test-Path $testHelper) {
|
||||
Write-Host "==> Probing external PostgreSQL..."
|
||||
$probeErr = Join-Path $env:TEMP ("netx-pg-probe-" + [Guid]::NewGuid().ToString("n") + ".txt")
|
||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $testHelper `
|
||||
-PgHost $h -Port $po -User $u -Password $pw -Database $dbn -PsqlPath $psqlProbe
|
||||
-PgHost $h -Port $po -User $u -Password $pw -Database $dbn `
|
||||
-PsqlPath $psqlProbe -OutErrFile $probeErr
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "external_db_probe_failed"
|
||||
$detail = if (Test-Path $probeErr) { (Get-Content -LiteralPath $probeErr -Raw) } else { "exit $LASTEXITCODE" }
|
||||
Remove-Item -LiteralPath $probeErr -Force -ErrorAction SilentlyContinue
|
||||
throw (T "external_db_probe_failed: $detail" "外置数据库连接失败: $detail")
|
||||
}
|
||||
Remove-Item -LiteralPath $probeErr -Force -ErrorAction SilentlyContinue
|
||||
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"
|
||||
throw (T "external_db_probe_failed: $ping" "外置数据库连接失败: $ping")
|
||||
}
|
||||
} finally {
|
||||
Remove-Item Env:PGPASSWORD -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Host (T `
|
||||
"[WARN] Skipped connection probe (no bundled psql or URL parse failed)." `
|
||||
"[WARN] 已跳过连接探测(缺少捆绑 psql 或连接串无法解析)。") -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Fernet key AFTER database prompts (interactive UX).
|
||||
# Priority: CLI/file > interactive prompt > existing .env > generate.
|
||||
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
|
||||
}
|
||||
|
||||
Write-DotEnvValue -Path $envPath -Values $values
|
||||
Write-Host ""
|
||||
Write-Host "Wrote $envPath" -ForegroundColor Green
|
||||
|
|
@ -327,3 +358,14 @@ if (-not $NonInteractive) {
|
|||
} else {
|
||||
Write-Host "Next: .\packaging\start_netx_app.ps1 or NetX-Tray.cmd"
|
||||
}
|
||||
|
||||
} catch {
|
||||
Write-Host ""
|
||||
Write-Host ("[ERR] " + $_.Exception.Message) -ForegroundColor Red
|
||||
if (-not $NonInteractive) {
|
||||
try {
|
||||
[void](Read-Host (T "Press Enter to close" "按回车关闭窗口"))
|
||||
} catch {}
|
||||
}
|
||||
exit 1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||
|
||||
[project]
|
||||
name = "netx-ops"
|
||||
version = "0.4.6"
|
||||
version = "0.4.7"
|
||||
description = "netx operations tool: alarm-centric workflows with REST API and stdio MCP"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue