<# .SYNOPSIS Registers the two v2 Windows services on a node: OtOpcUa (main server, net10) and OtOpcUaGalaxyHost (out-of-process Galaxy COM host, net48 x86). .DESCRIPTION Phase 2 Stream D.2 — replaces the v1 single-service install (TopShelf-based OtOpcUa.Host). Installs both services with the correct service-account SID + per-process shared secret provisioning per `driver-stability.md §"IPC Security"`. Galaxy.Host depends on OtOpcUa (Galaxy.Host must be reachable when OtOpcUa starts; service dependency wiring + retry handled by OtOpcUa.Server NodeBootstrap). .PARAMETER InstallRoot Where the binaries live (typically C:\Program Files\OtOpcUa). .PARAMETER ServiceAccount Service account SID or DOMAIN\name. Both services run under this account; the Galaxy.Host pipe ACL only allows this SID to connect (decision #76). .PARAMETER GalaxySharedSecret Per-process secret passed to Galaxy.Host via env var. Generated freshly per install. .PARAMETER ZbConnection Galaxy ZB SQL connection string (passed to Galaxy.Host via env var). .EXAMPLE .\Install-Services.ps1 -InstallRoot 'C:\Program Files\OtOpcUa' -ServiceAccount 'OTOPCUA\svc-otopcua' #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$InstallRoot, [Parameter(Mandatory)] [string]$ServiceAccount, [string]$GalaxySharedSecret, [string]$ZbConnection = 'Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;', [string]$GalaxyClientName = 'OtOpcUa-Galaxy.Host', [string]$GalaxyPipeName = 'OtOpcUaGalaxy' ) $ErrorActionPreference = 'Stop' if (-not (Test-Path "$InstallRoot\OtOpcUa.Server.exe")) { Write-Error "OtOpcUa.Server.exe not found at $InstallRoot — copy the publish output first" exit 1 } if (-not (Test-Path "$InstallRoot\Galaxy\OtOpcUa.Driver.Galaxy.Host.exe")) { Write-Error "OtOpcUa.Driver.Galaxy.Host.exe not found at $InstallRoot\Galaxy — copy the publish output first" exit 1 } # Generate a fresh shared secret per install if not supplied. Stored in DPAPI-protected file # rather than the registry so the service account can read it but other local users cannot. if (-not $GalaxySharedSecret) { $bytes = New-Object byte[] 32 [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes) $GalaxySharedSecret = [Convert]::ToBase64String($bytes) } # Resolve the SID — the IPC ACL needs the SID, not the down-level name. $sid = if ($ServiceAccount.StartsWith('S-1-')) { $ServiceAccount } else { (New-Object System.Security.Principal.NTAccount $ServiceAccount).Translate([System.Security.Principal.SecurityIdentifier]).Value } # --- Install OtOpcUaGalaxyHost first (OtOpcUa starts after, depends on it being up). $galaxyEnv = @( "OTOPCUA_GALAXY_PIPE=$GalaxyPipeName" "OTOPCUA_ALLOWED_SID=$sid" "OTOPCUA_GALAXY_SECRET=$GalaxySharedSecret" "OTOPCUA_GALAXY_BACKEND=mxaccess" "OTOPCUA_GALAXY_ZB_CONN=$ZbConnection" "OTOPCUA_GALAXY_CLIENT_NAME=$GalaxyClientName" ) -join "`0" $galaxyEnv += "`0`0" Write-Host "Installing OtOpcUaGalaxyHost..." & sc.exe create OtOpcUaGalaxyHost binPath= "`"$InstallRoot\Galaxy\OtOpcUa.Driver.Galaxy.Host.exe`"" ` DisplayName= 'OtOpcUa Galaxy Host (out-of-process MXAccess)' ` start= auto ` obj= $ServiceAccount | Out-Null # Set per-service environment variables via the registry — sc.exe doesn't expose them directly. $svcKey = "HKLM:\SYSTEM\CurrentControlSet\Services\OtOpcUaGalaxyHost" $envValue = $galaxyEnv.Split("`0") | Where-Object { $_ -ne '' } Set-ItemProperty -Path $svcKey -Name 'Environment' -Type MultiString -Value $envValue # --- Install OtOpcUa (depends on Galaxy host being installed; doesn't strictly require it # started — OtOpcUa.Server NodeBootstrap retries on the IPC connect path). Write-Host "Installing OtOpcUa..." & sc.exe create OtOpcUa binPath= "`"$InstallRoot\OtOpcUa.Server.exe`"" ` DisplayName= 'OtOpcUa Server' ` start= auto ` depend= 'OtOpcUaGalaxyHost' ` obj= $ServiceAccount | Out-Null Write-Host "" Write-Host "Installed. Start with:" Write-Host " sc.exe start OtOpcUaGalaxyHost" Write-Host " sc.exe start OtOpcUa" Write-Host "" Write-Host "Galaxy shared secret (record this offline — required for service rebinding):" Write-Host " $GalaxySharedSecret"