feat(install): rewrite Install/Refresh/Uninstall-Services.ps1 for v2 fused Host (Task 62)

- Install-Services.ps1: installs OtOpcUaHost (single fused binary) replacing
  the v1 OtOpcUa + OtOpcUaAdmin pair. Required -Roles param writes OTOPCUA_ROLES
  to the service env so Program.cs decides what to mount (admin / driver / both).
  -HttpPort param (default 9000) writes ASPNETCORE_URLS on admin-role nodes.
  sc.exe restart-on-failure: 5s, 30s, 60s; reset counter after 24h clean run.
  Wonderware historian sidecar install logic preserved from v1.

- Uninstall-Services.ps1: removes OtOpcUaHost + cleans up legacy v1 names
  (OtOpcUa, OtOpcUaAdmin) and the long-retired OtOpcUaGalaxyHost.

- Refresh-Services.ps1: updated service names (OtOpcUa -> OtOpcUaHost), publish
  path (ZB.MOM.WW.OtOpcUa.Server -> ZB.MOM.WW.OtOpcUa.Host), process names
  (OtOpcUa.Server -> OtOpcUa.Host). Switched nssm stop/start calls to
  Stop-Service/Start-Service so the script works whether the underlying
  service was installed via nssm or sc.exe.
This commit is contained in:
Joseph Doherty
2026-05-26 06:44:35 -04:00
parent 1689901c0e
commit e40615dad5
3 changed files with 103 additions and 68 deletions

View File

@@ -1,46 +1,63 @@
<# <#
.SYNOPSIS .SYNOPSIS
Registers the v2 Windows services on a node: OtOpcUa (main server, net10) and Registers the v2 Windows service on a node: OtOpcUaHost (fused binary, .NET 10)
optionally OtOpcUaWonderwareHistorian (Wonderware historian sidecar). and optionally OtOpcUaWonderwareHistorian (Wonderware historian sidecar, net48 x86).
.DESCRIPTION .DESCRIPTION
PR 7.2 retired the legacy out-of-process OtOpcUaGalaxyHost service alongside the v2 consolidates the legacy OtOpcUa + OtOpcUaAdmin services into a single role-gated
GalaxyProxyDriver / GalaxyHost / GalaxyShared projects. Galaxy access now flows OtOpcUaHost binary. The -Roles parameter sets the OTOPCUA_ROLES service env so
through the in-process GalaxyDriver talking gRPC to a separately-installed Program.cs decides what to mount (admin / driver / both). The Wonderware historian
mxaccessgw. The mxaccessgw server runs out of its own repo sidecar logic is unchanged from v1; install it with -InstallWonderwareHistorian.
(`c:\Users\dohertj2\Desktop\mxaccessgw\`) — see
`docs/v2/Galaxy.ParityRig.md` for the gw setup recipe. Galaxy access flows through the mxaccessgw sibling repo (separate service); see
docs/v2/Galaxy.ParityRig.md for the gateway setup.
.PARAMETER InstallRoot .PARAMETER InstallRoot
Where the binaries live (typically C:\Program Files\OtOpcUa). Where the binaries live (typically C:\Program Files\OtOpcUa). The OtOpcUaHost
service runs OtOpcUa.Host.exe from this directory; publish the Host project there
with `dotnet publish -c Release -r win-x64 --self-contained` first.
.PARAMETER ServiceAccount .PARAMETER ServiceAccount
Service account SID or DOMAIN\name. The OtOpcUa service runs under this account. Service account SID or DOMAIN\name. The OtOpcUaHost service runs under this account.
.PARAMETER Roles
Comma-separated cluster roles for this node. One of:
- "admin,driver" — single-node dev or all-in-one production node
- "admin" — admin-only HA pair member (Blazor + control-plane singletons)
- "driver" — driver-only node (OPC UA endpoint + per-node actors)
Written to the service env as OTOPCUA_ROLES.
.PARAMETER HttpPort
HTTP port for the AdminUI + auth endpoints. Default 9000. Written as ASPNETCORE_URLS.
Ignored on driver-only nodes (no Blazor surface).
.PARAMETER InstallWonderwareHistorian .PARAMETER InstallWonderwareHistorian
Gate the OtOpcUaWonderwareHistorian sidecar install. Off by default; set when Gate the OtOpcUaWonderwareHistorian sidecar install. Off by default; set when the
the deployment uses the Wonderware historian for history reads + alarm-event deployment uses the Wonderware historian for history reads + alarm-event persistence.
persistence.
.PARAMETER HistorianSharedSecret .PARAMETER HistorianSharedSecret
Per-process secret passed to the Historian sidecar via env var. Generated Per-process secret passed to the historian sidecar via env var. Generated freshly
freshly per install when not supplied. per install when not supplied.
.EXAMPLE .EXAMPLE
.\Install-Services.ps1 -InstallRoot 'C:\Program Files\OtOpcUa' -ServiceAccount 'OTOPCUA\svc-otopcua' .\Install-Services.ps1 -InstallRoot 'C:\Program Files\OtOpcUa' `
-ServiceAccount 'OTOPCUA\svc-otopcua' -Roles 'admin,driver'
.EXAMPLE .EXAMPLE
.\Install-Services.ps1 -InstallRoot 'C:\Program Files\OtOpcUa' -ServiceAccount 'OTOPCUA\svc-otopcua' ` .\Install-Services.ps1 -InstallRoot 'C:\Program Files\OtOpcUa' `
-ServiceAccount 'OTOPCUA\svc-otopcua' -Roles 'driver' `
-InstallWonderwareHistorian -InstallWonderwareHistorian
#> #>
[CmdletBinding()] [CmdletBinding()]
param( param(
[Parameter(Mandatory)] [string]$InstallRoot, [Parameter(Mandatory)] [string]$InstallRoot,
[Parameter(Mandatory)] [string]$ServiceAccount, [Parameter(Mandatory)] [string]$ServiceAccount,
[Parameter(Mandatory)] [ValidateSet('admin', 'driver', 'admin,driver', 'driver,admin')]
[string]$Roles,
[int]$HttpPort = 9000,
# PR 3.W — Wonderware historian sidecar. Optional; gates the # Wonderware historian sidecar. Optional; gates the OtOpcUaWonderwareHistorian
# OtOpcUaWonderwareHistorian service. Secret + pipe defaults match the server's # service. Secret + pipe defaults match the server's Historian:Wonderware appsettings.
# Historian:Wonderware appsettings block.
[switch]$InstallWonderwareHistorian, [switch]$InstallWonderwareHistorian,
[string]$HistorianSharedSecret, [string]$HistorianSharedSecret,
[string]$HistorianPipeName = 'OtOpcUaWonderwareHistorian', [string]$HistorianPipeName = 'OtOpcUaWonderwareHistorian',
@@ -51,18 +68,19 @@ param(
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
if (-not (Test-Path "$InstallRoot\OtOpcUa.Server.exe")) { if (-not (Test-Path "$InstallRoot\OtOpcUa.Host.exe")) {
Write-Error "OtOpcUa.Server.exe not found at $InstallRoot — copy the publish output first" Write-Error "OtOpcUa.Host.exe not found at $InstallRoot — copy the publish output first"
exit 1 exit 1
} }
# Generate fresh shared secrets per install if not supplied.
function New-SharedSecret { function New-SharedSecret {
$bytes = New-Object byte[] 32 $bytes = New-Object byte[] 32
[System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes) [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)
return [Convert]::ToBase64String($bytes) return [Convert]::ToBase64String($bytes)
} }
if ($InstallWonderwareHistorian -and -not $HistorianSharedSecret) { $HistorianSharedSecret = New-SharedSecret } if ($InstallWonderwareHistorian -and -not $HistorianSharedSecret) {
$HistorianSharedSecret = New-SharedSecret
}
if ($InstallWonderwareHistorian -and -not (Test-Path "$InstallRoot\WonderwareHistorian\OtOpcUa.Driver.Historian.Wonderware.exe")) { if ($InstallWonderwareHistorian -and -not (Test-Path "$InstallRoot\WonderwareHistorian\OtOpcUa.Driver.Historian.Wonderware.exe")) {
Write-Error "OtOpcUa.Driver.Historian.Wonderware.exe not found at $InstallRoot\WonderwareHistorian — copy the publish output first" Write-Error "OtOpcUa.Driver.Historian.Wonderware.exe not found at $InstallRoot\WonderwareHistorian — copy the publish output first"
@@ -76,10 +94,7 @@ $sid = if ($ServiceAccount.StartsWith('S-1-')) {
(New-Object System.Security.Principal.NTAccount $ServiceAccount).Translate([System.Security.Principal.SecurityIdentifier]).Value (New-Object System.Security.Principal.NTAccount $ServiceAccount).Translate([System.Security.Principal.SecurityIdentifier]).Value
} }
# --- Install OtOpcUaWonderwareHistorian (PR 3.W) — separate sidecar that exposes the # --- OtOpcUaWonderwareHistorian sidecar (optional, unchanged from v1) -------
# Wonderware Historian SDK via a named-pipe protocol consumed by the .NET 10 server.
# Optional: only installed when -InstallWonderwareHistorian is supplied. Depends on the
# hard AVEVA services that host the historian SDK runtime path.
$historianDepend = $null $historianDepend = $null
if ($InstallWonderwareHistorian) { if ($InstallWonderwareHistorian) {
$historianEnv = @( $historianEnv = @(
@@ -87,14 +102,10 @@ if ($InstallWonderwareHistorian) {
"OTOPCUA_ALLOWED_SID=$sid" "OTOPCUA_ALLOWED_SID=$sid"
"OTOPCUA_HISTORIAN_SECRET=$HistorianSharedSecret" "OTOPCUA_HISTORIAN_SECRET=$HistorianSharedSecret"
"OTOPCUA_HISTORIAN_ENABLED=true" "OTOPCUA_HISTORIAN_ENABLED=true"
# Default-on when the historian sidecar is installed; flip to false for a
# read-only deployment that still loads aahClientManaged for reads but
# rejects WriteAlarmEvents frames.
"OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED=true" "OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED=true"
"OTOPCUA_HISTORIAN_SERVER=$HistorianServer" "OTOPCUA_HISTORIAN_SERVER=$HistorianServer"
"OTOPCUA_HISTORIAN_PORT=$HistorianPort" "OTOPCUA_HISTORIAN_PORT=$HistorianPort"
) -join "`0" )
$historianEnv += "`0`0"
Write-Host "Installing OtOpcUaWonderwareHistorian..." Write-Host "Installing OtOpcUaWonderwareHistorian..."
& sc.exe create OtOpcUaWonderwareHistorian binPath= "`"$InstallRoot\WonderwareHistorian\OtOpcUa.Driver.Historian.Wonderware.exe`"" ` & sc.exe create OtOpcUaWonderwareHistorian binPath= "`"$InstallRoot\WonderwareHistorian\OtOpcUa.Driver.Historian.Wonderware.exe`"" `
@@ -105,36 +116,59 @@ if ($InstallWonderwareHistorian) {
& sc.exe config OtOpcUaWonderwareHistorian start= delayed-auto | Out-Null & sc.exe config OtOpcUaWonderwareHistorian start= delayed-auto | Out-Null
$svcKey = "HKLM:\SYSTEM\CurrentControlSet\Services\OtOpcUaWonderwareHistorian" $svcKey = "HKLM:\SYSTEM\CurrentControlSet\Services\OtOpcUaWonderwareHistorian"
$envValue = $historianEnv.Split("`0") | Where-Object { $_ -ne '' } Set-ItemProperty -Path $svcKey -Name 'Environment' -Type MultiString -Value $historianEnv
Set-ItemProperty -Path $svcKey -Name 'Environment' -Type MultiString -Value $envValue
& sc.exe failure OtOpcUaWonderwareHistorian reset= 86400 actions= restart/5000/restart/30000/restart/60000 | Out-Null
$historianDepend = 'OtOpcUaWonderwareHistorian' $historianDepend = 'OtOpcUaWonderwareHistorian'
} }
# --- Install OtOpcUa. Galaxy access flows through GalaxyDriver → mxaccessgw (gRPC), # --- OtOpcUaHost (the fused v2 binary) --------------------------------------
# so OtOpcUa no longer depends on a sibling service for Galaxy connectivity. The $normalisedRoles = ($Roles -split ',' | ForEach-Object { $_.Trim() } | Sort-Object -Unique) -join ','
# mxaccessgw is installed separately. When the Wonderware sidecar is installed,
# depend on it for startup ordering.
$otOpcUaDepends = @()
if ($historianDepend) { $otOpcUaDepends += $historianDepend }
Write-Host "Installing OtOpcUa..." $hasAdmin = $normalisedRoles -split ',' -contains 'admin'
$hostEnv = @(
"OTOPCUA_ROLES=$normalisedRoles",
'DOTNET_ENVIRONMENT=Production'
)
if ($hasAdmin) {
$hostEnv += "ASPNETCORE_URLS=http://+:$HttpPort"
}
$hostDepends = @()
if ($historianDepend) { $hostDepends += $historianDepend }
Write-Host "Installing OtOpcUaHost (roles=$normalisedRoles)..."
$createArgs = @( $createArgs = @(
'create', 'OtOpcUa', 'create', 'OtOpcUaHost',
'binPath=', "`"$InstallRoot\OtOpcUa.Server.exe`"", 'binPath=', "`"$InstallRoot\OtOpcUa.Host.exe`"",
'DisplayName=', 'OtOpcUa Server', 'DisplayName=', "OtOpcUa Host ($normalisedRoles)",
'start=', 'auto', 'start=', 'auto',
'obj=', $ServiceAccount 'obj=', $ServiceAccount
) )
if ($otOpcUaDepends.Count -gt 0) { if ($hostDepends.Count -gt 0) {
$createArgs += @('depend=', ($otOpcUaDepends -join '/')) $createArgs += @('depend=', ($hostDepends -join '/'))
} }
& sc.exe @createArgs | Out-Null & sc.exe @createArgs | Out-Null
# Env block via registry MultiString (sc.exe doesn't take env directly).
$svcKey = "HKLM:\SYSTEM\CurrentControlSet\Services\OtOpcUaHost"
Set-ItemProperty -Path $svcKey -Name 'Environment' -Type MultiString -Value $hostEnv
# Restart-on-failure: 5s, 30s, 60s; reset counter after a clean 24h run.
& sc.exe failure OtOpcUaHost reset= 86400 actions= restart/5000/restart/30000/restart/60000 | Out-Null
Write-Host "" Write-Host ""
Write-Host "Installed. Start with:" Write-Host "Installed OtOpcUaHost:"
Write-Host " Roles: $normalisedRoles"
if ($hasAdmin) { Write-Host " HTTP port: $HttpPort" }
Write-Host " Binary: $InstallRoot\OtOpcUa.Host.exe"
Write-Host " Account: $ServiceAccount"
Write-Host ""
Write-Host "Start with:"
if ($InstallWonderwareHistorian) { Write-Host " sc.exe start OtOpcUaWonderwareHistorian" } if ($InstallWonderwareHistorian) { Write-Host " sc.exe start OtOpcUaWonderwareHistorian" }
Write-Host " sc.exe start OtOpcUa" Write-Host " sc.exe start OtOpcUaHost"
if ($InstallWonderwareHistorian) { if ($InstallWonderwareHistorian) {
Write-Host "" Write-Host ""
Write-Host "Wonderware historian shared secret (configure into appsettings.json Historian:Wonderware:SharedSecret):" Write-Host "Wonderware historian shared secret (configure into appsettings.json Historian:Wonderware:SharedSecret):"
@@ -142,5 +176,5 @@ if ($InstallWonderwareHistorian) {
} }
Write-Host "" Write-Host ""
Write-Host "NOTE: Galaxy access flows through mxaccessgw — install + run that separately" Write-Host "NOTE: Galaxy access flows through mxaccessgw — install + run that separately"
Write-Host " per docs/v2/Galaxy.ParityRig.md. OtOpcUa connects via the Galaxy.Gateway" Write-Host " per docs/v2/Galaxy.ParityRig.md. OtOpcUaHost connects via the"
Write-Host " section of appsettings.json (default endpoint http://localhost:5120)." Write-Host " Galaxy.Gateway section of appsettings.json (default http://localhost:5120)."

View File

@@ -43,11 +43,11 @@ function Test-NssmService([string]$Name) {
# Step 1: Stop in reverse dependency order # Step 1: Stop in reverse dependency order
# ------------------------------------------------------------------------ # ------------------------------------------------------------------------
Step "Stopping services (OtOpcUa OtOpcUaWonderwareHistorian MxAccessGw)" Step "Stopping services (OtOpcUaHost > OtOpcUaWonderwareHistorian > MxAccessGw)"
foreach ($name in @('OtOpcUa', 'OtOpcUaWonderwareHistorian', 'MxAccessGw')) { foreach ($name in @('OtOpcUaHost', 'OtOpcUaWonderwareHistorian', 'MxAccessGw')) {
if (Test-NssmService $name) { if (Test-NssmService $name) {
Run { nssm stop $name } "stop $name" Run { Stop-Service $name -Force -ErrorAction SilentlyContinue } "stop $name"
} }
else { else {
Write-Host " ($name not installed; skipping)" -ForegroundColor DarkGray Write-Host " ($name not installed; skipping)" -ForegroundColor DarkGray
@@ -56,7 +56,7 @@ foreach ($name in @('OtOpcUa', 'OtOpcUaWonderwareHistorian', 'MxAccessGw')) {
if (-not $WhatIf) { if (-not $WhatIf) {
Start-Sleep -Seconds 3 Start-Sleep -Seconds 3
Get-Process MxGateway.Server, MxGateway.Worker, OtOpcUa.Server, OtOpcUa.Driver.Historian.Wonderware -ErrorAction SilentlyContinue | Get-Process MxGateway.Server, MxGateway.Worker, OtOpcUa.Host, OtOpcUa.Driver.Historian.Wonderware -ErrorAction SilentlyContinue |
ForEach-Object { ForEach-Object {
Write-Host " killing residual process $($_.ProcessName) (PID=$($_.Id))" -ForegroundColor DarkYellow Write-Host " killing residual process $($_.ProcessName) (PID=$($_.Id))" -ForegroundColor DarkYellow
Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
@@ -109,14 +109,14 @@ Run {
# Step 4: Refresh OtOpcUa + Wonderware historian sidecar # Step 4: Refresh OtOpcUa + Wonderware historian sidecar
# ------------------------------------------------------------------------ # ------------------------------------------------------------------------
Step "Publishing OtOpcUa server + Wonderware historian sidecar from $RepoRoot" Step "Publishing OtOpcUa.Host + Wonderware historian sidecar from $RepoRoot"
Run { Run {
& dotnet publish "$RepoRoot\src\Server\ZB.MOM.WW.OtOpcUa.Server" ` & dotnet publish "$RepoRoot\src\Server\ZB.MOM.WW.OtOpcUa.Host" `
-c Release -o (Join-Path $PublishRoot "lmxopcua") | Out-Null -c Release -o (Join-Path $PublishRoot "lmxopcua") | Out-Null
& dotnet publish "$RepoRoot\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware" ` & dotnet publish "$RepoRoot\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware" `
-c Release -o (Join-Path $PublishRoot "lmxopcua\WonderwareHistorian") | Out-Null -c Release -o (Join-Path $PublishRoot "lmxopcua\WonderwareHistorian") | Out-Null
} "dotnet publish (Server + sidecar)" } "dotnet publish (Host + sidecar)"
# ------------------------------------------------------------------------ # ------------------------------------------------------------------------
# Step 5: Service env block — ensure OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED # Step 5: Service env block — ensure OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED
@@ -143,16 +143,16 @@ if (Test-NssmService 'OtOpcUaWonderwareHistorian') {
# Step 6: Start in forward dependency order # Step 6: Start in forward dependency order
# ------------------------------------------------------------------------ # ------------------------------------------------------------------------
Step "Starting services (MxAccessGw OtOpcUaWonderwareHistorian OtOpcUa)" Step "Starting services (MxAccessGw > OtOpcUaWonderwareHistorian > OtOpcUaHost)"
foreach ($pair in @( foreach ($pair in @(
@{ Name = 'MxAccessGw'; Wait = 4 }, @{ Name = 'MxAccessGw'; Wait = 4 },
@{ Name = 'OtOpcUaWonderwareHistorian'; Wait = 4 }, @{ Name = 'OtOpcUaWonderwareHistorian'; Wait = 4 },
@{ Name = 'OtOpcUa'; Wait = 8 } @{ Name = 'OtOpcUaHost'; Wait = 8 }
)) { )) {
$name = $pair.Name $name = $pair.Name
if (Test-NssmService $name) { if (Test-NssmService $name) {
Run { nssm start $name } "start $name" Run { Start-Service $name } "start $name"
if (-not $WhatIf) { Start-Sleep -Seconds $pair.Wait } if (-not $WhatIf) { Start-Sleep -Seconds $pair.Wait }
} }
else { else {
@@ -167,7 +167,7 @@ foreach ($pair in @(
Step "Smoke verification" Step "Smoke verification"
if (-not $WhatIf) { if (-not $WhatIf) {
foreach ($name in @('MxAccessGw', 'OtOpcUaWonderwareHistorian', 'OtOpcUa')) { foreach ($name in @('MxAccessGw', 'OtOpcUaWonderwareHistorian', 'OtOpcUaHost')) {
if (Test-NssmService $name) { if (Test-NssmService $name) {
$status = (Get-Service $name).Status $status = (Get-Service $name).Status
$color = if ($status -eq 'Running') { 'Green' } else { 'Red' } $color = if ($status -eq 'Running') { 'Green' } else { 'Red' }

View File

@@ -3,16 +3,17 @@
Stops + removes the v2 services. Mirrors Install-Services.ps1. Stops + removes the v2 services. Mirrors Install-Services.ps1.
.DESCRIPTION .DESCRIPTION
PR 7.2 retired the legacy OtOpcUaGalaxyHost service. Galaxy access now flows Removes the v2 OtOpcUaHost service plus the optional OtOpcUaWonderwareHistorian
through the in-process GalaxyDriver against a separately-installed mxaccessgw. sidecar. Also cleans up legacy service names from prior installs:
OtOpcUaGalaxyHost is included in the cleanup loop below so this script safely - OtOpcUa (v1 server) — replaced by OtOpcUaHost in v2
removes it from any rig still carrying the legacy service from a pre-7.2 - OtOpcUaAdmin (v1 admin) — fused into OtOpcUaHost in v2
install. - OtOpcUaGalaxyHost (pre-7.2 Galaxy host) — long-retired
#> #>
[CmdletBinding()] param() [CmdletBinding()] param()
$ErrorActionPreference = 'Continue' $ErrorActionPreference = 'Continue'
foreach ($svc in 'OtOpcUa', 'OtOpcUaWonderwareHistorian', 'OtOpcUaGalaxyHost') { foreach ($svc in 'OtOpcUaHost', 'OtOpcUaWonderwareHistorian',
'OtOpcUa', 'OtOpcUaAdmin', 'OtOpcUaGalaxyHost') {
if (Get-Service $svc -ErrorAction SilentlyContinue) { if (Get-Service $svc -ErrorAction SilentlyContinue) {
Write-Host "Stopping $svc..." Write-Host "Stopping $svc..."
Stop-Service $svc -Force -ErrorAction SilentlyContinue Stop-Service $svc -Force -ErrorAction SilentlyContinue