mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 02:00:46 +08:00
Poll external processes instead of Start-Process -Wait on the UI thread, and restart the tray after a successful update. Co-authored-by: Cursor <cursoragent@cursor.com>
747 lines
32 KiB
PowerShell
747 lines
32 KiB
PowerShell
param(
|
||
[string]$ProgramRoot = "",
|
||
[string]$DataRoot = "",
|
||
[switch]$StartOnLaunch = $true,
|
||
[switch]$CheckUpdateOnLaunch = $false,
|
||
[switch]$AutoUpdate = $false
|
||
)
|
||
|
||
# System-tray controller for NetX (Windows packaged installs).
|
||
|
||
$ErrorActionPreference = "Stop"
|
||
. "$PSScriptRoot\_common.ps1"
|
||
|
||
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"
|
||
$port = 8890
|
||
$envPath = Join-Path $data ".env"
|
||
$map = @{}
|
||
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 {} }
|
||
}
|
||
$autoVal = ""
|
||
if ($map["NETX_UPDATE_AUTO"]) { $autoVal = $map["NETX_UPDATE_AUTO"] }
|
||
elseif ($env:NETX_UPDATE_AUTO) { $autoVal = $env:NETX_UPDATE_AUTO }
|
||
if ($autoVal -match '^(1|true|yes|on)$') {
|
||
$AutoUpdate = $true
|
||
$CheckUpdateOnLaunch = $true
|
||
}
|
||
$uiUrl = "http://${hostBind}:${port}/"
|
||
$script:ver = Get-NetxVersion -ProgramRoot $prog
|
||
$ver = $script:ver
|
||
$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 }
|
||
}
|
||
|
||
if (-not (Test-Path $envPath)) {
|
||
[void][System.Windows.Forms.MessageBox]::Show(
|
||
(T `
|
||
"NetX is not configured yet (missing $envPath).`nRe-run the installer and choose built-in or external DB,`nor use Start Menu → NetX → Reconfigure database." `
|
||
"尚未完成数据库配置(缺少 $envPath)。`n请重新运行安装向导并选择内置或外置数据库,`n或使用「开始菜单 → NetX → 重新配置数据库」。"),
|
||
"NetX",
|
||
[System.Windows.Forms.MessageBoxButtons]::OK,
|
||
[System.Windows.Forms.MessageBoxIcon]::Warning
|
||
)
|
||
exit 1
|
||
}
|
||
|
||
# Only one tray icon per machine session (desktop + postinstall + autostart can race).
|
||
$script:netxTrayMutex = $null
|
||
try {
|
||
$createdNew = $false
|
||
$script:netxTrayMutex = New-Object System.Threading.Mutex($true, "Local\NetX.WinTray.SingleInstance", [ref]$createdNew)
|
||
if (-not $createdNew) {
|
||
if (Test-NetxApiHealthy -HostName $hostBind -Port $port -TimeoutSec 2) {
|
||
try { Start-Process $uiUrl } catch {}
|
||
} elseif ($StartOnLaunch) {
|
||
Start-NetxPowerShell -File (Join-Path $PSScriptRoot "start_netx_app.ps1") `
|
||
-Arguments @("-ProgramRoot", $prog, "-DataRoot", $data, "-SkipBrowser") `
|
||
-WorkingDirectory $prog -WindowStyle Hidden | Out-Null
|
||
}
|
||
exit 0
|
||
}
|
||
} catch {
|
||
# If mutex fails, continue with a tray (better than no UI control).
|
||
}
|
||
|
||
$startPs1 = Join-Path $PSScriptRoot "start_netx_app.ps1"
|
||
$stopPs1 = Join-Path $PSScriptRoot "stop_netx_app.ps1"
|
||
$checkPs1 = Join-Path $PSScriptRoot "check_update.ps1"
|
||
$setupPs1 = Join-Path $PSScriptRoot "setup_first_run.ps1"
|
||
|
||
function Invoke-NetxScript {
|
||
param(
|
||
[string]$File,
|
||
[string[]]$ExtraArgs = @(),
|
||
[switch]$Wait = $false
|
||
)
|
||
return Start-NetxPowerShell -File $File -Arguments (@("-ProgramRoot", $prog, "-DataRoot", $data) + $ExtraArgs) `
|
||
-WorkingDirectory $prog -WindowStyle Hidden -PassThru -Wait:$Wait
|
||
}
|
||
|
||
function Update-NetxTrayTip {
|
||
# Refresh version after in-place updates (tray process may outlive version.json).
|
||
try {
|
||
$nowVer = Get-NetxVersion -ProgramRoot $prog
|
||
if ($nowVer -and $nowVer -ne $script:ver) {
|
||
$script:ver = $nowVer
|
||
if ($null -ne $script:miHeader) { $script:miHeader.Text = "NetX $script:ver" }
|
||
}
|
||
} catch {}
|
||
$running = Test-NetxApiHealthy -HostName $hostBind -Port $port -TimeoutSec 1
|
||
$state = if ($running) {
|
||
(T "Running" "运行中")
|
||
} else {
|
||
(T "Stopped" "已停止")
|
||
}
|
||
$v = $script:ver
|
||
$tip = "NetX $v · $state · $dbModeLabel`n$uiUrl"
|
||
if ($tip.Length -gt 63) {
|
||
# NotifyIcon.Text max ~63 chars on older Windows.
|
||
$tip = "NetX $v · $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 Start-NetxTrayWatchProcess {
|
||
# Never Start-Process -Wait on the WinForms UI thread — it freezes the tray menu.
|
||
param(
|
||
[Parameter(Mandatory = $true)]$Process,
|
||
[scriptblock]$OnExit
|
||
)
|
||
if ($null -eq $Process) {
|
||
if ($OnExit) { & $OnExit $null }
|
||
return
|
||
}
|
||
$script:netxWatchProc = $Process
|
||
$script:netxWatchOnExit = $OnExit
|
||
if ($script:netxWatchTimer) {
|
||
try { $script:netxWatchTimer.Stop(); $script:netxWatchTimer.Dispose() } catch {}
|
||
}
|
||
$timer = New-Object System.Windows.Forms.Timer
|
||
$timer.Interval = 400
|
||
$timer.add_Tick({
|
||
$p = $script:netxWatchProc
|
||
if ($null -eq $p) {
|
||
try { $script:netxWatchTimer.Stop(); $script:netxWatchTimer.Dispose() } catch {}
|
||
$script:netxWatchTimer = $null
|
||
return
|
||
}
|
||
$done = $false
|
||
try { $done = [bool]$p.HasExited } catch { $done = $true }
|
||
if (-not $done) { return }
|
||
try { $script:netxWatchTimer.Stop(); $script:netxWatchTimer.Dispose() } catch {}
|
||
$script:netxWatchTimer = $null
|
||
$cb = $script:netxWatchOnExit
|
||
$script:netxWatchOnExit = $null
|
||
$script:netxWatchProc = $null
|
||
if ($cb) { & $cb $p }
|
||
})
|
||
$script:netxWatchTimer = $timer
|
||
$timer.Start()
|
||
}
|
||
|
||
function Restart-NetxTraySelf {
|
||
# Fresh process picks up new version.json / scripts after in-place update.
|
||
try {
|
||
$notify.ShowBalloonTip(
|
||
2500, "NetX",
|
||
(T "Restarting tray…" "正在重启托盘…"),
|
||
[System.Windows.Forms.ToolTipIcon]::Info
|
||
)
|
||
} catch {}
|
||
$trayFile = Join-Path $PSScriptRoot "netx_tray.ps1"
|
||
$arg = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$trayFile`" -ProgramRoot `"$prog`" -DataRoot `"$data`""
|
||
try {
|
||
Start-Process -FilePath "powershell.exe" -ArgumentList $arg -WindowStyle Hidden | Out-Null
|
||
} catch {}
|
||
try { $notify.Visible = $false } catch {}
|
||
try { $form.Close() } catch {}
|
||
[System.Windows.Forms.Application]::Exit()
|
||
}
|
||
|
||
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 Complete-NetxTrayReconfig {
|
||
# Reload .env after reconfigure (host/port/mode may change).
|
||
if (Test-Path $envPath) {
|
||
$map = Read-DotEnv -Path $envPath
|
||
if ($map["NETX_HOST"]) { $script:hostBind = $map["NETX_HOST"]; $hostBind = $script:hostBind }
|
||
if ($map["NETX_PORT"]) {
|
||
try {
|
||
$script:port = [int]$map["NETX_PORT"]
|
||
$port = $script:port
|
||
} catch {}
|
||
}
|
||
$script:uiUrl = "http://${hostBind}:${port}/"
|
||
$uiUrl = $script:uiUrl
|
||
$dbMode = Get-DbMode -EnvMap $map
|
||
$script:dbModeLabel = if ($dbMode -eq "bundled") {
|
||
(T "built-in DB" "内置库")
|
||
} else {
|
||
(T "external DB" "外置库")
|
||
}
|
||
$dbModeLabel = $script:dbModeLabel
|
||
if ($miSub) { $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
|
||
}
|
||
}
|
||
|
||
function Open-NetxUiWhenReady {
|
||
param([int]$TimeoutSec = 180)
|
||
$notify.ShowBalloonTip(
|
||
5000,
|
||
"NetX",
|
||
(T "Starting… first run may take a minute." "正在启动…首次迁移可能需要一分钟。"),
|
||
[System.Windows.Forms.ToolTipIcon]::Info
|
||
)
|
||
# Poll on a timer — do not block the tray UI thread with Wait-NetxApiHealthy.
|
||
if ($script:netxUiReadyTimer) {
|
||
try { $script:netxUiReadyTimer.Stop(); $script:netxUiReadyTimer.Dispose() } catch {}
|
||
}
|
||
$script:netxUiReadyTicks = 0
|
||
$script:netxUiReadyMax = [Math]::Max(1, [int]($TimeoutSec * 2))
|
||
$timer = New-Object System.Windows.Forms.Timer
|
||
$timer.Interval = 500
|
||
$timer.add_Tick({
|
||
$script:netxUiReadyTicks++
|
||
if (Test-NetxApiHealthy -HostName $hostBind -Port $port -TimeoutSec 2) {
|
||
try { $script:netxUiReadyTimer.Stop(); $script:netxUiReadyTimer.Dispose() } catch {}
|
||
$script:netxUiReadyTimer = $null
|
||
try { Start-Process $uiUrl } catch {}
|
||
Update-NetxTrayTip
|
||
return
|
||
}
|
||
if ($script:netxUiReadyTicks -ge $script:netxUiReadyMax) {
|
||
try { $script:netxUiReadyTimer.Stop(); $script:netxUiReadyTimer.Dispose() } catch {}
|
||
$script:netxUiReadyTimer = $null
|
||
$notify.ShowBalloonTip(
|
||
8000,
|
||
"NetX",
|
||
(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
|
||
}
|
||
})
|
||
$script:netxUiReadyTimer = $timer
|
||
$timer.Start()
|
||
}
|
||
|
||
$form = New-Object System.Windows.Forms.Form
|
||
$form.Text = "NetX"
|
||
$form.ShowInTaskbar = $false
|
||
$form.WindowState = "Minimized"
|
||
$form.Visible = $false
|
||
$form.Size = New-Object System.Drawing.Size(0, 0)
|
||
|
||
$notify = New-Object System.Windows.Forms.NotifyIcon
|
||
$notify.Visible = $true
|
||
$iconPath = Join-Path $PSScriptRoot "assets\netx.ico"
|
||
try {
|
||
if (Test-Path $iconPath) {
|
||
$notify.Icon = New-Object System.Drawing.Icon $iconPath
|
||
} else {
|
||
$notify.Icon = [System.Drawing.SystemIcons]::Application
|
||
}
|
||
} 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
|
||
|
||
# --- header (info only) ---
|
||
$script:miHeader = New-NetxMenuItem -Text "NetX $script:ver" -Header
|
||
$miHeader = $script:miHeader
|
||
[void]$menu.Items.Add($miHeader)
|
||
|
||
$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({
|
||
Start-NetxTrayDeferred {
|
||
Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser") | Out-Null
|
||
Open-NetxUiWhenReady
|
||
}
|
||
})
|
||
[void]$menu.Items.Add($miStart)
|
||
|
||
$miStop = New-NetxMenuItem -Text (T "Stop" "停止")
|
||
$miStop.add_Click({
|
||
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)
|
||
|
||
[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 | 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 -PassThru
|
||
Start-NetxTrayWatchProcess -Process $p -OnExit {
|
||
param($sp)
|
||
if ($null -eq $sp) { Update-NetxTrayTip; return }
|
||
if (Test-NetxSetupCancelled -ExitCode $sp.ExitCode) {
|
||
$notify.ShowBalloonTip(
|
||
4000, "NetX",
|
||
(T "Database setup cancelled." "已取消数据库配置。"),
|
||
[System.Windows.Forms.ToolTipIcon]::Info
|
||
)
|
||
Update-NetxTrayTip
|
||
return
|
||
}
|
||
if ($null -ne $sp.ExitCode -and $sp.ExitCode -ne 0) {
|
||
$elev = [System.Windows.Forms.MessageBox]::Show(
|
||
(T `
|
||
"Database setup failed (exit $($sp.ExitCode)).`n`nIf this was a permission error on %ProgramData%\NetX\.env, click Yes to retry as Administrator." `
|
||
"数据库配置失败(退出码 $($sp.ExitCode))。`n`n若是 %ProgramData%\NetX\.env 权限问题,点「是」将以管理员身份重试。"),
|
||
"NetX",
|
||
[System.Windows.Forms.MessageBoxButtons]::YesNo,
|
||
[System.Windows.Forms.MessageBoxIcon]::Warning
|
||
)
|
||
if ($elev -ne [System.Windows.Forms.DialogResult]::Yes) {
|
||
Update-NetxTrayTip
|
||
return
|
||
}
|
||
$arg = "-NoProfile -ExecutionPolicy Bypass -File `"$setupPs1`" -ProgramRoot `"$prog`" -DataRoot `"$data`""
|
||
try {
|
||
$ep = Start-Process -FilePath "powershell.exe" -ArgumentList $arg `
|
||
-WorkingDirectory $prog -Verb RunAs -PassThru
|
||
} catch {
|
||
Update-NetxTrayTip
|
||
return
|
||
}
|
||
Start-NetxTrayWatchProcess -Process $ep -OnExit {
|
||
param($ep2)
|
||
if ($null -eq $ep2 -or (Test-NetxSetupCancelled -ExitCode $ep2.ExitCode)) {
|
||
Update-NetxTrayTip
|
||
return
|
||
}
|
||
if ($null -ne $ep2.ExitCode -and $ep2.ExitCode -ne 0) {
|
||
[System.Windows.Forms.MessageBox]::Show(
|
||
(T "Elevated database setup still failed (exit $($ep2.ExitCode))." "管理员配置仍失败(退出码 $($ep2.ExitCode))。"),
|
||
"NetX",
|
||
[System.Windows.Forms.MessageBoxButtons]::OK,
|
||
[System.Windows.Forms.MessageBoxIcon]::Error
|
||
)
|
||
Update-NetxTrayTip
|
||
return
|
||
}
|
||
Complete-NetxTrayReconfig
|
||
}
|
||
return
|
||
}
|
||
Complete-NetxTrayReconfig
|
||
}
|
||
} 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
|
||
}
|
||
}
|
||
})
|
||
[void]$menu.Items.Add($miReconfig)
|
||
|
||
$miUpdate = New-NetxMenuItem -Text (T "Check for updates…" "检查更新…")
|
||
$miUpdate.add_Click({
|
||
Start-NetxTrayDeferred {
|
||
try {
|
||
$notify.ShowBalloonTip(
|
||
3000, "NetX",
|
||
(T "Checking for updates…" "正在检查更新…"),
|
||
[System.Windows.Forms.ToolTipIcon]::Info
|
||
)
|
||
# No -Wait on UI thread (freezes ContextMenuStrip / tray).
|
||
$p = Start-NetxPowerShell -File $checkPs1 -Arguments @("-ProgramRoot", $prog, "-DataRoot", $data) `
|
||
-WorkingDirectory $prog -WindowStyle Normal -PassThru
|
||
Start-NetxTrayWatchProcess -Process $p -OnExit {
|
||
param($cp)
|
||
try {
|
||
if ($null -eq $cp) { return }
|
||
if (Test-NetxSetupCancelled -ExitCode $cp.ExitCode) { return }
|
||
if ($cp.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 -ne [System.Windows.Forms.DialogResult]::Yes) { return }
|
||
$arg = "-NoProfile -ExecutionPolicy Bypass -File `"$checkPs1`" -ProgramRoot `"$prog`" -DataRoot `"$data`" -Apply"
|
||
$notify.ShowBalloonTip(
|
||
5000, "NetX",
|
||
(T "Downloading / applying update… keep the console open." "正在下载/应用更新…请勿关闭控制台窗口。"),
|
||
[System.Windows.Forms.ToolTipIcon]::Info
|
||
)
|
||
try {
|
||
$ap = Start-Process -FilePath "powershell.exe" -ArgumentList $arg `
|
||
-WorkingDirectory $prog -WindowStyle Normal -Verb RunAs -PassThru
|
||
} catch {
|
||
$notify.ShowBalloonTip(4000, "NetX", (T "Update cancelled (UAC)." "已取消更新(UAC)。"), [System.Windows.Forms.ToolTipIcon]::Info)
|
||
return
|
||
}
|
||
Start-NetxTrayWatchProcess -Process $ap -OnExit {
|
||
param($ap2)
|
||
if ($null -eq $ap2) { return }
|
||
if (Test-NetxSetupCancelled -ExitCode $ap2.ExitCode) {
|
||
$notify.ShowBalloonTip(4000, "NetX", (T "Update cancelled." "已取消更新。"), [System.Windows.Forms.ToolTipIcon]::Info)
|
||
return
|
||
}
|
||
if ($null -ne $ap2.ExitCode -and $ap2.ExitCode -eq 0) {
|
||
$newVer = Get-NetxVersion -ProgramRoot $prog
|
||
[System.Windows.Forms.MessageBox]::Show(
|
||
(T "Updated to $newVer.`nTray will restart." "已更新到 $newVer。`n即将重启托盘。"),
|
||
(T "NetX update" "NetX 更新"),
|
||
[System.Windows.Forms.MessageBoxButtons]::OK,
|
||
[System.Windows.Forms.MessageBoxIcon]::Information
|
||
)
|
||
Restart-NetxTraySelf
|
||
return
|
||
}
|
||
[System.Windows.Forms.MessageBox]::Show(
|
||
(T "Update failed (exit $($ap2.ExitCode)). Re-run Check for updates, or install NetX-Setup manually." "更新失败(退出码 $($ap2.ExitCode))。请重试「检查更新」,或手动运行 Setup 安装包。"),
|
||
(T "NetX update" "NetX 更新"),
|
||
[System.Windows.Forms.MessageBoxButtons]::OK,
|
||
[System.Windows.Forms.MessageBoxIcon]::Warning
|
||
)
|
||
Update-NetxTrayTip
|
||
}
|
||
} elseif ($cp.ExitCode -eq 0) {
|
||
[System.Windows.Forms.MessageBox]::Show(
|
||
(T "NetX is up to date ($script:ver)." "已是最新版本 ($script:ver)。"),
|
||
(T "NetX update" "NetX 更新")
|
||
)
|
||
}
|
||
} catch {
|
||
[System.Windows.Forms.MessageBox]::Show(
|
||
(T "Update check failed: $($_.Exception.Message)" "检查更新失败:$($_.Exception.Message)"),
|
||
"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)
|
||
|
||
$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
|
||
}
|
||
})
|
||
|
||
# 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) {
|
||
$notify.ShowBalloonTip(4000, "NetX", (T "Checking for updates…" "正在检查更新…"), [System.Windows.Forms.ToolTipIcon]::Info)
|
||
Start-NetxPowerShell -File $checkPs1 `
|
||
-Arguments @("-ProgramRoot", $prog, "-DataRoot", $data, "-Apply", "-Quiet", "-AutoOnly") `
|
||
-WorkingDirectory $prog -WindowStyle Hidden -Wait | Out-Null
|
||
} elseif ($CheckUpdateOnLaunch) {
|
||
Start-NetxPowerShell -File $checkPs1 `
|
||
-Arguments @("-ProgramRoot", $prog, "-DataRoot", $data, "-Quiet") `
|
||
-WorkingDirectory $prog -WindowStyle Hidden | Out-Null
|
||
}
|
||
if ($StartOnLaunch) {
|
||
Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser") | Out-Null
|
||
$script:netxUiWaitTicks = 0
|
||
$script:netxUiWaitMax = 360 # 360 * 500ms = 180s
|
||
$script:netxUiTimer = New-Object System.Windows.Forms.Timer
|
||
$script:netxUiTimer.Interval = 500
|
||
$script:netxUiTimer.add_Tick({
|
||
$script:netxUiWaitTicks++
|
||
if (Test-NetxApiHealthy -HostName $hostBind -Port $port -TimeoutSec 2) {
|
||
$script:netxUiTimer.Stop()
|
||
$script:netxUiTimer.Dispose()
|
||
try { Start-Process $uiUrl } catch {}
|
||
Update-NetxTrayTip
|
||
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
|
||
)
|
||
Update-NetxTrayTip
|
||
}
|
||
})
|
||
$script:netxUiTimer.Start()
|
||
}
|
||
})
|
||
|
||
$form.add_FormClosing({
|
||
try { $script:tipTimer.Stop(); $script:tipTimer.Dispose() } catch {}
|
||
$notify.Visible = $false
|
||
$notify.Dispose()
|
||
})
|
||
|
||
[System.Windows.Forms.Application]::Run($form)
|