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>
|
|
@ -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).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
|
||||||
|
|
@ -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)")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 575 B After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 361 KiB |
BIN
packaging/assets/wizard-100.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
packaging/assets/wizard-125.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
packaging/assets/wizard-150.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
packaging/assets/wizard-200.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
packaging/assets/wizard-small-100.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
packaging/assets/wizard-small-125.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
packaging/assets/wizard-small-150.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
packaging/assets/wizard-small-200.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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") `
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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\"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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" `
|
||||||
|
|
|
||||||
52
packaging/runtime_ctl.ps1
Normal 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
|
||||||
|
|
@ -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 `
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||