mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 03:10:46 +08:00
Add Windows service, silent auto-update, and code-signing hooks.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
fe9df32024
commit
b639f01c0f
13 changed files with 463 additions and 31 deletions
|
|
@ -122,6 +122,8 @@ NETX_UME_NOTIFICATION_TOPIC=ALARM
|
||||||
# NETX_BUNDLED_PG_DATA_DIR=
|
# NETX_BUNDLED_PG_DATA_DIR=
|
||||||
# NETX_UPDATE_URL=
|
# NETX_UPDATE_URL=
|
||||||
# NETX_UPDATE_CHANNEL=stable
|
# NETX_UPDATE_CHANNEL=stable
|
||||||
|
# NETX_UPDATE_AUTO=false
|
||||||
|
# When true: tray/scheduled task may download+apply zip updates silently.
|
||||||
# Heavier fleets: raise CLI/DB together; also ensure Postgres max_connections and bastion session limits.
|
# Heavier fleets: raise CLI/DB together; also ensure Postgres max_connections and bastion session limits.
|
||||||
# One-click start (recommended): .\scripts\start_netx.ps1 -Background -WithWeb
|
# One-click start (recommended): .\scripts\start_netx.ps1 -Background -WithWeb
|
||||||
# -> API + netx_api.worker + N× netx_api.biz_state_worker + optional Vite
|
# -> API + netx_api.worker + N× netx_api.biz_state_worker + optional Vite
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -20,4 +20,6 @@ data/runtime/
|
||||||
packaging/cache/
|
packaging/cache/
|
||||||
packaging/release/
|
packaging/release/
|
||||||
packaging/postgres/pgsql/
|
packaging/postgres/pgsql/
|
||||||
|
packaging/winsw/
|
||||||
*.exe
|
*.exe
|
||||||
|
!packaging/installer/*.iss
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,40 @@ Default source: latest GitHub Release for `hansjone/netx`. Optional custom manif
|
||||||
# Remove: .\packaging\install_autostart.ps1 -Remove
|
# Remove: .\packaging\install_autostart.ps1 -Remove
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Windows Service (admin)
|
||||||
|
|
||||||
|
Uses [WinSW](https://github.com/winsw/winsw) (downloaded on first install into `packaging/cache`).
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Elevated PowerShell
|
||||||
|
.\packaging\install_service.ps1 -Start
|
||||||
|
# Uninstall: .\packaging\install_service.ps1 -Uninstall
|
||||||
|
|
||||||
|
# Fallback without WinSW binary management: SYSTEM scheduled task at startup
|
||||||
|
.\packaging\install_service.ps1 -Mode task -Start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Silent auto-update
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Writes NETX_UPDATE_AUTO=true and registers a daily task (default 03:30)
|
||||||
|
.\packaging\install_update_task.ps1
|
||||||
|
|
||||||
|
# Manual silent path (only applies when NETX_UPDATE_AUTO=true)
|
||||||
|
.\packaging\check_update.ps1 -Apply -Quiet -AutoOnly
|
||||||
|
```
|
||||||
|
|
||||||
|
Tray also auto-applies on launch when `NETX_UPDATE_AUTO=true`.
|
||||||
|
|
||||||
|
## Code signing (optional, publisher machine)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\packaging\sign_release.ps1 -Files .\packaging\release\NetX-Setup-0.4.0.exe -Thumbprint <cert-sha1>
|
||||||
|
# or: -PfxPath .\certs\code.pfx -PfxPassword ***
|
||||||
|
```
|
||||||
|
|
||||||
|
Needs `signtool.exe` (Windows SDK) and a real code-signing certificate. Without a trusted cert, SmartScreen may still warn.
|
||||||
|
|
||||||
## Layout reminder
|
## Layout reminder
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -72,8 +72,9 @@ Copy-Item -Path (Join-Path $PSScriptRoot "_common.ps1") -Destination $packOut -F
|
||||||
foreach ($name in @(
|
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",
|
||||||
"netx_tray.ps1", "install_autostart.ps1", "build_release.ps1",
|
"netx_tray.ps1", "install_autostart.ps1", "install_service.ps1",
|
||||||
"publish_release.ps1", "README.md", "manifest.example.json"
|
"service_run.ps1", "install_update_task.ps1", "sign_release.ps1",
|
||||||
|
"build_release.ps1", "publish_release.ps1", "README.md", "manifest.example.json"
|
||||||
)) {
|
)) {
|
||||||
$src = Join-Path $PSScriptRoot $name
|
$src = Join-Path $PSScriptRoot $name
|
||||||
if (Test-Path $src) { Copy-Item $src (Join-Path $packOut $name) -Force }
|
if (Test-Path $src) { Copy-Item $src (Join-Path $packOut $name) -Force }
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ param(
|
||||||
[string]$UpdateUrl = "",
|
[string]$UpdateUrl = "",
|
||||||
[string]$Channel = "",
|
[string]$Channel = "",
|
||||||
[switch]$Apply = $false,
|
[switch]$Apply = $false,
|
||||||
[switch]$Quiet = $false
|
[switch]$Quiet = $false,
|
||||||
|
# Only apply when NETX_UPDATE_AUTO=true (scheduled silent updates).
|
||||||
|
[switch]$AutoOnly = $false
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check for a newer NetX Windows package.
|
# Check for a newer NetX Windows package.
|
||||||
|
|
@ -116,33 +118,68 @@ if (-not $result.update_available) {
|
||||||
Write-Host "==> Update available: $current -> $latest" -ForegroundColor Cyan
|
Write-Host "==> Update available: $current -> $latest" -ForegroundColor Cyan
|
||||||
if ($result.notes_url) { Write-Host " Notes: $($result.notes_url)" }
|
if ($result.notes_url) { Write-Host " Notes: $($result.notes_url)" }
|
||||||
|
|
||||||
|
$autoEnabled = $false
|
||||||
|
$autoVal = ""
|
||||||
|
if ($map["NETX_UPDATE_AUTO"]) { $autoVal = $map["NETX_UPDATE_AUTO"] }
|
||||||
|
elseif ($env:NETX_UPDATE_AUTO) { $autoVal = $env:NETX_UPDATE_AUTO }
|
||||||
|
if ($autoVal -match '^(1|true|yes|on)$') { $autoEnabled = $true }
|
||||||
|
|
||||||
if (-not $Apply) {
|
if (-not $Apply) {
|
||||||
Write-Host " Download (zip): $($result.download_url)"
|
Write-Host " Download (zip): $($result.download_url)"
|
||||||
if ($result.setup_url) { Write-Host " Or install: $($result.setup_url)" }
|
if ($result.setup_url) { Write-Host " Or install: $($result.setup_url)" }
|
||||||
Write-Host " To apply: .\packaging\check_update.ps1 -Apply"
|
Write-Host " To apply: .\packaging\check_update.ps1 -Apply"
|
||||||
$result | ConvertTo-Json -Compress | Write-Output
|
if (-not $Quiet) {
|
||||||
|
$result | ConvertTo-Json -Compress | Write-Output
|
||||||
|
}
|
||||||
exit 10
|
exit 10
|
||||||
}
|
}
|
||||||
|
|
||||||
$dlDir = Join-Path $data "backups\downloads"
|
if ($AutoOnly -and -not $autoEnabled) {
|
||||||
if (-not (Test-Path $dlDir)) {
|
Write-Host "==> Update available but NETX_UPDATE_AUTO is not enabled; skip apply"
|
||||||
New-Item -ItemType Directory -Path $dlDir -Force | Out-Null
|
if (-not $Quiet) {
|
||||||
}
|
$result | ConvertTo-Json -Compress | Write-Output
|
||||||
$zipName = "NetX-$latest-win64.zip"
|
|
||||||
$zipPath = Join-Path $dlDir $zipName
|
|
||||||
Write-Host "==> Downloading $zipName ..."
|
|
||||||
Invoke-WebRequest -Uri $result.download_url -OutFile $zipPath -UseBasicParsing
|
|
||||||
if ($result.sha256 -and $result.sha256 -notmatch 'REPLACE') {
|
|
||||||
$hash = (Get-FileHash -Path $zipPath -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
||||||
$expect = $result.sha256.ToLowerInvariant()
|
|
||||||
if ($hash -ne $expect) {
|
|
||||||
throw "sha256_mismatch: got $hash expected $expect"
|
|
||||||
}
|
}
|
||||||
Write-Host "==> SHA256 OK"
|
exit 10
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "==> Applying update via update_netx.ps1"
|
$lockFile = Join-Path $data "data\runtime\update.lock"
|
||||||
& powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "update_netx.ps1") `
|
$lockDir = Split-Path -Parent $lockFile
|
||||||
-PackagePath $zipPath -ProgramRoot $prog -DataRoot $data
|
if (-not (Test-Path $lockDir)) {
|
||||||
Write-Host "==> Update applied to $latest" -ForegroundColor Green
|
New-Item -ItemType Directory -Path $lockDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
if (Test-Path $lockFile) {
|
||||||
|
$ageHrs = ((Get-Date) - (Get-Item $lockFile).LastWriteTime).TotalHours
|
||||||
|
if ($ageHrs -lt 2) {
|
||||||
|
Write-Host "==> Another update appears in progress ($lockFile); abort"
|
||||||
|
exit 3
|
||||||
|
}
|
||||||
|
Remove-Item -Force $lockFile -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
Set-Content -Path $lockFile -Value (Get-Date).ToString("o") -Encoding ascii
|
||||||
|
|
||||||
|
try {
|
||||||
|
$dlDir = Join-Path $data "backups\downloads"
|
||||||
|
if (-not (Test-Path $dlDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $dlDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
$zipName = "NetX-$latest-win64.zip"
|
||||||
|
$zipPath = Join-Path $dlDir $zipName
|
||||||
|
Write-Host "==> Downloading $zipName ..."
|
||||||
|
Invoke-WebRequest -Uri $result.download_url -OutFile $zipPath -UseBasicParsing
|
||||||
|
if ($result.sha256 -and $result.sha256 -notmatch 'REPLACE' -and $result.sha256.Trim()) {
|
||||||
|
$hash = (Get-FileHash -Path $zipPath -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||||
|
$expect = $result.sha256.ToLowerInvariant()
|
||||||
|
if ($hash -ne $expect) {
|
||||||
|
throw "sha256_mismatch: got $hash expected $expect"
|
||||||
|
}
|
||||||
|
Write-Host "==> SHA256 OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "==> Applying update via update_netx.ps1"
|
||||||
|
& powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "update_netx.ps1") `
|
||||||
|
-PackagePath $zipPath -ProgramRoot $prog -DataRoot $data
|
||||||
|
Write-Host "==> Update applied to $latest" -ForegroundColor Green
|
||||||
|
} finally {
|
||||||
|
Remove-Item -Force $lockFile -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
exit 0
|
exit 0
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,4 @@
|
||||||
# NETX_UI_DIST_DIR=web/dist
|
# NETX_UI_DIST_DIR=web/dist
|
||||||
# NETX_UPDATE_URL=
|
# NETX_UPDATE_URL=
|
||||||
# NETX_UPDATE_CHANNEL=stable
|
# NETX_UPDATE_CHANNEL=stable
|
||||||
|
# NETX_UPDATE_AUTO=false
|
||||||
|
|
|
||||||
127
packaging/install_service.ps1
Normal file
127
packaging/install_service.ps1
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
param(
|
||||||
|
[string]$ProgramRoot = "",
|
||||||
|
[string]$DataRoot = "",
|
||||||
|
[ValidateSet("winsw", "task")]
|
||||||
|
[string]$Mode = "winsw",
|
||||||
|
[switch]$Uninstall = $false,
|
||||||
|
[switch]$Start = $false
|
||||||
|
)
|
||||||
|
|
||||||
|
# Install NetX as a Windows Service (WinSW) or as a SYSTEM startup Scheduled Task.
|
||||||
|
# Requires elevation for winsw / task modes that run as SYSTEM.
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
. "$PSScriptRoot\_common.ps1"
|
||||||
|
|
||||||
|
$prog = Get-NetxProgramRoot -Override $ProgramRoot
|
||||||
|
$data = Get-NetxDataRoot -ProgramRoot $prog -Override $DataRoot
|
||||||
|
$svcName = "NetX"
|
||||||
|
$winswDir = Join-Path $prog "packaging\winsw"
|
||||||
|
$winswExe = Join-Path $winswDir "NetX.exe"
|
||||||
|
$winswXml = Join-Path $winswDir "NetX.xml"
|
||||||
|
$cacheDir = Join-Path $PSScriptRoot "cache"
|
||||||
|
|
||||||
|
function Test-IsAdmin {
|
||||||
|
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||||
|
$p = New-Object Security.Principal.WindowsPrincipal($id)
|
||||||
|
return $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-WinSWPath([string]$Path) {
|
||||||
|
return ($Path -replace '\\', '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-WinSW {
|
||||||
|
if (-not (Test-Path $winswDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $winswDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
if (-not (Test-Path $cacheDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $cacheDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
$dl = Join-Path $cacheDir "WinSW-x64.exe"
|
||||||
|
if (-not (Test-Path $dl)) {
|
||||||
|
$url = "https://github.com/winsw/winsw/releases/download/v2.12.0/WinSW-x64.exe"
|
||||||
|
Write-Host "==> Downloading WinSW: $url"
|
||||||
|
Invoke-WebRequest -Uri $url -OutFile $dl -UseBasicParsing
|
||||||
|
}
|
||||||
|
Copy-Item -Path $dl -Destination $winswExe -Force
|
||||||
|
|
||||||
|
$runPs1 = ConvertTo-WinSWPath (Join-Path $PSScriptRoot "service_run.ps1")
|
||||||
|
$stopPs1 = ConvertTo-WinSWPath (Join-Path $PSScriptRoot "stop_netx_app.ps1")
|
||||||
|
$progFs = ConvertTo-WinSWPath $prog
|
||||||
|
$dataFs = ConvertTo-WinSWPath $data
|
||||||
|
$logPath = ConvertTo-WinSWPath (Join-Path $data "data\runtime")
|
||||||
|
if (-not (Test-Path (Join-Path $data "data\runtime"))) {
|
||||||
|
New-Item -ItemType Directory -Path (Join-Path $data "data\runtime") -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$xml = @"
|
||||||
|
<service>
|
||||||
|
<id>$svcName</id>
|
||||||
|
<name>NetX Ops</name>
|
||||||
|
<description>NetX API, workers, and optional bundled PostgreSQL</description>
|
||||||
|
<executable>powershell.exe</executable>
|
||||||
|
<arguments>-NoProfile -ExecutionPolicy Bypass -File "$runPs1" -ProgramRoot "$progFs" -DataRoot "$dataFs"</arguments>
|
||||||
|
<logpath>$logPath</logpath>
|
||||||
|
<log mode="roll-by-size">
|
||||||
|
<sizeThreshold>10240</sizeThreshold>
|
||||||
|
<keepFiles>4</keepFiles>
|
||||||
|
</log>
|
||||||
|
<onfailure action="restart" delay="10 sec"/>
|
||||||
|
<onfailure action="restart" delay="30 sec"/>
|
||||||
|
<resetfailure>1 hour</resetfailure>
|
||||||
|
<stoptimeout>60sec</stoptimeout>
|
||||||
|
<stopexecutable>powershell.exe</stopexecutable>
|
||||||
|
<stoparguments>-NoProfile -ExecutionPolicy Bypass -File "$stopPs1" -ProgramRoot "$progFs" -DataRoot "$dataFs"</stoparguments>
|
||||||
|
<workingdirectory>$progFs</workingdirectory>
|
||||||
|
</service>
|
||||||
|
"@
|
||||||
|
# WinSW expects UTF-8 without BOM issues; ASCII-compatible paths preferred.
|
||||||
|
[System.IO.File]::WriteAllText($winswXml, $xml)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Uninstall) {
|
||||||
|
if (-not (Test-IsAdmin)) { throw "admin_required_for_uninstall" }
|
||||||
|
if (Test-Path $winswExe) {
|
||||||
|
Write-Host "==> Stopping/uninstalling WinSW service"
|
||||||
|
& $winswExe stop 2>$null
|
||||||
|
& $winswExe uninstall 2>$null
|
||||||
|
}
|
||||||
|
Unregister-ScheduledTask -TaskName "NetXService" -TaskPath "\NetX\" -Confirm:$false -ErrorAction SilentlyContinue
|
||||||
|
Write-Host "==> Service/task removed"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-IsAdmin)) {
|
||||||
|
throw "admin_required: run elevated PowerShell to install the NetX service"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Mode -eq "winsw") {
|
||||||
|
Ensure-WinSW
|
||||||
|
Write-Host "==> Installing Windows Service via WinSW"
|
||||||
|
& $winswExe stop 2>$null
|
||||||
|
& $winswExe uninstall 2>$null
|
||||||
|
& $winswExe install
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "winsw_install_failed" }
|
||||||
|
if ($Start) {
|
||||||
|
& $winswExe start
|
||||||
|
Write-Host "==> Service started" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "==> Installed. Start with: Start-Service NetX or `"$winswExe`" start"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "==> Registering Scheduled Task NetX\NetXService (At startup, SYSTEM)"
|
||||||
|
$runPs1 = Join-Path $PSScriptRoot "service_run.ps1"
|
||||||
|
$arg = "-NoProfile -ExecutionPolicy Bypass -File `"$runPs1`" -ProgramRoot `"$prog`" -DataRoot `"$data`""
|
||||||
|
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $arg -WorkingDirectory $prog
|
||||||
|
$trigger = New-ScheduledTaskTrigger -AtStartup
|
||||||
|
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
|
||||||
|
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1)
|
||||||
|
Register-ScheduledTask -TaskName "NetXService" -TaskPath "\NetX\" -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
|
||||||
|
if ($Start) {
|
||||||
|
Start-ScheduledTask -TaskName "NetXService" -TaskPath "\NetX\"
|
||||||
|
Write-Host "==> Task started" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "==> Task registered. Start with: Start-ScheduledTask -TaskPath '\NetX\' -TaskName NetXService"
|
||||||
|
}
|
||||||
|
}
|
||||||
57
packaging/install_update_task.ps1
Normal file
57
packaging/install_update_task.ps1
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
param(
|
||||||
|
[string]$ProgramRoot = "",
|
||||||
|
[string]$DataRoot = "",
|
||||||
|
[ValidateSet("Daily", "AtLogOn", "Hourly")]
|
||||||
|
[string]$Trigger = "Daily",
|
||||||
|
[string]$At = "03:30",
|
||||||
|
[switch]$Remove = $false,
|
||||||
|
[switch]$EnableAutoEnv = $true
|
||||||
|
)
|
||||||
|
|
||||||
|
# Schedule silent update checks. With NETX_UPDATE_AUTO=true, applies updates when available.
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
. "$PSScriptRoot\_common.ps1"
|
||||||
|
|
||||||
|
$prog = Get-NetxProgramRoot -Override $ProgramRoot
|
||||||
|
$data = Get-NetxDataRoot -ProgramRoot $prog -Override $DataRoot
|
||||||
|
$taskName = "NetXUpdate"
|
||||||
|
$taskPath = "\NetX\"
|
||||||
|
$checkPs1 = Join-Path $PSScriptRoot "check_update.ps1"
|
||||||
|
|
||||||
|
if ($Remove) {
|
||||||
|
Unregister-ScheduledTask -TaskName $taskName -TaskPath $taskPath -Confirm:$false -ErrorAction SilentlyContinue
|
||||||
|
Write-Host "==> Update task removed"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($EnableAutoEnv) {
|
||||||
|
$envFile = Join-Path $data ".env"
|
||||||
|
if (-not (Test-Path $data)) {
|
||||||
|
New-Item -ItemType Directory -Path $data -Force | Out-Null
|
||||||
|
}
|
||||||
|
Write-DotEnvValue -Path $envFile -Values @{ "NETX_UPDATE_AUTO" = "true" }
|
||||||
|
Write-Host "==> Set NETX_UPDATE_AUTO=true in $envFile"
|
||||||
|
}
|
||||||
|
|
||||||
|
$arg = "-NoProfile -ExecutionPolicy Bypass -File `"$checkPs1`" -ProgramRoot `"$prog`" -DataRoot `"$data`" -Apply -Quiet -AutoOnly"
|
||||||
|
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $arg -WorkingDirectory $prog
|
||||||
|
|
||||||
|
switch ($Trigger) {
|
||||||
|
"Hourly" {
|
||||||
|
$trig = New-ScheduledTaskTrigger -Once -At (Get-Date).Date.AddHours((Get-Date).Hour + 1) `
|
||||||
|
-RepetitionInterval (New-TimeSpan -Hours 1) -RepetitionDuration ([TimeSpan]::MaxValue)
|
||||||
|
}
|
||||||
|
"AtLogOn" {
|
||||||
|
$trig = New-ScheduledTaskTrigger -AtLogOn
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
$trig = New-ScheduledTaskTrigger -Daily -At $At
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable
|
||||||
|
$principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive -RunLevel Limited
|
||||||
|
Register-ScheduledTask -TaskName $taskName -TaskPath $taskPath -Action $action -Trigger $trig -Settings $settings -Principal $principal -Force | Out-Null
|
||||||
|
Write-Host "==> Scheduled update task: $taskPath$taskName ($Trigger)" -ForegroundColor Green
|
||||||
|
Write-Host " Manual run: .\packaging\check_update.ps1 -Apply -Quiet -AutoOnly"
|
||||||
|
|
@ -60,13 +60,17 @@ Name: "{group}\Stop NetX"; Filename: "powershell.exe"; Parameters: "-ExecutionPo
|
||||||
Name: "{group}\Check for updates"; Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\check_update.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}"
|
Name: "{group}\Check for updates"; Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\check_update.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}"
|
||||||
Name: "{group}\Open NetX UI"; Filename: "http://127.0.0.1:8890/"
|
Name: "{group}\Open NetX UI"; Filename: "http://127.0.0.1:8890/"
|
||||||
Name: "{group}\First-time setup"; Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\setup_first_run.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}"
|
Name: "{group}\First-time setup"; Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\setup_first_run.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}"
|
||||||
|
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}"
|
||||||
|
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}"
|
||||||
Name: "{group}\Enable start at logon"; Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\install_autostart.ps1"" -ProgramRoot ""{app}"""; WorkingDir: "{app}"
|
Name: "{group}\Enable start at logon"; Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\install_autostart.ps1"" -ProgramRoot ""{app}"""; WorkingDir: "{app}"
|
||||||
Name: "{autodesktop}\NetX"; Filename: "powershell.exe"; Parameters: "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File ""{app}\packaging\netx_tray.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"" -StartOnLaunch"; WorkingDir: "{app}"; Tasks: desktopicon
|
Name: "{autodesktop}\NetX"; Filename: "powershell.exe"; Parameters: "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File ""{app}\packaging\netx_tray.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"" -StartOnLaunch"; WorkingDir: "{app}"; Tasks: desktopicon
|
||||||
|
|
||||||
[Run]
|
[Run]
|
||||||
Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\setup_first_run.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}"; Flags: postinstall skipifsilent; Tasks: firstrun
|
Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\setup_first_run.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}"; Flags: postinstall skipifsilent; Tasks: firstrun
|
||||||
Filename: "powershell.exe"; Parameters: "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File ""{app}\packaging\netx_tray.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"" -StartOnLaunch"; WorkingDir: "{app}"; Description: "Start NetX tray now"; Flags: postinstall nowait skipifsilent unchecked
|
Filename: "powershell.exe"; Parameters: "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File ""{app}\packaging\netx_tray.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"" -StartOnLaunch"; WorkingDir: "{app}"; Description: "Start NetX tray now"; Flags: postinstall nowait skipifsilent unchecked
|
||||||
Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\install_autostart.ps1"" -ProgramRoot ""{app}"""; WorkingDir: "{app}"; Description: "Start NetX at Windows logon"; Flags: postinstall skipifsilent unchecked
|
Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\install_autostart.ps1"" -ProgramRoot ""{app}"""; WorkingDir: "{app}"; Description: "Start NetX tray at Windows logon"; Flags: postinstall skipifsilent unchecked
|
||||||
|
Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\install_update_task.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}"; Description: "Enable daily silent auto-update"; Flags: postinstall skipifsilent unchecked
|
||||||
|
Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\install_service.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"" -Start"; WorkingDir: "{app}"; Description: "Install as Windows Service (admin)"; Flags: postinstall skipifsilent unchecked
|
||||||
|
|
||||||
[UninstallDelete]
|
[UninstallDelete]
|
||||||
; Do NOT delete {commonappdata}\NetX — preserves DB and secrets across reinstall
|
; Do NOT delete {commonappdata}\NetX — preserves DB and secrets across reinstall
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ param(
|
||||||
[string]$ProgramRoot = "",
|
[string]$ProgramRoot = "",
|
||||||
[string]$DataRoot = "",
|
[string]$DataRoot = "",
|
||||||
[switch]$StartOnLaunch = $true,
|
[switch]$StartOnLaunch = $true,
|
||||||
[switch]$CheckUpdateOnLaunch = $false
|
[switch]$CheckUpdateOnLaunch = $false,
|
||||||
|
[switch]$AutoUpdate = $false
|
||||||
)
|
)
|
||||||
|
|
||||||
# Simple system-tray controller for NetX (Windows packaged installs).
|
# Simple system-tray controller for NetX (Windows packaged installs).
|
||||||
|
|
@ -19,11 +20,19 @@ $data = Get-NetxDataRoot -ProgramRoot $prog -Override $DataRoot
|
||||||
$hostBind = "127.0.0.1"
|
$hostBind = "127.0.0.1"
|
||||||
$port = 8890
|
$port = 8890
|
||||||
$envPath = Join-Path $data ".env"
|
$envPath = Join-Path $data ".env"
|
||||||
|
$map = @{}
|
||||||
if (Test-Path $envPath) {
|
if (Test-Path $envPath) {
|
||||||
$map = Read-DotEnv -Path $envPath
|
$map = Read-DotEnv -Path $envPath
|
||||||
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 {} }
|
||||||
}
|
}
|
||||||
|
$autoVal = ""
|
||||||
|
if ($map["NETX_UPDATE_AUTO"]) { $autoVal = $map["NETX_UPDATE_AUTO"] }
|
||||||
|
elseif ($env:NETX_UPDATE_AUTO) { $autoVal = $env:NETX_UPDATE_AUTO }
|
||||||
|
if ($autoVal -match '^(1|true|yes|on)$') {
|
||||||
|
$AutoUpdate = $true
|
||||||
|
$CheckUpdateOnLaunch = $true
|
||||||
|
}
|
||||||
$uiUrl = "http://${hostBind}:${port}/"
|
$uiUrl = "http://${hostBind}:${port}/"
|
||||||
$ver = Get-NetxVersion -ProgramRoot $prog
|
$ver = Get-NetxVersion -ProgramRoot $prog
|
||||||
|
|
||||||
|
|
@ -112,17 +121,23 @@ $notify.ContextMenuStrip = $menu
|
||||||
$notify.add_DoubleClick({ Start-Process $uiUrl })
|
$notify.add_DoubleClick({ Start-Process $uiUrl })
|
||||||
|
|
||||||
$form.add_Shown({
|
$form.add_Shown({
|
||||||
if ($StartOnLaunch) {
|
if ($AutoUpdate) {
|
||||||
Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser")
|
$notify.ShowBalloonTip(4000, "NetX", "Checking for updates…", [System.Windows.Forms.ToolTipIcon]::Info)
|
||||||
Start-Sleep -Seconds 2
|
Start-Process -FilePath "powershell.exe" -ArgumentList @(
|
||||||
try { Start-Process $uiUrl } catch {}
|
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $checkPs1,
|
||||||
}
|
"-ProgramRoot", $prog, "-DataRoot", $data, "-Apply", "-Quiet", "-AutoOnly"
|
||||||
if ($CheckUpdateOnLaunch) {
|
) -Wait -WindowStyle Hidden
|
||||||
|
} elseif ($CheckUpdateOnLaunch) {
|
||||||
Start-Process -FilePath "powershell.exe" -ArgumentList @(
|
Start-Process -FilePath "powershell.exe" -ArgumentList @(
|
||||||
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $checkPs1,
|
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $checkPs1,
|
||||||
"-ProgramRoot", $prog, "-DataRoot", $data, "-Quiet"
|
"-ProgramRoot", $prog, "-DataRoot", $data, "-Quiet"
|
||||||
) -WindowStyle Hidden
|
) -WindowStyle Hidden
|
||||||
}
|
}
|
||||||
|
if ($StartOnLaunch) {
|
||||||
|
Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser")
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
try { Start-Process $uiUrl } catch {}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
$form.add_FormClosing({
|
$form.add_FormClosing({
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ param(
|
||||||
[switch]$SkipBuild = $false,
|
[switch]$SkipBuild = $false,
|
||||||
[switch]$SkipInstaller = $false,
|
[switch]$SkipInstaller = $false,
|
||||||
[switch]$SkipGitHub = $false,
|
[switch]$SkipGitHub = $false,
|
||||||
[switch]$Draft = $false
|
[switch]$Draft = $false,
|
||||||
|
[switch]$Sign = $false
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
@ -51,6 +52,17 @@ if (-not $SkipInstaller) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($Sign) {
|
||||||
|
$toSign = @()
|
||||||
|
if (Test-Path $setup) { $toSign += $setup }
|
||||||
|
if ($toSign.Count -eq 0) {
|
||||||
|
Write-Host "[WARN] -Sign set but no Setup.exe to sign"
|
||||||
|
} else {
|
||||||
|
Write-Host "==> Signing release artifacts"
|
||||||
|
& powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "sign_release.ps1") -Files $toSign
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($SkipGitHub) {
|
if ($SkipGitHub) {
|
||||||
Write-Host "==> Skip GitHub release (-SkipGitHub)"
|
Write-Host "==> Skip GitHub release (-SkipGitHub)"
|
||||||
exit 0
|
exit 0
|
||||||
|
|
|
||||||
62
packaging/service_run.ps1
Normal file
62
packaging/service_run.ps1
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
param(
|
||||||
|
[string]$ProgramRoot = "",
|
||||||
|
[string]$DataRoot = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Long-running supervisor for Windows Service / Task Scheduler.
|
||||||
|
# Starts NetX (and bundled PG), waits until stopped, then tears down.
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
. "$PSScriptRoot\_common.ps1"
|
||||||
|
|
||||||
|
$prog = Get-NetxProgramRoot -Override $ProgramRoot
|
||||||
|
$data = Get-NetxDataRoot -ProgramRoot $prog -Override $DataRoot
|
||||||
|
$logDir = Join-Path $data "data\runtime"
|
||||||
|
if (-not (Test-Path $logDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
$logFile = Join-Path $logDir "service_run.log"
|
||||||
|
|
||||||
|
function Write-SvcLog([string]$Msg) {
|
||||||
|
$line = "{0} {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Msg
|
||||||
|
Add-Content -Path $logFile -Value $line -Encoding utf8
|
||||||
|
Write-Host $line
|
||||||
|
}
|
||||||
|
|
||||||
|
$stopFlag = Join-Path $logDir "service.stop"
|
||||||
|
Remove-Item -Force $stopFlag -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
Write-SvcLog "service_run starting program=$prog data=$data"
|
||||||
|
|
||||||
|
$startPs1 = Join-Path $PSScriptRoot "start_netx_app.ps1"
|
||||||
|
$stopPs1 = Join-Path $PSScriptRoot "stop_netx_app.ps1"
|
||||||
|
|
||||||
|
try {
|
||||||
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $startPs1 `
|
||||||
|
-ProgramRoot $prog -DataRoot $data -SkipBrowser
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "start_netx_app_failed exit=$LASTEXITCODE"
|
||||||
|
}
|
||||||
|
Write-SvcLog "NetX started; entering watch loop"
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
if (Test-Path $stopFlag) {
|
||||||
|
Write-SvcLog "stop flag detected"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
Start-Sleep -Seconds 5
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-SvcLog "ERROR: $($_.Exception.Message)"
|
||||||
|
throw
|
||||||
|
} finally {
|
||||||
|
Write-SvcLog "stopping NetX"
|
||||||
|
try {
|
||||||
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $stopPs1 `
|
||||||
|
-ProgramRoot $prog -DataRoot $data
|
||||||
|
} catch {
|
||||||
|
Write-SvcLog "stop warning: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
Remove-Item -Force $stopFlag -ErrorAction SilentlyContinue
|
||||||
|
Write-SvcLog "service_run exited"
|
||||||
|
}
|
||||||
78
packaging/sign_release.ps1
Normal file
78
packaging/sign_release.ps1
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string[]]$Files,
|
||||||
|
[string]$Thumbprint = "",
|
||||||
|
[string]$PfxPath = "",
|
||||||
|
[string]$PfxPassword = "",
|
||||||
|
[string]$TimestampUrl = "http://timestamp.digicert.com",
|
||||||
|
[string]$Description = "NetX"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sign release artifacts with signtool (Authenticode).
|
||||||
|
# Requires Windows SDK / signtool.exe and a code-signing certificate.
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# .\sign_release.ps1 -Files .\packaging\release\NetX-Setup-0.4.0.exe -Thumbprint ABCDEF...
|
||||||
|
# .\sign_release.ps1 -Files .\packaging\release\*.exe -PfxPath .\certs\netx.pfx -PfxPassword ***
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function Find-SignTool {
|
||||||
|
$cmd = Get-Command signtool.exe -ErrorAction SilentlyContinue
|
||||||
|
if ($cmd) { return $cmd.Source }
|
||||||
|
$roots = @(
|
||||||
|
"${env:ProgramFiles(x86)}\Windows Kits\10\bin",
|
||||||
|
"${env:ProgramFiles}\Windows Kits\10\bin"
|
||||||
|
)
|
||||||
|
foreach ($root in $roots) {
|
||||||
|
if (-not (Test-Path $root)) { continue }
|
||||||
|
$hit = Get-ChildItem -Path $root -Recurse -Filter signtool.exe -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { $_.FullName -match '\\x64\\signtool\.exe$' } |
|
||||||
|
Select-Object -First 1
|
||||||
|
if ($hit) { return $hit.FullName }
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$signtool = Find-SignTool
|
||||||
|
if (-not $signtool) {
|
||||||
|
throw "signtool_not_found: install Windows SDK or add signtool.exe to PATH"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $Thumbprint -and -not $PfxPath) {
|
||||||
|
if ($env:NETX_SIGN_THUMBPRINT) { $Thumbprint = $env:NETX_SIGN_THUMBPRINT }
|
||||||
|
if ($env:NETX_SIGN_PFX) { $PfxPath = $env:NETX_SIGN_PFX }
|
||||||
|
if ($env:NETX_SIGN_PFX_PASSWORD) { $PfxPassword = $env:NETX_SIGN_PFX_PASSWORD }
|
||||||
|
}
|
||||||
|
if (-not $Thumbprint -and -not $PfxPath) {
|
||||||
|
throw "provide -Thumbprint or -PfxPath (or NETX_SIGN_THUMBPRINT / NETX_SIGN_PFX)"
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolved = @()
|
||||||
|
foreach ($pattern in $Files) {
|
||||||
|
$resolved += @(Resolve-Path -Path $pattern -ErrorAction Stop)
|
||||||
|
}
|
||||||
|
if ($resolved.Count -eq 0) { throw "no_files_to_sign" }
|
||||||
|
|
||||||
|
foreach ($f in $resolved) {
|
||||||
|
$path = $f.Path
|
||||||
|
Write-Host "==> Signing $path"
|
||||||
|
$args = @(
|
||||||
|
"sign", "/fd", "SHA256", "/td", "SHA256", "/tr", $TimestampUrl,
|
||||||
|
"/d", $Description
|
||||||
|
)
|
||||||
|
if ($PfxPath) {
|
||||||
|
$args += @("/f", $PfxPath)
|
||||||
|
if ($PfxPassword) { $args += @("/p", $PfxPassword) }
|
||||||
|
} else {
|
||||||
|
$args += @("/sha1", $Thumbprint)
|
||||||
|
}
|
||||||
|
$args += $path
|
||||||
|
& $signtool @args
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "sign_failed: $path" }
|
||||||
|
& $signtool verify /pa $path
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "verify_failed: $path" }
|
||||||
|
Write-Host " OK" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "==> Done. Without a trusted CA certificate, Windows SmartScreen may still warn."
|
||||||
Loading…
Add table
Add a link
Reference in a new issue