<# .SYNOPSIS Registers the v2 Windows service on a node: OtOpcUaHost (fused binary, .NET 10) and optionally OtOpcUaWonderwareHistorian (Wonderware historian sidecar, net48 x86 — communicates over TCP, optionally TLS, instead of a Windows named pipe). .DESCRIPTION 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 is installed with -InstallWonderwareHistorian; it listens on a TCP port (default 32569, configurable via -HistorianTcpPort) protected by the shared secret. Optional TLS (-HistorianUseTls) is recommended for production cross-host deployments. 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). 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 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. .PARAMETER HistorianSharedSecret Per-process secret passed to the historian sidecar via env var. Generated freshly per install when not supplied. .PARAMETER HistorianTcpPort TCP port the sidecar listens on (OTOPCUA_HISTORIAN_TCP_PORT). Default 32569. A matching Windows Firewall inbound rule is created automatically. .PARAMETER HistorianBind IP address the sidecar binds to (OTOPCUA_HISTORIAN_BIND). Default 0.0.0.0 (all interfaces). Set to 127.0.0.1 for loopback-only on same-host deployments. .PARAMETER HistorianUseTls When set, enables TLS on the sidecar TCP listener. Supply -HistorianTlsCert with a certificate path or store thumbprint; recommended for cross-host production use. .PARAMETER HistorianTlsCert Certificate source for TLS. Either: - Absolute path to a .pfx file (e.g. C:\ProgramData\OtOpcUa\pki\historian.pfx), exported with MachineKeySet so the service account can read the private key; or - A LocalMachine\My store thumbprint (40-hex-char string, no spaces). Do NOT embed a cert path or thumbprint in automation scripts for unattended installs; pass it at call time or from a secrets vault. .PARAMETER HistorianTlsCertPassword Password for the .pfx file (if using a pfx path). Leave empty when using a store thumbprint. Never store passwords in scripts or source control. .EXAMPLE .\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' -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, # Wonderware historian sidecar. Optional; gates the OtOpcUaWonderwareHistorian # service. Secret defaults match the server's AlarmHistorian appsettings. [switch]$InstallWonderwareHistorian, [string]$HistorianSharedSecret, [string]$HistorianServer = 'localhost', [int]$HistorianPort = 32568, # SDK port (OTOPCUA_HISTORIAN_PORT) — Wonderware InSQL connection [int]$HistorianTcpPort = 32569, # TCP listen port for the IPC channel (OTOPCUA_HISTORIAN_TCP_PORT) [string]$HistorianBind = '0.0.0.0', [switch]$HistorianUseTls, [string]$HistorianTlsCert, [string]$HistorianTlsCertPassword, [string[]]$AvevaServiceDependencies = @('NmxSvc', 'aaBootstrap', 'aaGR') ) $ErrorActionPreference = 'Stop' if (-not (Test-Path "$InstallRoot\OtOpcUa.Host.exe")) { Write-Error "OtOpcUa.Host.exe not found at $InstallRoot — copy the publish output first" exit 1 } 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 (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" exit 1 } # --- OtOpcUaWonderwareHistorian sidecar (optional, TCP transport) ----------- $historianDepend = $null if ($InstallWonderwareHistorian) { # Build the TCP/TLS env block. # The Windows named-pipe transport and OTOPCUA_ALLOWED_SID have been retired; # the sidecar now listens on a TCP port protected by the shared secret. # TLS is optional but strongly recommended for cross-host production deployments. # # Cert provisioning for prod (when -HistorianUseTls): # Option A — pfx file: export with -KeyExportPolicy Exportable -KeyStorageFlags MachineKeySet; # place at e.g. C:\ProgramData\OtOpcUa\pki\historian.pfx; # ACL the file so the service account has Read access. # Option B — store thumbprint: import into LocalMachine\My; grant the service account # Read on the private key via certlm.msc → Manage Private Keys. # Either value is passed via -HistorianTlsCert at install time; NEVER hard-code it here. $historianEnv = @( "OTOPCUA_HISTORIAN_SECRET=$HistorianSharedSecret" "OTOPCUA_HISTORIAN_ENABLED=true" "OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED=true" "OTOPCUA_HISTORIAN_SERVER=$HistorianServer" "OTOPCUA_HISTORIAN_PORT=$HistorianPort" "OTOPCUA_HISTORIAN_TCP_PORT=$HistorianTcpPort" "OTOPCUA_HISTORIAN_BIND=$HistorianBind" ) if ($HistorianUseTls) { $historianEnv += "OTOPCUA_HISTORIAN_TLS_ENABLED=true" $historianEnv += "OTOPCUA_HISTORIAN_TLS_CERT=$HistorianTlsCert" if ($HistorianTlsCertPassword) { $historianEnv += "OTOPCUA_HISTORIAN_TLS_CERT_PASSWORD=$HistorianTlsCertPassword" } } else { $historianEnv += "OTOPCUA_HISTORIAN_TLS_ENABLED=false" } Write-Host "Installing OtOpcUaWonderwareHistorian..." & sc.exe create OtOpcUaWonderwareHistorian binPath= "`"$InstallRoot\WonderwareHistorian\OtOpcUa.Driver.Historian.Wonderware.exe`"" ` DisplayName= 'OtOpcUa Wonderware Historian Sidecar (out-of-process aahClient, TCP)' ` start= auto ` depend= ($AvevaServiceDependencies -join '/') ` obj= $ServiceAccount | Out-Null & sc.exe config OtOpcUaWonderwareHistorian start= delayed-auto | Out-Null $svcKey = "HKLM:\SYSTEM\CurrentControlSet\Services\OtOpcUaWonderwareHistorian" 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 # Idempotent Windows Firewall rule for the sidecar TCP port. $fwName = "OtOpcUa Wonderware Historian (TCP $HistorianTcpPort)" Get-NetFirewallRule -DisplayName $fwName -ErrorAction SilentlyContinue | Remove-NetFirewallRule -ErrorAction SilentlyContinue New-NetFirewallRule -DisplayName $fwName -Direction Inbound -Action Allow -Protocol TCP -LocalPort $HistorianTcpPort | Out-Null $historianDepend = 'OtOpcUaWonderwareHistorian' } # --- OtOpcUaHost (the fused v2 binary) -------------------------------------- $normalisedRoles = ($Roles -split ',' | ForEach-Object { $_.Trim() } | Sort-Object -Unique) -join ',' $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', 'OtOpcUaHost', 'binPath=', "`"$InstallRoot\OtOpcUa.Host.exe`"", 'DisplayName=', "OtOpcUa Host ($normalisedRoles)", 'start=', 'auto', 'obj=', $ServiceAccount ) 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 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 OtOpcUaHost" if ($InstallWonderwareHistorian) { Write-Host "" Write-Host "Wonderware historian shared secret (configure into appsettings.json AlarmHistorian:SharedSecret):" Write-Host " $HistorianSharedSecret" } Write-Host "" Write-Host "NOTE: Galaxy access flows through mxaccessgw — install + run that separately" Write-Host " per docs/v2/Galaxy.ParityRig.md. OtOpcUaHost connects via the" Write-Host " Galaxy.Gateway section of appsettings.json (default http://localhost:5120)."