From e40615dad53126522773a2279cbd805475eb5042 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 26 May 2026 06:44:35 -0400 Subject: [PATCH] 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. --- scripts/install/Install-Services.ps1 | 136 +++++++++++++++---------- scripts/install/Refresh-Services.ps1 | 22 ++-- scripts/install/Uninstall-Services.ps1 | 13 +-- 3 files changed, 103 insertions(+), 68 deletions(-) diff --git a/scripts/install/Install-Services.ps1 b/scripts/install/Install-Services.ps1 index 15a5b0a..32e6bc3 100644 --- a/scripts/install/Install-Services.ps1 +++ b/scripts/install/Install-Services.ps1 @@ -1,46 +1,63 @@ <# .SYNOPSIS - Registers the v2 Windows services on a node: OtOpcUa (main server, net10) and - optionally OtOpcUaWonderwareHistorian (Wonderware historian sidecar). + Registers the v2 Windows service on a node: OtOpcUaHost (fused binary, .NET 10) + and optionally OtOpcUaWonderwareHistorian (Wonderware historian sidecar, net48 x86). .DESCRIPTION - PR 7.2 retired the legacy out-of-process OtOpcUaGalaxyHost service alongside the - GalaxyProxyDriver / GalaxyHost / GalaxyShared projects. Galaxy access now flows - through the in-process GalaxyDriver talking gRPC to a separately-installed - mxaccessgw. The mxaccessgw server runs out of its own repo - (`c:\Users\dohertj2\Desktop\mxaccessgw\`) — see - `docs/v2/Galaxy.ParityRig.md` for the gw setup recipe. + v2 consolidates the legacy OtOpcUa + OtOpcUaAdmin services into a single role-gated + OtOpcUaHost binary. The -Roles parameter sets the OTOPCUA_ROLES service env so + Program.cs decides what to mount (admin / driver / both). The Wonderware historian + sidecar logic is unchanged from v1; install it with -InstallWonderwareHistorian. + + Galaxy access flows through the mxaccessgw sibling repo (separate service); see + docs/v2/Galaxy.ParityRig.md for the gateway setup. .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 - 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 - Gate the OtOpcUaWonderwareHistorian sidecar install. Off by default; set when - the deployment uses the Wonderware historian for history reads + alarm-event - persistence. + Gate the OtOpcUaWonderwareHistorian sidecar install. Off by default; set when the + deployment uses the Wonderware historian for history reads + alarm-event persistence. .PARAMETER HistorianSharedSecret - Per-process secret passed to the Historian sidecar via env var. Generated - freshly per install when not supplied. + Per-process secret passed to the historian sidecar via env var. Generated freshly + per install when not supplied. .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 - .\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 #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$InstallRoot, [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 - # OtOpcUaWonderwareHistorian service. Secret + pipe defaults match the server's - # Historian:Wonderware appsettings block. + # Wonderware historian sidecar. Optional; gates the OtOpcUaWonderwareHistorian + # service. Secret + pipe defaults match the server's Historian:Wonderware appsettings. [switch]$InstallWonderwareHistorian, [string]$HistorianSharedSecret, [string]$HistorianPipeName = 'OtOpcUaWonderwareHistorian', @@ -51,18 +68,19 @@ param( $ErrorActionPreference = 'Stop' -if (-not (Test-Path "$InstallRoot\OtOpcUa.Server.exe")) { - Write-Error "OtOpcUa.Server.exe not found at $InstallRoot — copy the publish output first" +if (-not (Test-Path "$InstallRoot\OtOpcUa.Host.exe")) { + Write-Error "OtOpcUa.Host.exe not found at $InstallRoot — copy the publish output first" exit 1 } -# Generate fresh shared secrets per install if not supplied. function New-SharedSecret { $bytes = New-Object byte[] 32 [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($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")) { 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 } -# --- Install OtOpcUaWonderwareHistorian (PR 3.W) — separate sidecar that exposes the -# 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. +# --- OtOpcUaWonderwareHistorian sidecar (optional, unchanged from v1) ------- $historianDepend = $null if ($InstallWonderwareHistorian) { $historianEnv = @( @@ -87,14 +102,10 @@ if ($InstallWonderwareHistorian) { "OTOPCUA_ALLOWED_SID=$sid" "OTOPCUA_HISTORIAN_SECRET=$HistorianSharedSecret" "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_SERVER=$HistorianServer" "OTOPCUA_HISTORIAN_PORT=$HistorianPort" - ) -join "`0" - $historianEnv += "`0`0" + ) Write-Host "Installing OtOpcUaWonderwareHistorian..." & 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 $svcKey = "HKLM:\SYSTEM\CurrentControlSet\Services\OtOpcUaWonderwareHistorian" - $envValue = $historianEnv.Split("`0") | Where-Object { $_ -ne '' } - Set-ItemProperty -Path $svcKey -Name 'Environment' -Type MultiString -Value $envValue + Set-ItemProperty -Path $svcKey -Name 'Environment' -Type MultiString -Value $historianEnv + + & sc.exe failure OtOpcUaWonderwareHistorian reset= 86400 actions= restart/5000/restart/30000/restart/60000 | Out-Null $historianDepend = 'OtOpcUaWonderwareHistorian' } -# --- Install OtOpcUa. Galaxy access flows through GalaxyDriver → mxaccessgw (gRPC), -# so OtOpcUa no longer depends on a sibling service for Galaxy connectivity. The -# mxaccessgw is installed separately. When the Wonderware sidecar is installed, -# depend on it for startup ordering. -$otOpcUaDepends = @() -if ($historianDepend) { $otOpcUaDepends += $historianDepend } +# --- OtOpcUaHost (the fused v2 binary) -------------------------------------- +$normalisedRoles = ($Roles -split ',' | ForEach-Object { $_.Trim() } | Sort-Object -Unique) -join ',' -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 = @( - 'create', 'OtOpcUa', - 'binPath=', "`"$InstallRoot\OtOpcUa.Server.exe`"", - 'DisplayName=', 'OtOpcUa Server', + 'create', 'OtOpcUaHost', + 'binPath=', "`"$InstallRoot\OtOpcUa.Host.exe`"", + 'DisplayName=', "OtOpcUa Host ($normalisedRoles)", 'start=', 'auto', 'obj=', $ServiceAccount ) -if ($otOpcUaDepends.Count -gt 0) { - $createArgs += @('depend=', ($otOpcUaDepends -join '/')) +if ($hostDepends.Count -gt 0) { + $createArgs += @('depend=', ($hostDepends -join '/')) } & 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 "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" } -Write-Host " sc.exe start OtOpcUa" +Write-Host " sc.exe start OtOpcUaHost" if ($InstallWonderwareHistorian) { Write-Host "" Write-Host "Wonderware historian shared secret (configure into appsettings.json Historian:Wonderware:SharedSecret):" @@ -142,5 +176,5 @@ if ($InstallWonderwareHistorian) { } Write-Host "" 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 " section of appsettings.json (default endpoint http://localhost:5120)." +Write-Host " per docs/v2/Galaxy.ParityRig.md. OtOpcUaHost connects via the" +Write-Host " Galaxy.Gateway section of appsettings.json (default http://localhost:5120)." diff --git a/scripts/install/Refresh-Services.ps1 b/scripts/install/Refresh-Services.ps1 index 1b7a62e..bd81e10 100644 --- a/scripts/install/Refresh-Services.ps1 +++ b/scripts/install/Refresh-Services.ps1 @@ -43,11 +43,11 @@ function Test-NssmService([string]$Name) { # 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) { - Run { nssm stop $name } "stop $name" + Run { Stop-Service $name -Force -ErrorAction SilentlyContinue } "stop $name" } else { Write-Host " ($name not installed; skipping)" -ForegroundColor DarkGray @@ -56,7 +56,7 @@ foreach ($name in @('OtOpcUa', 'OtOpcUaWonderwareHistorian', 'MxAccessGw')) { if (-not $WhatIf) { 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 { Write-Host " killing residual process $($_.ProcessName) (PID=$($_.Id))" -ForegroundColor DarkYellow Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue @@ -109,14 +109,14 @@ Run { # 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 { - & 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 & dotnet publish "$RepoRoot\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware" ` -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 @@ -143,16 +143,16 @@ if (Test-NssmService 'OtOpcUaWonderwareHistorian') { # Step 6: Start in forward dependency order # ------------------------------------------------------------------------ -Step "Starting services (MxAccessGw → OtOpcUaWonderwareHistorian → OtOpcUa)" +Step "Starting services (MxAccessGw > OtOpcUaWonderwareHistorian > OtOpcUaHost)" foreach ($pair in @( @{ Name = 'MxAccessGw'; Wait = 4 }, @{ Name = 'OtOpcUaWonderwareHistorian'; Wait = 4 }, - @{ Name = 'OtOpcUa'; Wait = 8 } + @{ Name = 'OtOpcUaHost'; Wait = 8 } )) { $name = $pair.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 } } else { @@ -167,7 +167,7 @@ foreach ($pair in @( Step "Smoke verification" if (-not $WhatIf) { - foreach ($name in @('MxAccessGw', 'OtOpcUaWonderwareHistorian', 'OtOpcUa')) { + foreach ($name in @('MxAccessGw', 'OtOpcUaWonderwareHistorian', 'OtOpcUaHost')) { if (Test-NssmService $name) { $status = (Get-Service $name).Status $color = if ($status -eq 'Running') { 'Green' } else { 'Red' } diff --git a/scripts/install/Uninstall-Services.ps1 b/scripts/install/Uninstall-Services.ps1 index f5c8206..2fd172b 100644 --- a/scripts/install/Uninstall-Services.ps1 +++ b/scripts/install/Uninstall-Services.ps1 @@ -3,16 +3,17 @@ Stops + removes the v2 services. Mirrors Install-Services.ps1. .DESCRIPTION - PR 7.2 retired the legacy OtOpcUaGalaxyHost service. Galaxy access now flows - through the in-process GalaxyDriver against a separately-installed mxaccessgw. - OtOpcUaGalaxyHost is included in the cleanup loop below so this script safely - removes it from any rig still carrying the legacy service from a pre-7.2 - install. + Removes the v2 OtOpcUaHost service plus the optional OtOpcUaWonderwareHistorian + sidecar. Also cleans up legacy service names from prior installs: + - OtOpcUa (v1 server) — replaced by OtOpcUaHost in v2 + - OtOpcUaAdmin (v1 admin) — fused into OtOpcUaHost in v2 + - OtOpcUaGalaxyHost (pre-7.2 Galaxy host) — long-retired #> [CmdletBinding()] param() $ErrorActionPreference = 'Continue' -foreach ($svc in 'OtOpcUa', 'OtOpcUaWonderwareHistorian', 'OtOpcUaGalaxyHost') { +foreach ($svc in 'OtOpcUaHost', 'OtOpcUaWonderwareHistorian', + 'OtOpcUa', 'OtOpcUaAdmin', 'OtOpcUaGalaxyHost') { if (Get-Service $svc -ErrorAction SilentlyContinue) { Write-Host "Stopping $svc..." Stop-Service $svc -Force -ErrorAction SilentlyContinue