Add Windows service, silent auto-update, and code-signing hooks.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-10-06 23:53:07 +08:00
parent fe9df32024
commit b639f01c0f
13 changed files with 463 additions and 31 deletions

View file

@ -122,6 +122,8 @@ NETX_UME_NOTIFICATION_TOPIC=ALARM
# NETX_BUNDLED_PG_DATA_DIR=
# NETX_UPDATE_URL=
# 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.
# One-click start (recommended): .\scripts\start_netx.ps1 -Background -WithWeb
# -> API + netx_api.worker + N× netx_api.biz_state_worker + optional Vite

2
.gitignore vendored
View file

@ -20,4 +20,6 @@ data/runtime/
packaging/cache/
packaging/release/
packaging/postgres/pgsql/
packaging/winsw/
*.exe
!packaging/installer/*.iss

View file

@ -108,6 +108,40 @@ Default source: latest GitHub Release for `hansjone/netx`. Optional custom manif
# 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
```

View file

@ -72,8 +72,9 @@ Copy-Item -Path (Join-Path $PSScriptRoot "_common.ps1") -Destination $packOut -F
foreach ($name in @(
"download_postgres.ps1", "setup_first_run.ps1", "start_netx_app.ps1",
"stop_netx_app.ps1", "update_netx.ps1", "check_update.ps1",
"netx_tray.ps1", "install_autostart.ps1", "build_release.ps1",
"publish_release.ps1", "README.md", "manifest.example.json"
"netx_tray.ps1", "install_autostart.ps1", "install_service.ps1",
"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
if (Test-Path $src) { Copy-Item $src (Join-Path $packOut $name) -Force }

View file

@ -4,7 +4,9 @@ param(
[string]$UpdateUrl = "",
[string]$Channel = "",
[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.
@ -116,33 +118,68 @@ if (-not $result.update_available) {
Write-Host "==> Update available: $current -> $latest" -ForegroundColor Cyan
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) {
Write-Host " Download (zip): $($result.download_url)"
if ($result.setup_url) { Write-Host " Or install: $($result.setup_url)" }
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
}
$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') {
$hash = (Get-FileHash -Path $zipPath -Algorithm SHA256).Hash.ToLowerInvariant()
$expect = $result.sha256.ToLowerInvariant()
if ($hash -ne $expect) {
throw "sha256_mismatch: got $hash expected $expect"
if ($AutoOnly -and -not $autoEnabled) {
Write-Host "==> Update available but NETX_UPDATE_AUTO is not enabled; skip apply"
if (-not $Quiet) {
$result | ConvertTo-Json -Compress | Write-Output
}
Write-Host "==> SHA256 OK"
exit 10
}
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
$lockFile = Join-Path $data "data\runtime\update.lock"
$lockDir = Split-Path -Parent $lockFile
if (-not (Test-Path $lockDir)) {
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

View file

@ -7,3 +7,4 @@
# NETX_UI_DIST_DIR=web/dist
# NETX_UPDATE_URL=
# NETX_UPDATE_CHANNEL=stable
# NETX_UPDATE_AUTO=false

View 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"
}
}

View 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"

View file

@ -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}\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}\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: "{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]
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: "-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]
; Do NOT delete {commonappdata}\NetX — preserves DB and secrets across reinstall

View file

@ -2,7 +2,8 @@ param(
[string]$ProgramRoot = "",
[string]$DataRoot = "",
[switch]$StartOnLaunch = $true,
[switch]$CheckUpdateOnLaunch = $false
[switch]$CheckUpdateOnLaunch = $false,
[switch]$AutoUpdate = $false
)
# 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"
$port = 8890
$envPath = Join-Path $data ".env"
$map = @{}
if (Test-Path $envPath) {
$map = Read-DotEnv -Path $envPath
if ($map["NETX_HOST"]) { $hostBind = $map["NETX_HOST"] }
if ($map["NETX_PORT"]) { try { $port = [int]$map["NETX_PORT"] } catch {} }
}
$autoVal = ""
if ($map["NETX_UPDATE_AUTO"]) { $autoVal = $map["NETX_UPDATE_AUTO"] }
elseif ($env:NETX_UPDATE_AUTO) { $autoVal = $env:NETX_UPDATE_AUTO }
if ($autoVal -match '^(1|true|yes|on)$') {
$AutoUpdate = $true
$CheckUpdateOnLaunch = $true
}
$uiUrl = "http://${hostBind}:${port}/"
$ver = Get-NetxVersion -ProgramRoot $prog
@ -112,17 +121,23 @@ $notify.ContextMenuStrip = $menu
$notify.add_DoubleClick({ Start-Process $uiUrl })
$form.add_Shown({
if ($StartOnLaunch) {
Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser")
Start-Sleep -Seconds 2
try { Start-Process $uiUrl } catch {}
}
if ($CheckUpdateOnLaunch) {
if ($AutoUpdate) {
$notify.ShowBalloonTip(4000, "NetX", "Checking for updates…", [System.Windows.Forms.ToolTipIcon]::Info)
Start-Process -FilePath "powershell.exe" -ArgumentList @(
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $checkPs1,
"-ProgramRoot", $prog, "-DataRoot", $data, "-Apply", "-Quiet", "-AutoOnly"
) -Wait -WindowStyle Hidden
} elseif ($CheckUpdateOnLaunch) {
Start-Process -FilePath "powershell.exe" -ArgumentList @(
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $checkPs1,
"-ProgramRoot", $prog, "-DataRoot", $data, "-Quiet"
) -WindowStyle Hidden
}
if ($StartOnLaunch) {
Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser")
Start-Sleep -Seconds 2
try { Start-Process $uiUrl } catch {}
}
})
$form.add_FormClosing({

View file

@ -3,7 +3,8 @@ param(
[switch]$SkipBuild = $false,
[switch]$SkipInstaller = $false,
[switch]$SkipGitHub = $false,
[switch]$Draft = $false
[switch]$Draft = $false,
[switch]$Sign = $false
)
$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) {
Write-Host "==> Skip GitHub release (-SkipGitHub)"
exit 0

62
packaging/service_run.ps1 Normal file
View 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"
}

View 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."