LiveStackConfig resolves the pipe name + per-install shared secret from two sources in order: OTOPCUA_GALAXY_PIPE + OTOPCUA_GALAXY_SECRET env vars first (for CI / benchwork overrides), then the service's per-process Environment registry values under HKLM\SYSTEM\CurrentControlSet\Services\OtOpcUaGalaxyHost (what Install-Services.ps1 writes at install time). Registry read requires the test host to run elevated on most boxes — the skip message says so explicitly so operators see the right remediation. Hard-coded secrets are deliberately avoided: the installer generates 32 fresh random bytes per install, a committed secret would diverge from production the moment the service is re-installed. LiveStackFixture is an IAsyncLifetime that (1) runs AvevaPrerequisites.CheckAllAsync with CheckGalaxyHostPipe=true + CheckHistorian=false — produces a structured PrerequisiteReport whose SkipReason is the exact operator-facing 'here's what you need to fix' text, (2) resolves LiveStackConfig and surfaces a clear skip when the secret isn't discoverable, (3) instantiates GalaxyProxyDriver + calls InitializeAsync (the IPC handshake), capturing a skip with the exception detail + common-cause hints (secret mismatch, SID not in pipe ACL, Host's backend couldn't connect to ZB) rather than letting a NullRef cascade through every subsequent test. SkipIfUnavailable() translates the captured SkipReason into Assert.Skip at the top of every fact so tests read as cleanly-skipped with a visible reason, not silently-passed or crashed. LiveStackSmokeTests (5 facts, Collection=LiveStack, Category=LiveGalaxy): Fixture_initialized_successfully (cheapest possible end-to-end assertion — if this passes, the IPC handshake worked); Driver_reports_Healthy_after_IPC_handshake (DriverHealth.State post-connect); DiscoverAsync_returns_at_least_one_variable_from_live_galaxy (captures every Variable() call from DiscoverAsync via CapturingAddressSpaceBuilder and asserts > 0 — zero here usually means the Host couldn't read ZB, the skip message names OTOPCUA_GALAXY_ZB_CONN to check); GetHostStatuses_reports_at_least_one_platform (IHostConnectivityProbe surface — zero means the probe loop hasn't fired or no Platform is deployed locally); Can_read_a_discovered_variable_from_live_galaxy (reads the first discovered attribute's full reference, asserts status != BadInternalError — Galaxy's Uncertain-quality-until-first-Engine-scan is intentionally NOT treated as failure since it depends on runtime state that varies across test runs). Read-only by design; writes need an agreed scratch tag to avoid mutating a process-critical attribute — deferred to a follow-up PR that reuses this fixture. CapturingAddressSpaceBuilder is a minimal IAddressSpaceBuilder that flattens every Variable() call into a list so tests can inspect what discovery produced without booting the full OPC UA node-manager stack; alarm annotation + property calls are no-ops. Scoped private to the test class. Galaxy.Proxy.Tests csproj gains a ProjectReference to Driver.Galaxy.TestSupport (PR 36) for AvevaPrerequisites. The NU1702 warning about the Host project being net48-referenced-by-net10 is pre-existing from the HostSubprocessParityTests — Proxy.Tests only needs the Host EXE path for that parity scenario, not type surface. Test run on THIS machine (OtOpcUaGalaxyHost not yet installed): Skipped! Failed 0, Passed 0, Skipped 5 — each skip message includes the full prerequisites report pointing at the missing service. Once the service is installed + started (scripts\install\Install-Services.ps1), the 5 facts will execute against live Galaxy. Proxy.Tests Unit: 17 pass / 0 fail (unchanged — new tests are Category=LiveGalaxy, separate suite). Full Proxy build clean. Memory already captures the 'live tests run via already-running service, don't spawn' convention (project_galaxy_host_service.md). lmx-followups.md #5 updated: status is 'IN PROGRESS' across PRs 36 + 37 with the explicit remaining work (install + start services, subscribe-and-receive, write round-trip). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
76 lines
3.3 KiB
C#
76 lines
3.3 KiB
C#
using System.Runtime.InteropServices;
|
|
using System.Runtime.Versioning;
|
|
using Microsoft.Win32;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests.LiveStack;
|
|
|
|
/// <summary>
|
|
/// Resolves the pipe name + shared secret the live <see cref="GalaxyProxyDriver"/> needs
|
|
/// to connect to a running <c>OtOpcUaGalaxyHost</c> Windows service. Two sources are
|
|
/// consulted, first match wins:
|
|
/// <list type="number">
|
|
/// <item>Explicit env vars (<c>OTOPCUA_GALAXY_PIPE</c>, <c>OTOPCUA_GALAXY_SECRET</c>) — lets CI / benchwork override.</item>
|
|
/// <item>The service's per-process <c>Environment</c> registry values under
|
|
/// <c>HKLM\SYSTEM\CurrentControlSet\Services\OtOpcUaGalaxyHost</c> — what
|
|
/// <c>Install-Services.ps1</c> writes at install time. Requires the test to run as a
|
|
/// principal with read access to that registry key (typically Administrators).</item>
|
|
/// </list>
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Explicitly NOT baked-in-to-source: the shared secret is rotated per install (the
|
|
/// installer generates 32 random bytes and stores the base64 string). A hard-coded secret
|
|
/// in tests would diverge from production the moment someone re-installed the service.
|
|
/// </remarks>
|
|
public sealed record LiveStackConfig(string PipeName, string SharedSecret, string? Source)
|
|
{
|
|
public const string EnvPipeName = "OTOPCUA_GALAXY_PIPE";
|
|
public const string EnvSharedSecret = "OTOPCUA_GALAXY_SECRET";
|
|
public const string ServiceRegistryKey =
|
|
@"SYSTEM\CurrentControlSet\Services\OtOpcUaGalaxyHost";
|
|
public const string DefaultPipeName = "OtOpcUaGalaxy";
|
|
|
|
public static LiveStackConfig? Resolve()
|
|
{
|
|
var envPipe = Environment.GetEnvironmentVariable(EnvPipeName);
|
|
var envSecret = Environment.GetEnvironmentVariable(EnvSharedSecret);
|
|
if (!string.IsNullOrWhiteSpace(envPipe) && !string.IsNullOrWhiteSpace(envSecret))
|
|
return new LiveStackConfig(envPipe, envSecret, "env vars");
|
|
|
|
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
return null;
|
|
|
|
return FromServiceRegistry();
|
|
}
|
|
|
|
[SupportedOSPlatform("windows")]
|
|
private static LiveStackConfig? FromServiceRegistry()
|
|
{
|
|
try
|
|
{
|
|
using var key = Registry.LocalMachine.OpenSubKey(ServiceRegistryKey);
|
|
if (key is null) return null;
|
|
var env = key.GetValue("Environment") as string[];
|
|
if (env is null || env.Length == 0) return null;
|
|
|
|
string? pipe = null, secret = null;
|
|
foreach (var line in env)
|
|
{
|
|
var eq = line.IndexOf('=');
|
|
if (eq <= 0) continue;
|
|
var name = line[..eq];
|
|
var value = line[(eq + 1)..];
|
|
if (name.Equals(EnvPipeName, StringComparison.OrdinalIgnoreCase)) pipe = value;
|
|
else if (name.Equals(EnvSharedSecret, StringComparison.OrdinalIgnoreCase)) secret = value;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(secret)) return null;
|
|
return new LiveStackConfig(pipe ?? DefaultPipeName, secret, "service registry");
|
|
}
|
|
catch
|
|
{
|
|
// Access denied / key missing / malformed — caller gets null and surfaces a Skip.
|
|
return null;
|
|
}
|
|
}
|
|
}
|