diff --git a/docs/v2/dev-environment.md b/docs/v2/dev-environment.md index 0fa50d8..4e7eafb 100644 --- a/docs/v2/dev-environment.md +++ b/docs/v2/dev-environment.md @@ -408,6 +408,49 @@ For production: - Per-NodeId credentials in `ClusterNodeCredential` table (per decision #83) - Admin app uses LDAP (no SQL credential at all on the user-facing side) +## Service Refresh — `Refresh-Services.ps1` + +The deploy host hosts three NSSM-wrapped services (`MxAccessGw`, +`OtOpcUaWonderwareHistorian`, `OtOpcUa`) that consume binaries from +`C:\publish\`. After landing changes in either repo, refresh the +deployed bits with `scripts\install\Refresh-Services.ps1`: + +```powershell +# Default invocation (dev rig). +& C:\Users\dohertj2\Desktop\lmxopcua\scripts\install\Refresh-Services.ps1 + +# Skip the timestamped backup (faster on iterative dev cycles). +& Refresh-Services.ps1 -SkipBackup + +# Dry-run — print the actions without doing them. +& Refresh-Services.ps1 -WhatIf +``` + +The script: + +1. Stops services in reverse-dependency order (`OtOpcUa` → + `OtOpcUaWonderwareHistorian` → `MxAccessGw`) and force-kills + any residual processes. +2. Snapshots the existing `C:\publish\mxaccessgw\` and + `C:\publish\lmxopcua\` trees to `C:\publish\.backup-\` + for rollback (skip with `-SkipBackup`). +3. Builds + copies mxaccessgw worker (x86 net48) + server (net10.0) + binaries from the sibling repo. +4. `dotnet publish`-es the OtOpcUa server + Wonderware historian + sidecar from this repo. +5. Ensures `OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED=true` is set on + the historian service env block (PR C.2 toggle). +6. Starts services in forward-dependency order (`MxAccessGw` → + `OtOpcUaWonderwareHistorian` → `OtOpcUa`). +7. Smoke-verifies — service status, listening ports (5120 / 4840 / + 4841), recent log tails. + +Functional verification (alarm raise / scripted alarm historian +round-trip / sub-attribute fallback) is the operator's next step +after the refresh; see +[docs/plans/alarms-over-gateway.md](../plans/alarms-over-gateway.md) +§Track D for the scenarios. + ## Test Data Seed Each environment needs a baseline data set so cross-developer tests are reproducible. Lives in `tests/ZB.MOM.WW.OtOpcUa.IntegrationTests/SeedData/`: diff --git a/scripts/install/Refresh-Services.ps1 b/scripts/install/Refresh-Services.ps1 new file mode 100644 index 0000000..b462b00 --- /dev/null +++ b/scripts/install/Refresh-Services.ps1 @@ -0,0 +1,210 @@ +[CmdletBinding()] +param( + [string]$RepoRoot = "C:\Users\dohertj2\Desktop\lmxopcua", + [string]$GatewayRoot = "C:\Users\dohertj2\Desktop\mxaccessgw", + [string]$PublishRoot = "C:\publish", + [switch]$SkipBackup, + [switch]$WhatIf +) + +# PR D.1 — refresh C:\publish + restart services for the alarms-over-gateway +# epic. Stops services in reverse-dependency order (OtOpcUa → +# OtOpcUaWonderwareHistorian → MxAccessGw), refreshes binaries from the +# repos, then starts in forward order. A timestamped backup of the existing +# C:\publish trees lands under C:\publish\.backup-YYYY-MM-DD\ unless +# -SkipBackup is supplied. +# +# Designed to run as a single elevated PowerShell session on the deploy host +# (the dev rig today; production refresh is a separate runbook). + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +function Step([string]$Message) { + Write-Host "" + Write-Host "==> $Message" -ForegroundColor Cyan +} + +function Run([scriptblock]$Block, [string]$Description) { + if ($WhatIf) { + Write-Host " (skip) $Description" -ForegroundColor DarkYellow + return + } + Write-Host " $Description" + & $Block +} + +function Test-NssmService([string]$Name) { + $svc = Get-Service -Name $Name -ErrorAction SilentlyContinue + return $null -ne $svc +} + +# ------------------------------------------------------------------------ +# Step 1: Stop in reverse dependency order +# ------------------------------------------------------------------------ + +Step "Stopping services (OtOpcUa → OtOpcUaWonderwareHistorian → MxAccessGw)" + +foreach ($name in @('OtOpcUa', 'OtOpcUaWonderwareHistorian', 'MxAccessGw')) { + if (Test-NssmService $name) { + Run { nssm stop $name } "stop $name" + } + else { + Write-Host " ($name not installed; skipping)" -ForegroundColor DarkGray + } +} + +if (-not $WhatIf) { + Start-Sleep -Seconds 3 + Get-Process MxGateway.Server, MxGateway.Worker, OtOpcUa.Server, 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 + } +} + +# ------------------------------------------------------------------------ +# Step 2: Backup existing C:\publish trees +# ------------------------------------------------------------------------ + +if (-not $SkipBackup -and (Test-Path $PublishRoot)) { + $backupRoot = Join-Path $PublishRoot ".backup-$((Get-Date).ToString('yyyy-MM-dd-HHmmss'))" + Step "Backing up $PublishRoot → $backupRoot" + + Run { + New-Item -ItemType Directory -Path $backupRoot | Out-Null + foreach ($subdir in @('mxaccessgw', 'lmxopcua')) { + $src = Join-Path $PublishRoot $subdir + if (Test-Path $src) { + Copy-Item -Recurse -Path $src -Destination (Join-Path $backupRoot $subdir) + } + } + } "snapshot publish dirs (rollback target)" +} +else { + Write-Host " (backup skipped)" -ForegroundColor DarkGray +} + +# ------------------------------------------------------------------------ +# Step 3: Refresh mxaccessgw binaries (Track A output) +# ------------------------------------------------------------------------ + +Step "Building + copying mxaccessgw binaries from $GatewayRoot" + +Run { + & dotnet build "$GatewayRoot\src\MxGateway.Worker" -c Release | Out-Null + & dotnet build "$GatewayRoot\src\MxGateway.Server" -c Release | Out-Null +} "dotnet build (Worker x86 net48 + Server net10.0)" + +Run { + $serverDest = Join-Path $PublishRoot "mxaccessgw\Server" + $workerDest = Join-Path $PublishRoot "mxaccessgw\Worker" + if (-not (Test-Path $serverDest)) { New-Item -ItemType Directory -Path $serverDest -Force | Out-Null } + if (-not (Test-Path $workerDest)) { New-Item -ItemType Directory -Path $workerDest -Force | Out-Null } + Copy-Item -Recurse -Force "$GatewayRoot\src\MxGateway.Server\bin\Release\net10.0\*" $serverDest + Copy-Item -Recurse -Force "$GatewayRoot\src\MxGateway.Worker\bin\x86\Release\net48\*" $workerDest +} "copy gateway server + worker outputs" + +# ------------------------------------------------------------------------ +# Step 4: Refresh OtOpcUa + Wonderware historian sidecar +# ------------------------------------------------------------------------ + +Step "Publishing OtOpcUa server + Wonderware historian sidecar from $RepoRoot" + +Run { + & dotnet publish "$RepoRoot\src\ZB.MOM.WW.OtOpcUa.Server" ` + -c Release -o (Join-Path $PublishRoot "lmxopcua") | Out-Null + & dotnet publish "$RepoRoot\src\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware" ` + -c Release -o (Join-Path $PublishRoot "lmxopcua\WonderwareHistorian") | Out-Null +} "dotnet publish (Server + sidecar)" + +# ------------------------------------------------------------------------ +# Step 5: Service env block — ensure OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED +# is set on the Wonderware historian service (PR C.2 toggle). +# ------------------------------------------------------------------------ + +if (Test-NssmService 'OtOpcUaWonderwareHistorian') { + Step "Ensuring OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED is set on the historian service" + + Run { + $existing = nssm get OtOpcUaWonderwareHistorian AppEnvironmentExtra + if ($existing -notmatch 'OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED') { + $combined = $existing + "`r`nOTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED=true" + nssm set OtOpcUaWonderwareHistorian AppEnvironmentExtra $combined | Out-Null + Write-Host " appended OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED=true" -ForegroundColor DarkGreen + } + else { + Write-Host " already present; leaving service env block untouched" + } + } "patch service env block" +} + +# ------------------------------------------------------------------------ +# Step 6: Start in forward dependency order +# ------------------------------------------------------------------------ + +Step "Starting services (MxAccessGw → OtOpcUaWonderwareHistorian → OtOpcUa)" + +foreach ($pair in @( + @{ Name = 'MxAccessGw'; Wait = 4 }, + @{ Name = 'OtOpcUaWonderwareHistorian'; Wait = 4 }, + @{ Name = 'OtOpcUa'; Wait = 8 } +)) { + $name = $pair.Name + if (Test-NssmService $name) { + Run { nssm start $name } "start $name" + if (-not $WhatIf) { Start-Sleep -Seconds $pair.Wait } + } + else { + Write-Host " ($name not installed; skipping)" -ForegroundColor DarkGray + } +} + +# ------------------------------------------------------------------------ +# Step 7: Smoke verification +# ------------------------------------------------------------------------ + +Step "Smoke verification" + +if (-not $WhatIf) { + foreach ($name in @('MxAccessGw', 'OtOpcUaWonderwareHistorian', 'OtOpcUa')) { + if (Test-NssmService $name) { + $status = (Get-Service $name).Status + $color = if ($status -eq 'Running') { 'Green' } else { 'Red' } + Write-Host " $name = $status" -ForegroundColor $color + } + } + + foreach ($port in @(5120, 4840, 4841)) { + $listening = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue + $color = if ($listening) { 'Green' } else { 'DarkYellow' } + Write-Host " TCP $port listening = $($null -ne $listening)" -ForegroundColor $color + } + + Write-Host "" + Write-Host " Recent log tails:" -ForegroundColor DarkCyan + $tails = @( + "$PublishRoot\lmxopcua\logs\otopcua-*.log", + "$PublishRoot\mxaccessgw\stdout.log", + "$env:ProgramData\OtOpcUa\historian-wonderware-*.log" + ) + foreach ($pattern in $tails) { + $latest = Get-ChildItem -Path $pattern -ErrorAction SilentlyContinue | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 + if ($null -ne $latest) { + Write-Host "" + Write-Host " --- $($latest.FullName) (last 10 lines) ---" -ForegroundColor DarkGray + Get-Content $latest.FullName -Tail 10 | ForEach-Object { Write-Host " $_" } + } + } +} + +Write-Host "" +Write-Host "Refresh complete." -ForegroundColor Green +Write-Host "" +Write-Host "Next: run the functional verification scenarios from" +Write-Host " docs\plans\alarms-over-gateway.md §Track D §6 'Functional verification'" +Write-Host " - Galaxy-native alarm raise" +Write-Host " - Scripted alarm → AVEVA Historian round-trip" +Write-Host " - Sub-attribute fallback path with IAlarmSource disabled"