Modernize the Windows installer, tray and runtime control; fix lifecycle bugs.

Installer: dynamic light/dark Win11 style, new brand wizard images, auto
language, exclusive run mode (tray / Windows service) plus autostart and
auto-update on the Tasks page (pre-selected from what is installed on
upgrades, untouched by silent updates), 5 localized Start Menu entries.

Tray: theme-aware rounded menu with live status, icons and status-dot tray
icon; health polled off the UI thread; Settings submenu toggles autostart
and daily updates; browser opened de-elevated via explorer.

Fixes:
- tray Stop/Start was ignored in service mode (service_run restarted the
  children) - all start/stop now goes through runtime_ctl.ps1
- logon tray task and SYSTEM service task were killed after 72h (default
  ExecutionTimeLimit); autostart was registered for the elevating account
  and opened a browser on every logon
- uninstall left the NetXTray/NetXUpdate tasks behind
- silent auto-update killed the tray without restarting it
- repair_venv.ps1 was never staged into the release
- wildcard NETX_HOST (0.0.0.0) broke health checks
- ICO frames were PNG, which System.Drawing decodes as noise; icons now
  use the current brand mark

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
oliver 2026-10-13 20:32:33 +08:00
parent 4c4c7d91d0
commit e21719b66c
27 changed files with 1209 additions and 687 deletions

View file

@ -61,7 +61,10 @@ Optional local zip only: `build_release.ps1 -CreateZip` (not for release upload)
- **Reinstall** — same Database wizard as first install (`ApplyDatabaseConfig`); does **not** delete ProgramData (use Uninstall → delete data for a wipe). - **Reinstall** — same Database wizard as first install (`ApplyDatabaseConfig`); does **not** delete ProgramData (use Uninstall → delete data for a wipe).
4. Files → `%ProgramFiles%\NetX\` (program root). 4. Files → `%ProgramFiles%\NetX\` (program root).
5. Data → `%ProgramData%\NetX\` (`.env`, `pgdata`, spool, secrets). 5. Data → `%ProgramData%\NetX\` (`.env`, `pgdata`, spool, secrets).
6. Start menu: **Start NetX** / **Stop NetX** / **Open NetX UI** / **Reconfigure database**. 6. **Tasks page — run mode (exclusive):** *Desktop tray* (default; optional *start when I sign in* → `\NetX\NetXTray` logon task for local Administrators, quiet start without a browser) or *Windows service* (WinSW, runs at boot). Optional *daily automatic updates* (`\NetX\NetXUpdate`). On upgrades the page pre-selects what is actually installed; silent installs leave these alone unless `/TASKS=` is passed (e.g. `/TASKS="runservice,autoupdate"`).
7. Start menu: **NetX** (tray) / **Start NetX** / **Stop NetX** / **Check for updates** / **Reconfigure database** (names follow the Setup language). Autostart and auto-update can also be toggled later from tray → Settings.
Start/stop from the tray or shortcuts go through `packaging\runtime_ctl.ps1`, which drives the Windows service / SYSTEM task when one is installed (stopping its children directly would just get them restarted). Uninstall removes the service and all `\NetX\` scheduled tasks.
**Elevation (required on fresh PCs):** all end-user entry points (`NetX-Start.cmd`, `NetX-Tray.cmd`, `NetX-Stop.cmd`, `NetX-FirstRun.cmd`, tray, start/stop/setup/update) **self-elevate via UAC**. Program Files installs cannot start reliably as a normal user (`.venv` repair / writes). Approve the UAC prompt once per launch (tray stays elevated afterward). **Elevation (required on fresh PCs):** all end-user entry points (`NetX-Start.cmd`, `NetX-Tray.cmd`, `NetX-Stop.cmd`, `NetX-FirstRun.cmd`, tray, start/stop/setup/update) **self-elevate via UAC**. Program Files installs cannot start reliably as a normal user (`.venv` repair / writes). Approve the UAC prompt once per launch (tray stays elevated afterward).

View file

@ -282,6 +282,37 @@ function Test-NetxApiHealthy {
} }
} }
function Get-NetxClientHost {
param([string]$BindHost = "")
# A wildcard bind address is not connectable on Windows; probe/open via loopback.
$h = "$BindHost".Trim()
if (-not $h -or $h -in @("0.0.0.0", "::", "[::]", "*")) { return "127.0.0.1" }
return $h
}
function Open-NetxUrl {
param([Parameter(Mandatory = $true)][string]$Url)
# Packaged scripts run elevated; Start-Process <url> would launch the browser
# elevated too. explorer.exe hands the URL to the user's non-elevated shell.
try {
Start-Process -FilePath (Join-Path $env:SystemRoot "explorer.exe") -ArgumentList "`"$Url`"" | Out-Null
return
} catch {}
try { Start-Process $Url | Out-Null } catch {}
}
function Get-NetxManagedRunner {
# "service" (WinSW) / "task" (SYSTEM startup task) own the runtime when present;
# killing their children directly would just make service_run.ps1 restart them.
$svc = Get-Service -Name "NetX" -ErrorAction SilentlyContinue
if ($svc -and [string]$svc.StartType -ne "Disabled") { return "service" }
try {
$t = Get-ScheduledTask -TaskName "NetXService" -TaskPath "\NetX\" -ErrorAction Stop
if ([string]$t.State -ne "Disabled") { return "task" }
} catch {}
return ""
}
function Wait-NetxApiHealthy { function Wait-NetxApiHealthy {
param( param(
[string]$HostName = "127.0.0.1", [string]$HostName = "127.0.0.1",

View file

@ -1,6 +1,6 @@
# NetX packaging assets # NetX packaging assets
- `netx.ico` — installer / shortcuts / tray (regenerate with `python gen_icon.py`) - `netx.ico` 鈥?installer / shortcuts / tray (regenerate with `python gen_icon.py`)
- `netx-256.png` / `netx-64.png` — previews - `netx-256.png` / `netx-64.png` 鈥?previews
Mark matches `web/public/favicon.svg` (network “N” nodes on navy). Mark matches `web/public/favicon.svg` (network 鈥淣鈥?nodes on navy).

View file

@ -1,54 +1,113 @@
"""Generate NetX Windows .ico from brand mark (matches web/public/favicon.svg).""" """Generate NetX Windows .ico/.png from the brand mark.
Mirrors web/public/favicon.svg and web/src/layout/BrandMark.tsx (24x24 viewBox):
blue gradient tile, an "X" of links, four nodes around a hub, one node lit cyan.
"""
from __future__ import annotations from __future__ import annotations
import os import os
from PIL import Image, ImageDraw from PIL import Image, ImageDraw
OUT_DIR = os.path.dirname(os.path.abspath(__file__)) OUT_DIR = os.path.dirname(os.path.abspath(__file__))
BG = (15, 39, 68, 255) GRAD_A = (0x3B, 0x82, 0xF6) # #3b82f6 (top-left)
LINE = (126, 182, 232, 255) GRAD_B = (0x1D, 0x4E, 0xD8) # #1d4ed8 (bottom-right)
NODE = (232, 242, 252, 255) WHITE = (255, 255, 255, 255)
ACCENT = (61, 130, 247, 255) LINK = (255, 255, 255, int(255 * 0.55))
LIVE = (0x67, 0xE8, 0xF9, 255) # #67e8f9
SS = 4 # supersample factor
def make(size: int) -> Image.Image: def _gradient(px: int) -> Image.Image:
img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) # Diagonal gradient along (0,0)->(24,24), like the SVG userSpaceOnUse gradient.
d = ImageDraw.Draw(img) img = Image.new("RGBA", (px, px))
radius = max(2, int(size * 0.22)) pix = img.load()
d.rounded_rectangle([0, 0, size - 1, size - 1], radius=radius, fill=BG) span = 2 * (px - 1) or 1
s = size / 32.0 for y in range(px):
for x in range(px):
def xy(x: float, y: float) -> tuple[float, float]: t = (x + y) / span
return (x * s, y * s) pix[x, y] = tuple(round(a + (b - a) * t) for a, b in zip(GRAD_A, GRAD_B)) + (255,)
stroke = max(2, int(round(2.2 * s)))
for a, b in (
(xy(9.5, 8.5), xy(9.5, 23.5)),
(xy(9.5, 8.5), xy(22.5, 23.5)),
(xy(22.5, 8.5), xy(22.5, 23.5)),
):
d.line([a, b], fill=LINE, width=stroke)
def circle(cx: float, cy: float, rad: float, fill: tuple[int, int, int, int]) -> None:
x, y = xy(cx, cy)
rr = rad * s
d.ellipse([x - rr, y - rr, x + rr, y + rr], fill=fill)
circle(9.5, 8.5, 2.35, NODE)
circle(9.5, 23.5, 2.35, NODE)
circle(22.5, 8.5, 2.35, NODE)
circle(22.5, 23.5, 2.35, NODE)
circle(16, 16, 1.7, ACCENT)
return img return img
def make(size: int) -> Image.Image:
px = size * SS
u = px / 24.0
tile = _gradient(px)
mask = Image.new("L", (px, px), 0)
ImageDraw.Draw(mask).rounded_rectangle([0, 0, px - 1, px - 1], radius=6.5 * u, fill=255)
img = Image.new("RGBA", (px, px), (0, 0, 0, 0))
img.paste(tile, (0, 0), mask)
links = Image.new("RGBA", (px, px), (0, 0, 0, 0))
ld = ImageDraw.Draw(links)
w = max(1, round(1.6 * u))
for (x1, y1), (x2, y2) in (((7, 7), (17, 17)), ((17, 7), (7, 17))):
ld.line([(x1 * u, y1 * u), (x2 * u, y2 * u)], fill=LINK, width=w)
for cx, cy in ((x1, y1), (x2, y2)): # round caps
r = w / 2
ld.ellipse([cx * u - r, cy * u - r, cx * u + r, cy * u + r], fill=LINK)
img = Image.alpha_composite(img, links)
d = ImageDraw.Draw(img)
def node(cx: float, cy: float, r: float, fill: tuple[int, int, int, int]) -> None:
d.ellipse([(cx - r) * u, (cy - r) * u, (cx + r) * u, (cy + r) * u], fill=fill)
node(12, 12, 2.6, WHITE)
node(7, 7, 1.9, WHITE)
node(7, 17, 1.9, WHITE)
node(17, 17, 1.9, WHITE)
node(17, 7, 2.1, LIVE)
return img.resize((size, size), Image.LANCZOS)
WIZ_TOP = (0x0A, 0x12, 0x22) # near-black navy
WIZ_BOTTOM = (0x10, 0x2A, 0x55) # deep blue
def make_wizard(w: int, h: int) -> Image.Image:
"""Inno Setup side image: dark navy gradient, faint link mesh, brand mark."""
img = Image.new("RGBA", (w, h))
pix = img.load()
for y in range(h):
t = y / max(1, h - 1)
row = tuple(round(a + (b - a) * t) for a, b in zip(WIZ_TOP, WIZ_BOTTOM)) + (255,)
for x in range(w):
pix[x, y] = row
mesh = Image.new("RGBA", (w * SS, h * SS), (0, 0, 0, 0))
md = ImageDraw.Draw(mesh)
pts = [(0.12, 0.62), (0.42, 0.70), (0.80, 0.60), (0.28, 0.86), (0.66, 0.90), (0.95, 0.80), (0.05, 0.95)]
pts = [(x * w * SS, y * h * SS) for x, y in pts]
line = (0x60, 0xA5, 0xFA, 46)
for a, b in ((0, 1), (1, 2), (0, 3), (1, 3), (1, 4), (2, 4), (2, 5), (4, 5), (3, 6)):
md.line([pts[a], pts[b]], fill=line, width=max(1, SS))
for i, (x, y) in enumerate(pts):
r = (2.4 if i != 2 else 3.0) * SS * w / 164
fill = (0x67, 0xE8, 0xF9, 150) if i == 2 else (0xBF, 0xDB, 0xFE, 90)
md.ellipse([x - r, y - r, x + r, y + r], fill=fill)
img = Image.alpha_composite(img, mesh.resize((w, h), Image.LANCZOS))
side = round(w * 0.42)
mark = make(side)
img.alpha_composite(mark, ((w - side) // 2, round(h * 0.26)))
return img.convert("RGB")
def main() -> None: def main() -> None:
sizes = [16, 24, 32, 48, 64, 128, 256] sizes = [16, 20, 24, 32, 40, 48, 64, 128, 256]
images = [make(s) for s in sizes] images = {s: make(s) for s in sizes}
ico_path = os.path.join(OUT_DIR, "netx.ico") ico_path = os.path.join(OUT_DIR, "netx.ico")
images[-1].save(ico_path, format="ICO", sizes=[(s, s) for s in sizes]) images[256].save(ico_path, format="ICO", sizes=[(s, s) for s in sizes], bitmap_format="bmp",
images[-1].save(os.path.join(OUT_DIR, "netx-256.png")) append_images=[images[s] for s in sizes[:-1]])
make(64).save(os.path.join(OUT_DIR, "netx-64.png")) images[256].save(os.path.join(OUT_DIR, "netx-256.png"))
images[64].save(os.path.join(OUT_DIR, "netx-64.png"))
# Inno Setup wizard images at 100/125/150/200% DPI (modern style base sizes).
for pct in (100, 125, 150, 200):
k = pct / 100
make_wizard(round(164 * k), round(314 * k)).save(os.path.join(OUT_DIR, f"wizard-{pct}.png"))
make(round(55 * k)).save(os.path.join(OUT_DIR, f"wizard-small-{pct}.png"))
print(f"wrote {ico_path} ({os.path.getsize(ico_path)} bytes)") print(f"wrote {ico_path} ({os.path.getsize(ico_path)} bytes)")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 575 B

After

Width:  |  Height:  |  Size: 4.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 361 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View file

@ -110,7 +110,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", "service_lifecycle.ps1", "install_update_task.ps1", "uninstall_prepare.ps1", "uninstall_delete_data.ps1", "sign_release.ps1", "service_run.ps1", "service_lifecycle.ps1", "install_update_task.ps1", "repair_venv.ps1", "runtime_ctl.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

View file

@ -440,6 +440,10 @@ try {
# (see installer/netx.iss /InstallMode=update). # (see installer/netx.iss /InstallMode=update).
Write-Host "==> Applying update via silent Setup.exe" Write-Host "==> Applying update via silent Setup.exe"
$setupArgs = "/VERYSILENT /NORESTART /SUPPRESSMSGBOXES /DIR=`"$prog`" /InstallMode=update" $setupArgs = "/VERYSILENT /NORESTART /SUPPRESSMSGBOXES /DIR=`"$prog`" /InstallMode=update"
# Setup stops the tray (StopForUpdate -KillTray); remember to bring it back.
$trayWasRunning = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object {
$_.CommandLine -and $_.CommandLine -match 'netx_tray\.ps1'
}).Count -gt 0
$sp = Start-Process -FilePath $pkgPath -ArgumentList $setupArgs -Wait -PassThru $sp = Start-Process -FilePath $pkgPath -ArgumentList $setupArgs -Wait -PassThru
if ($null -eq $sp.ExitCode -or $sp.ExitCode -ne 0) { if ($null -eq $sp.ExitCode -or $sp.ExitCode -ne 0) {
$code = if ($null -eq $sp.ExitCode) { "null" } else { $sp.ExitCode } $code = if ($null -eq $sp.ExitCode) { "null" } else { $sp.ExitCode }
@ -475,6 +479,14 @@ try {
-ProgramRoot $prog -DataRoot $data -SkipBrowser -ProgramRoot $prog -DataRoot $data -SkipBrowser
if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { exit $LASTEXITCODE } if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
} }
if ($trayWasRunning) {
$trayPs1 = Join-Path $prog "packaging\netx_tray.ps1"
if (Test-Path -LiteralPath $trayPs1) {
Write-Host "==> Restarting NetX tray"
Start-NetxPowerShell -File $trayPs1 -Arguments @("-ProgramRoot", $prog, "-DataRoot", $data, "-QuietStart") `
-WorkingDirectory $prog -WindowStyle Hidden | Out-Null
}
}
} else { } else {
Write-Host "==> Applying legacy zip update via update_netx.ps1" Write-Host "==> Applying legacy zip update via update_netx.ps1"
& powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "update_netx.ps1") ` & powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "update_netx.ps1") `

View file

@ -28,11 +28,18 @@ if ($Remove) {
exit 0 exit 0
} }
$arg = "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$tray`" -ProgramRoot `"$prog`" -DataRoot `"$data`" -StartOnLaunch" # -QuietStart: bring NetX up at logon without popping a browser window.
$arg = "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$tray`" -ProgramRoot `"$prog`" -DataRoot `"$data`" -StartOnLaunch -QuietStart"
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $arg -WorkingDirectory $prog $action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $arg -WorkingDirectory $prog
$trig = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME # Any user logon, run for the local Administrators group: Setup runs elevated
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable # (often as a different admin account), so $env:USERNAME is not the desktop user.
$principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive -RunLevel Highest $trig = New-ScheduledTaskTrigger -AtLogOn
$trig.Delay = "PT10S"
$admins = (New-Object System.Security.Principal.SecurityIdentifier("S-1-5-32-544")).Translate(
[System.Security.Principal.NTAccount]).Value
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable `
-ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances Parallel
$principal = New-ScheduledTaskPrincipal -GroupId $admins -RunLevel Highest
Register-ScheduledTask -TaskName $taskName -TaskPath $taskPath ` Register-ScheduledTask -TaskName $taskName -TaskPath $taskPath `
-Action $action -Trigger $trig -Settings $settings -Principal $principal -Force | Out-Null -Action $action -Trigger $trig -Settings $settings -Principal $principal -Force | Out-Null

View file

@ -129,7 +129,9 @@ if ($Mode -eq "winsw") {
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $arg -WorkingDirectory $prog $action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $arg -WorkingDirectory $prog
$trigger = New-ScheduledTaskTrigger -AtStartup $trigger = New-ScheduledTaskTrigger -AtStartup
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) # ExecutionTimeLimit 0: the default (72h) would kill the long-running supervisor.
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) `
-ExecutionTimeLimit ([TimeSpan]::Zero)
Register-ScheduledTask -TaskName "NetXService" -TaskPath "\NetX\" -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null Register-ScheduledTask -TaskName "NetXService" -TaskPath "\NetX\" -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
if ($Start) { if ($Start) {
Start-ScheduledTask -TaskName "NetXService" -TaskPath "\NetX\" Start-ScheduledTask -TaskName "NetXService" -TaskPath "\NetX\"

View file

@ -22,6 +22,11 @@ $checkPs1 = Join-Path $PSScriptRoot "check_update.ps1"
if ($Remove) { if ($Remove) {
Unregister-ScheduledTask -TaskName $taskName -TaskPath $taskPath -Confirm:$false -ErrorAction SilentlyContinue Unregister-ScheduledTask -TaskName $taskName -TaskPath $taskPath -Confirm:$false -ErrorAction SilentlyContinue
# Tray also applies updates on launch when NETX_UPDATE_AUTO=true; turn that off too.
$envFile = Join-Path $data ".env"
if (Test-Path $envFile) {
Write-DotEnvValue -Path $envFile -Values @{ "NETX_UPDATE_AUTO" = "false" }
}
Write-Host "==> Update task removed" Write-Host "==> Update task removed"
exit 0 exit 0
} }

View file

@ -14,11 +14,16 @@
; Stage directory produced by build_release.ps1 (relative to this .iss) ; Stage directory produced by build_release.ps1 (relative to this .iss)
#define StageDir "..\release\netx-win64" #define StageDir "..\release\netx-win64"
#define IconFile "..\assets\netx.ico" #define IconFile "..\assets\netx.ico"
#define AssetsDir "..\assets"
[Setup] [Setup]
AppId={{A7E3C2D1-9F40-4B8E-9C1A-NETXWIN64001} AppId={{A7E3C2D1-9F40-4B8E-9C1A-NETXWIN64001}
AppName={#MyAppName} AppName={#MyAppName}
AppVersion={#MyAppVersion} AppVersion={#MyAppVersion}
AppVerName={#MyAppName} {#MyAppVersion}
VersionInfoVersion={#MyAppVersion}
VersionInfoProductName={#MyAppName}
UninstallDisplayName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher} AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL} AppPublisherURL={#MyAppURL}
DefaultDirName={autopf}\NetX DefaultDirName={autopf}\NetX
@ -34,7 +39,11 @@ SetupIconFile={#IconFile}
UninstallDisplayIcon={app}\packaging\assets\netx.ico UninstallDisplayIcon={app}\packaging\assets\netx.ico
Compression=lzma2 Compression=lzma2
SolidCompression=yes SolidCompression=yes
WizardStyle=modern ; Follows the Windows light/dark setting; windows11 = flat Fluent-style controls.
WizardStyle=modern dynamic windows11 hidebevels
WizardImageFile={#AssetsDir}\wizard-100.png,{#AssetsDir}\wizard-125.png,{#AssetsDir}\wizard-150.png,{#AssetsDir}\wizard-200.png
WizardSmallImageFile={#AssetsDir}\wizard-small-100.png,{#AssetsDir}\wizard-small-125.png,{#AssetsDir}\wizard-small-150.png,{#AssetsDir}\wizard-small-200.png
WizardImageBackColor=#0a1222
ArchitecturesAllowed=x64compatible ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible ArchitecturesInstallIn64BitMode=x64compatible
PrivilegesRequired=admin PrivilegesRequired=admin
@ -42,7 +51,8 @@ PrivilegesRequired=admin
; 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. ; pages while scanning locked NetX tray/service processes with no visible dialog.
CloseApplications=no CloseApplications=no
ShowLanguageDialog=yes ; Pick from the Windows UI language; no extra dialog.
ShowLanguageDialog=auto
[Languages] [Languages]
Name: "english"; MessagesFile: "compiler:Default.isl" Name: "english"; MessagesFile: "compiler:Default.isl"
@ -51,10 +61,17 @@ Name: "chinesesimplified"; MessagesFile: "Languages\ChineseSimplified.isl"
[CustomMessages] [CustomMessages]
english.CreateDesktopIcon=Create a desktop shortcut english.CreateDesktopIcon=Create a desktop shortcut
english.SetupOptions=After install: english.SetupOptions=After install:
english.StartTrayNow=Start NetX tray now english.StartTrayNow=Start NetX now
english.EnableAutostart=Start NetX tray at Windows logon english.RunModeGroup=How NetX runs:
english.EnableAutoUpdate=Enable daily silent auto-update (needs network later) english.RunModeTray=Desktop tray (recommended) - control NetX from the notification area
english.InstallService=Install as Windows Service (uses bundled WinSW, offline) english.EnableAutostart=Start automatically when I sign in to Windows
english.RunModeService=Windows service - runs at boot without anyone signed in (servers)
english.MaintenanceGroup=Maintenance:
english.EnableAutoUpdate=Install updates automatically every day (needs network)
english.ServiceInstallFailed=NetX was installed, but the Windows service could not be registered (exit %1).%n%nYou can still start NetX from the Start Menu. Log: %2
english.IconStart=Start NetX
english.IconStop=Stop NetX
english.IconUpdate=Check for updates
english.UninstallDeleteData=Also delete NetX data (%ProgramData%\NetX)?%n%nYes = remove database, settings, and secrets%nNo = keep data for a later reinstall english.UninstallDeleteData=Also delete NetX data (%ProgramData%\NetX)?%n%nYes = remove database, settings, and secrets%nNo = keep data for a later reinstall
english.DbPageCaption=Database english.DbPageCaption=Database
english.DbPageDescription=Choose built-in or external PostgreSQL. Optional credential key: paste from an existing install, or leave empty to auto-generate. english.DbPageDescription=Choose built-in or external PostgreSQL. Optional credential key: paste from an existing install, or leave empty to auto-generate.
@ -86,10 +103,17 @@ english.ModeUpdateHint=Replaces program files only. ProgramData\.env and databas
english.ModeReinstallHint=Shows the Database page again. Does not delete ProgramData unless you uninstall and choose to. english.ModeReinstallHint=Shows the Database page again. Does not delete ProgramData unless you uninstall and choose to.
chinesesimplified.CreateDesktopIcon=创建桌面快捷方式 chinesesimplified.CreateDesktopIcon=创建桌面快捷方式
chinesesimplified.SetupOptions=安装完成后: chinesesimplified.SetupOptions=安装完成后:
chinesesimplified.StartTrayNow=立即启动 NetX 托盘 chinesesimplified.StartTrayNow=立即启动 NetX
chinesesimplified.EnableAutostart=开机时自动启动 NetX 托盘 chinesesimplified.RunModeGroup=运行方式:
chinesesimplified.EnableAutoUpdate=启用每日静默自动更新(以后需要联网) chinesesimplified.RunModeTray=桌面托盘(推荐)— 在任务栏通知区域控制 NetX
chinesesimplified.InstallService=安装为 Windows 服务(使用已捆绑的 WinSW,可离线) chinesesimplified.EnableAutostart=登录 Windows 时自动启动
chinesesimplified.RunModeService=Windows 服务 — 开机即运行,无需登录(适合服务器)
chinesesimplified.MaintenanceGroup=维护:
chinesesimplified.EnableAutoUpdate=每天自动安装更新(需要联网)
chinesesimplified.ServiceInstallFailed=NetX 已安装,但 Windows 服务注册失败(退出码 %1)。%n%n仍可从开始菜单启动 NetX。日志:%2
chinesesimplified.IconStart=启动 NetX
chinesesimplified.IconStop=停止 NetX
chinesesimplified.IconUpdate=检查更新
chinesesimplified.UninstallDeleteData=是否同时删除 NetX 数据(%ProgramData%\NetX)?%n%n是 = 删除数据库、配置和密钥%n否 = 保留数据以便以后重装 chinesesimplified.UninstallDeleteData=是否同时删除 NetX 数据(%ProgramData%\NetX)?%n%n是 = 删除数据库、配置和密钥%n否 = 保留数据以便以后重装
chinesesimplified.DbPageCaption=数据库 chinesesimplified.DbPageCaption=数据库
chinesesimplified.DbPageDescription=选择内置或外置 PostgreSQL。可选粘贴已有 NETX_CREDENTIAL_SECRET_KEY;留空则自动生成。 chinesesimplified.DbPageDescription=选择内置或外置 PostgreSQL。可选粘贴已有 NETX_CREDENTIAL_SECRET_KEY;留空则自动生成。
@ -121,6 +145,12 @@ chinesesimplified.ModeUpdateHint=仅替换程序文件,保留 ProgramData\.env
chinesesimplified.ModeReinstallHint=再次显示数据库页。不会删除 ProgramData(除非卸载时选择删除)。 chinesesimplified.ModeReinstallHint=再次显示数据库页。不会删除 ProgramData(除非卸载时选择删除)。
[Tasks] [Tasks]
; Run mode is exclusive: tray + service together would race for the same port.
; On upgrades CurPageChanged pre-selects whatever is actually installed.
Name: "runtray"; Description: "{cm:RunModeTray}"; GroupDescription: "{cm:RunModeGroup}"; Flags: exclusive
Name: "runtray\autostart"; Description: "{cm:EnableAutostart}"
Name: "runservice"; Description: "{cm:RunModeService}"; GroupDescription: "{cm:RunModeGroup}"; Flags: exclusive unchecked
Name: "autoupdate"; Description: "{cm:EnableAutoUpdate}"; GroupDescription: "{cm:MaintenanceGroup}"; Flags: unchecked
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce
[Files] [Files]
@ -163,26 +193,30 @@ Type: filesandordirs; Name: "{app}\.venv"
Type: files; Name: "{commondesktop}\NetX Start.lnk" Type: files; Name: "{commondesktop}\NetX Start.lnk"
Type: files; Name: "{commondesktop}\NetX First-time setup.lnk" Type: files; Name: "{commondesktop}\NetX First-time setup.lnk"
Type: files; Name: "{commondesktop}\首次配置(内置或外置数据库).lnk" Type: files; Name: "{commondesktop}\首次配置(内置或外置数据库).lnk"
; Start Menu entries from <= 0.4.16 (English-only names; toggles moved into the tray / Tasks page).
Type: files; Name: "{group}\NetX Tray.lnk"
Type: files; Name: "{group}\Start NetX.lnk"
Type: files; Name: "{group}\Stop NetX.lnk"
Type: files; Name: "{group}\Check for updates.lnk"
Type: files; Name: "{group}\Open NetX UI.url"
Type: files; Name: "{group}\Install Windows Service (admin).lnk"
Type: files; Name: "{group}\Enable silent auto-update (daily).lnk"
Type: files; Name: "{group}\Enable start at logon.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.
Name: "{group}\NetX Tray"; Filename: "{app}\NetX-Tray.cmd"; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico" ; Autostart / auto-update / service toggles live in the tray Settings menu and the Tasks page.
Name: "{group}\Start NetX"; Filename: "{app}\NetX-Start.cmd"; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico" Name: "{group}\NetX"; Filename: "{app}\NetX-Tray.cmd"; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico"
Name: "{group}\Stop NetX"; Filename: "{app}\NetX-Stop.cmd"; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico" Name: "{group}\{cm:IconStart}"; Filename: "{app}\NetX-Start.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}\{cm:IconStop}"; Filename: "{app}\NetX-Stop.cmd"; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico"
Name: "{group}\Open NetX UI"; Filename: "http://127.0.0.1:8890/"; IconFilename: "{app}\packaging\assets\netx.ico" Name: "{group}\{cm:IconUpdate}"; Filename: "powershell.exe"; Parameters: "-NoProfile -WindowStyle Hidden -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}\{cm:ReconfigureDb}"; 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" ; One desktop shortcut only (tray = start + tray UI).
Name: "{group}\Enable silent auto-update (daily)"; Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\install_update_task.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico"
Name: "{group}\Enable start at logon"; Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\install_autostart.ps1"" -ProgramRoot ""{app}"""; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico"
; One desktop shortcut only (tray = start + tray UI). Start Menu keeps Start/Stop entries.
Name: "{commondesktop}\NetX"; 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]
; Autostart / service / auto-update are applied from the Tasks page in CurStepChanged.
Filename: "powershell.exe"; Parameters: "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File ""{app}\packaging\netx_tray.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"" -StartOnLaunch"; WorkingDir: "{app}"; Description: "{cm:StartTrayNow}"; Flags: postinstall runhidden nowait skipifsilent; Check: NetxShouldStartTray Filename: "powershell.exe"; Parameters: "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File ""{app}\packaging\netx_tray.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"" -StartOnLaunch"; WorkingDir: "{app}"; Description: "{cm:StartTrayNow}"; Flags: postinstall runhidden nowait skipifsilent; Check: NetxShouldStartTray
Filename: "powershell.exe"; Parameters: "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File ""{app}\packaging\install_autostart.ps1"" -ProgramRoot ""{app}"""; WorkingDir: "{app}"; Description: "{cm:EnableAutostart}"; Flags: postinstall runhidden skipifsilent unchecked
Filename: "powershell.exe"; Parameters: "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File ""{app}\packaging\install_update_task.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}"; Description: "{cm:EnableAutoUpdate}"; Flags: postinstall runhidden skipifsilent unchecked
Filename: "powershell.exe"; Parameters: "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File ""{app}\packaging\install_service.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"" -Start"; WorkingDir: "{app}"; Description: "{cm:InstallService}"; Flags: postinstall runhidden skipifsilent unchecked
; Stop runtime BEFORE files are deleted. Do not also Exec prepare from ; Stop runtime BEFORE files are deleted. Do not also Exec prepare from
; InitializeUninstall (that previously raced/killed unins000 or hung the UI). ; InitializeUninstall (that previously raced/killed unins000 or hung the UI).
@ -232,6 +266,7 @@ var
GPgToolsReady: Boolean; GPgToolsReady: Boolean;
GDbConnOk: Boolean; GDbConnOk: Boolean;
GNextBtnCaption: String; GNextBtnCaption: String;
GTasksSynced: Boolean;
function EnvHasDbMode(): Boolean; function EnvHasDbMode(): Boolean;
var var
@ -822,6 +857,7 @@ begin
EdCredKey.Left := ScaleX(120); EdCredKey.Left := ScaleX(120);
EdCredKey.Width := DbPage.SurfaceWidth - ScaleX(120); EdCredKey.Width := DbPage.SurfaceWidth - ScaleX(120);
EdCredKey.Text := ''; EdCredKey.Text := '';
EdCredKey.PasswordChar := '*';
TopPos := TopPos + ScaleY(24); TopPos := TopPos + ScaleY(24);
LblCredHint := TNewStaticText.Create(DbPage); LblCredHint := TNewStaticText.Create(DbPage);
@ -1120,6 +1156,106 @@ begin
Exec('powershell.exe', Params, ExpandConstant('{app}'), SW_HIDE, ewWaitUntilTerminated, ResultCode); Exec('powershell.exe', Params, ExpandConstant('{app}'), SW_HIDE, ewWaitUntilTerminated, ResultCode);
end; end;
function NetxTaskExists(const Name: String): Boolean;
var
RC: Integer;
begin
Result := Exec(ExpandConstant('{sys}\schtasks.exe'), '/Query /TN "\NetX\' + Name + '"', '',
SW_HIDE, ewWaitUntilTerminated, RC) and (RC = 0);
end;
function NetxServiceExists(): Boolean;
var
RC: Integer;
begin
Result := Exec(ExpandConstant('{sys}\sc.exe'), 'query NetX', '', SW_HIDE, ewWaitUntilTerminated, RC) and (RC = 0);
end;
procedure CurPageChanged(CurPageID: Integer);
begin
{ Existing install: show the run mode / toggles that are really in place
(older versions set them from Finish-page checkboxes Inno never recorded). }
if (CurPageID = wpSelectTasks) and (not GTasksSynced) then
begin
GTasksSynced := True;
if FileExists(ExpandConstant('{app}\packaging\netx_tray.ps1')) then
begin
if NetxServiceExists() then
WizardSelectTasks('runservice')
else
WizardSelectTasks('runtray');
if NetxTaskExists('NetXTray') then
WizardSelectTasks('runtray\autostart')
else
WizardSelectTasks('!runtray\autostart');
if NetxTaskExists('NetXUpdate') then
WizardSelectTasks('autoupdate')
else
WizardSelectTasks('!autoupdate');
end;
end;
end;
function RunNetxScript(const Script, Extra: String): Integer;
var
Params: String;
begin
Params :=
'-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + ExpandConstant('{app}\packaging\' + Script) + '"' +
' -ProgramRoot "' + ExpandConstant('{app}') + '"' +
' -DataRoot "' + ExpandConstant('{commonappdata}\NetX') + '"' + Extra;
if not Exec('powershell.exe', Params, ExpandConstant('{app}'), SW_HIDE, ewWaitUntilTerminated, Result) then
Result := -1;
end;
function TasksGivenOnCommandLine(): Boolean;
var
i: Integer;
P: String;
begin
Result := False;
for i := 1 to ParamCount do
begin
P := Uppercase(ParamStr(i));
if (Pos('/TASKS=', P) = 1) or (Pos('/MERGETASKS=', P) = 1) then
begin
Result := True;
Exit;
end;
end;
end;
procedure ApplyRunModeTasks();
var
Code: Integer;
begin
{ Silent auto-updates keep whatever is installed unless /TASKS is passed. }
if WizardSilent and (not TasksGivenOnCommandLine()) then
Exit;
if not FileExists(ExpandConstant('{commonappdata}\NetX\.env')) then
Exit;
if WizardIsTaskSelected('runservice') then
begin
Code := RunNetxScript('install_service.ps1', ' -Start');
if Code <> 0 then
SuppressibleMsgBox(FmtMessage(ExpandConstant('{cm:ServiceInstallFailed}'), [IntToStr(Code),
ExpandConstant('{commonappdata}\NetX\data\runtime')]), mbError, MB_OK, IDOK);
end
else if NetxServiceExists() then
RunNetxScript('install_service.ps1', ' -Uninstall');
if WizardIsTaskSelected('runtray\autostart') then
RunNetxScript('install_autostart.ps1', '')
else
RunNetxScript('install_autostart.ps1', ' -Remove');
if WizardIsTaskSelected('autoupdate') then
RunNetxScript('install_update_task.ps1', '')
else if NetxTaskExists('NetXUpdate') then
RunNetxScript('install_update_task.ps1', ' -Remove');
end;
procedure CurStepChanged(CurStep: TSetupStep); procedure CurStepChanged(CurStep: TSetupStep);
var var
UninstallKey: String; UninstallKey: String;
@ -1138,6 +1274,7 @@ begin
UninstallKey := UninstallKey :=
'Software\Microsoft\Windows\CurrentVersion\Uninstall\{A7E3C2D1-9F40-4B8E-9C1A-NETXWIN64001}_is1'; 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{A7E3C2D1-9F40-4B8E-9C1A-NETXWIN64001}_is1';
RegDeleteValue(HKLM, UninstallKey, 'QuietUninstallString'); RegDeleteValue(HKLM, UninstallKey, 'QuietUninstallString');
ApplyRunModeTasks();
ResumeNetxServiceAfterInstall(); ResumeNetxServiceAfterInstall();
end; end;
end; end;

View file

@ -79,7 +79,7 @@ try {
} }
} }
"stop" { "stop" {
$p = Invoke-NetxHiddenPs1 -File (Join-Path $PSScriptRoot "stop_netx_app.ps1") $p = Invoke-NetxHiddenPs1 -File (Join-Path $PSScriptRoot "runtime_ctl.ps1") -ExtraArgs @("-Action", "stop")
if ($null -ne $p.ExitCode -and $p.ExitCode -ne 0) { throw "stop_exit_$($p.ExitCode)" } if ($null -ne $p.ExitCode -and $p.ExitCode -ne 0) { throw "stop_exit_$($p.ExitCode)" }
} }
"update" { "update" {
@ -102,10 +102,11 @@ try {
if ($map["NETX_HOST"]) { $hostBind = $map["NETX_HOST"] } if ($map["NETX_HOST"]) { $hostBind = $map["NETX_HOST"] }
if ($map["NETX_PORT"]) { try { $port = [int]$map["NETX_PORT"] } catch {} } if ($map["NETX_PORT"]) { try { $port = [int]$map["NETX_PORT"] } catch {} }
} }
$p = Invoke-NetxHiddenPs1 -File (Join-Path $PSScriptRoot "start_netx_app.ps1") -ExtraArgs @("-SkipBrowser") $hostBind = Get-NetxClientHost -BindHost $hostBind
$p = Invoke-NetxHiddenPs1 -File (Join-Path $PSScriptRoot "runtime_ctl.ps1") -ExtraArgs @("-Action", "start")
if ($p.ExitCode -ne 0) { throw "start_exit_$($p.ExitCode)" } if ($p.ExitCode -ne 0) { throw "start_exit_$($p.ExitCode)" }
if (Wait-NetxApiHealthy -HostName $hostBind -Port $port -TimeoutSec 180) { if (Wait-NetxApiHealthy -HostName $hostBind -Port $port -TimeoutSec 180) {
try { Start-Process "http://${hostBind}:${port}/" } catch {} Open-NetxUrl -Url "http://${hostBind}:${port}/"
} else { } else {
throw (T ` throw (T `
"NetX started but /health not ready within 180s.`nSee: $logDir\netx.err.log" ` "NetX started but /health not ready within 180s.`nSee: $logDir\netx.err.log" `

File diff suppressed because it is too large Load diff

52
packaging/runtime_ctl.ps1 Normal file
View file

@ -0,0 +1,52 @@
param(
[ValidateSet("start", "stop", "restart")]
[string]$Action = "start",
[string]$ProgramRoot = "",
[string]$DataRoot = ""
)
# Single start/stop/restart entry for tray + shortcuts. Routes through the
# Windows service / SYSTEM task when one is installed, else the plain scripts.
$ErrorActionPreference = "Stop"
. "$PSScriptRoot\_common.ps1"
Assert-NetxAdminOrRelaunch -ScriptPath $PSCommandPath -BoundParameters $PSBoundParameters -WindowStyle Hidden
$prog = Get-NetxProgramRoot -Override $ProgramRoot
$data = Get-NetxDataRoot -ProgramRoot $prog -Override $DataRoot
function Invoke-NetxChildScript([string]$Name, [string[]]$Extra = @()) {
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot $Name) `
-ProgramRoot $prog -DataRoot $data @Extra
return $LASTEXITCODE
}
$runner = Get-NetxManagedRunner
switch ($runner) {
"service" {
switch ($Action) {
"start" { Start-Service -Name "NetX" }
"stop" { Stop-Service -Name "NetX" -Force }
"restart" { Restart-Service -Name "NetX" -Force }
}
}
"task" {
if ($Action -ne "start") {
Stop-ScheduledTask -TaskName "NetXService" -TaskPath "\NetX\"
$null = Invoke-NetxChildScript "stop_netx_app.ps1"
}
if ($Action -ne "stop") {
Start-ScheduledTask -TaskName "NetXService" -TaskPath "\NetX\"
}
}
default {
if ($Action -ne "start") {
$null = Invoke-NetxChildScript "stop_netx_app.ps1"
}
if ($Action -ne "stop") {
$code = Invoke-NetxChildScript "start_netx_app.ps1" @("-SkipBrowser")
if ($code) { exit $code }
}
}
}
exit 0

View file

@ -84,11 +84,12 @@ try {
'ResumeAfterUpdate' { 'ResumeAfterUpdate' {
if (-not (Test-Path -LiteralPath $stateFile)) { break } if (-not (Test-Path -LiteralPath $stateFile)) { break }
$state = Get-Content -LiteralPath $stateFile -Raw | ConvertFrom-Json $state = Get-Content -LiteralPath $stateFile -Raw | ConvertFrom-Json
if ($state.serviceWasRunning) { # Setup may have switched run mode (service -> tray) in between: skip what is gone.
if ($state.serviceWasRunning -and (Get-Service -Name 'NetX' -ErrorAction SilentlyContinue)) {
Write-LifecycleLog 'Restarting Windows service NetX after update' Write-LifecycleLog 'Restarting Windows service NetX after update'
Start-Service -Name 'NetX' -ErrorAction Stop Start-Service -Name 'NetX' -ErrorAction Stop
} }
if ($state.taskWasRunning) { if ($state.taskWasRunning -and (Get-NetxTask)) {
Write-LifecycleLog 'Restarting scheduled task NetXService after update' Write-LifecycleLog 'Restarting scheduled task NetXService after update'
Start-ScheduledTask -TaskName 'NetXService' -TaskPath '\NetX\' -ErrorAction Stop Start-ScheduledTask -TaskName 'NetXService' -TaskPath '\NetX\' -ErrorAction Stop
} }
@ -96,6 +97,17 @@ try {
} }
'RemoveForUninstall' { 'RemoveForUninstall' {
$null = Stop-NetxRunners -Remove $null = Stop-NetxRunners -Remove
# Logon tray + daily update tasks would otherwise outlive the program files.
foreach ($t in @('NetXTray', 'NetXUpdate')) {
Unregister-ScheduledTask -TaskName $t -TaskPath '\NetX\' -Confirm:$false -ErrorAction SilentlyContinue
}
try {
$sched = New-Object -ComObject Schedule.Service
$sched.Connect()
$root = $sched.GetFolder('\')
$nx = $root.GetFolder('\NetX')
if ($nx.GetTasks(1).Count -eq 0 -and $nx.GetFolders(0).Count -eq 0) { $root.DeleteFolder('NetX', 0) }
} catch {}
$stopScript = Join-Path $prog 'packaging\stop_netx_app.ps1' $stopScript = Join-Path $prog 'packaging\stop_netx_app.ps1'
if (Test-Path -LiteralPath $stopScript) { if (Test-Path -LiteralPath $stopScript) {
& powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File $stopScript ` & powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File $stopScript `

View file

@ -43,15 +43,16 @@ if (Test-Path (Join-Path $dist "index.html")) {
$mode = Get-DbMode -EnvMap $map $mode = Get-DbMode -EnvMap $map
$hostBind = if ($env:NETX_HOST) { $env:NETX_HOST } else { "127.0.0.1" } $hostBind = if ($env:NETX_HOST) { $env:NETX_HOST } else { "127.0.0.1" }
$port = if ($env:NETX_PORT) { [int]$env:NETX_PORT } else { 8890 } $port = if ($env:NETX_PORT) { [int]$env:NETX_PORT } else { 8890 }
$clientHost = Get-NetxClientHost -BindHost $hostBind
Write-Host "==> Program: $prog" Write-Host "==> Program: $prog"
Write-Host "==> Data: $data" Write-Host "==> Data: $data"
Write-Host "==> DB mode: $mode" Write-Host "==> DB mode: $mode"
if (-not $Force -and (Test-NetxApiHealthy -HostName $hostBind -Port $port)) { if (-not $Force -and (Test-NetxApiHealthy -HostName $clientHost -Port $port)) {
Write-Host "==> NetX already healthy at http://${hostBind}:${port}/ — skip start (use -Force to restart)" -ForegroundColor Green Write-Host "==> NetX already healthy at http://${clientHost}:${port}/ — skip start (use -Force to restart)" -ForegroundColor Green
if (-not $SkipBrowser) { if (-not $SkipBrowser) {
try { Start-Process "http://${hostBind}:${port}/" } catch {} Open-NetxUrl -Url "http://${clientHost}:${port}/"
} }
exit 0 exit 0
} }
@ -106,8 +107,8 @@ try {
exit 1 exit 1
} }
if (-not (Test-NetxTcpPortFree -HostName $hostBind -Port $port)) { if (-not (Test-NetxTcpPortFree -HostName $clientHost -Port $port)) {
if (Test-NetxApiHealthy -HostName $hostBind -Port $port) { if (Test-NetxApiHealthy -HostName $clientHost -Port $port) {
Write-Host "==> Port $port already serves NetX — skip start" -ForegroundColor Green Write-Host "==> Port $port already serves NetX — skip start" -ForegroundColor Green
exit 0 exit 0
} }
@ -132,14 +133,14 @@ if ($InlineSchedulers) { $startArgs["InlineSchedulers"] = $true }
& $startScript @startArgs & $startScript @startArgs
if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { exit $LASTEXITCODE } if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$url = "http://${hostBind}:${port}/" $url = "http://${clientHost}:${port}/"
if (-not (Test-NetxApiHealthy -HostName $hostBind -Port $port -TimeoutSec 2)) { if (-not (Test-NetxApiHealthy -HostName $clientHost -Port $port -TimeoutSec 2)) {
if (-not (Wait-NetxApiHealthy -HostName $hostBind -Port $port -TimeoutSec 30)) { if (-not (Wait-NetxApiHealthy -HostName $clientHost -Port $port -TimeoutSec 30)) {
Write-Host "[ERR] NetX started but /health not ready: $url" -ForegroundColor Red Write-Host "[ERR] NetX started but /health not ready: $url" -ForegroundColor Red
exit 1 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 {} Open-NetxUrl -Url $url
} }

View file

@ -147,6 +147,8 @@ function Show-LogTail {
function Test-NetxApiListening { function Test-NetxApiListening {
param([string]$HostName, [int]$LocalPort, [int]$WaitSec = 45) param([string]$HostName, [int]$LocalPort, [int]$WaitSec = 45)
$deadline = (Get-Date).AddSeconds($WaitSec) $deadline = (Get-Date).AddSeconds($WaitSec)
# Wildcard binds are not connectable on Windows; probe via loopback.
if ($HostName -in @("0.0.0.0", "::", "[::]", "*")) { $HostName = "127.0.0.1" }
$healthUrl = "http://${HostName}:${LocalPort}/health" $healthUrl = "http://${HostName}:${LocalPort}/health"
while ((Get-Date) -lt $deadline) { while ((Get-Date) -lt $deadline) {
try { try {