Phase 3 PR 36 — AVEVA prerequisites test-support library. New tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport multi-targeted class library (net10.0 + net48 so both the modern and the MXAccess-COM x86 test projects can consume it) that probes every piece of the AVEVA System Platform + OtOpcUa stack a live-Galaxy test depends on and returns a structured PrerequisiteReport. Closes the gap where live-smoke tests silently returned 'unreachable' without telling operators which specific piece failed.
AvevaPrerequisites.CheckAllAsync walks eight probe categories producing PrerequisiteCheck rows each with Name (e.g. 'service:aaBootstrap', 'sql:ZB', 'com:LMXProxy', 'registry:ArchestrA.Framework'), Category (AvevaCoreService / AvevaSoftService / AvevaInstall / MxAccessCom / GalaxyRepository / AvevaHistorian / OtOpcUaService / Environment), Status (Pass / Warn / Fail / Skip), and operator-facing Detail message. Report aggregates them: IsLivetestReady (no Fails anywhere) and IsAvevaSideReady (AVEVA-side categories pass, our v2 services can be absent while still considering the environment AVEVA-ready) so different test tiers can use the right threshold.
Individual probes: ServiceProbe.Check queries the Windows Service Control Manager via System.ServiceProcess.ServiceController — treats DemandStart+Stopped as Warn (NmxSvc is DemandStart by design; master pulls it up) but AutoStart+Stopped as Fail; not-installed is Fail for hard-required services, Warn for soft ones; non-Windows hosts get Skip; transitional states like StartPending get Warn with a 'try again' hint. RegistryProbe reads HKLM\SOFTWARE\WOW6432Node\ArchestrA\{Framework,Framework\Platform,MSIInstall} — Framework key presence + populated InstallPath/RootPath values mean System Platform installed; PfeConfigOptions in the Platform subkey (format 'PlatformId=N,EngineId=N,...') indicates a Platform has been deployed from the IDE (PlatformId=0 means never deployed — MXAccess will connect but every subscription will be Bad quality); RebootRequired='True' under MSIInstall surfaces as a loud warn since post-patch behavior is undefined. MxAccessComProbe resolves the LMXProxy.LMXProxyServer ProgID → CLSID → HKLM\SOFTWARE\Classes\WOW6432Node\CLSID\{guid}\InprocServer32, verifying the registered file exists on disk (catches the orphan-registry case where a previous uninstall left the ProgID registered but the DLL is gone — distinguishes it from the 'totally not installed' case by message); also emits a Warn when the test process is 64-bit (MXAccess COM activation fails with REGDB_E_CLASSNOTREG 0x80040154 regardless of registration, so seeing this warning tells operators why the activation would fail even on a fully-installed machine). SqlProbe tests Galaxy Repository via Microsoft.Data.SqlClient using the Windows-auth localhost connection string the repo code defaults to — distinguishes 'SQL Server unreachable' (connection fails) from 'ZB database does not exist' (SELECT DB_ID('ZB') returns null) because they have different remediation paths (sc.exe start MSSQLSERVER vs. restore from .cab backup); a secondary CheckDeployedObjectCountAsync query on 'gobject WHERE deployed_version > 0' warns when the count is zero because discovery smoke tests will return empty hierarchies. NamedPipeProbe opens a 2s NamedPipeClientStream against OtOpcUaGalaxyHost's pipe ('OtOpcUaGalaxy' per the installer default) — pipe accepting a connection proves the Host service is listening; disconnects immediately so we don't consume a session slot.
Service lists kept as internal static data so tests can inspect + override: CoreServices (aaBootstrap + aaGR + NmxSvc + MSSQLSERVER — hard fail if missing), SoftServices (aaLogger + aaUserValidator + aaGlobalDataCacheMonitorSvr — warn only; stack runs without them but diagnostics/auth are degraded), HistorianServices (aahClientAccessPoint + aahGateway — opt-in via Options.CheckHistorian, only matters for HistoryRead IPC paths), OtOpcUaServices (our OtOpcUaGalaxyHost hard-required for end-to-end live tests + OtOpcUa warn + GLAuth warn). Narrower entry points CheckRepositoryOnlyAsync and CheckGalaxyHostPipeOnlyAsync for tests that only care about specific subsystems — avoid paying the full probe cost on every GalaxyRepositoryLiveSmokeTests fact.
Multi-targeting mechanics: System.ServiceProcess.ServiceController + Microsoft.Win32.Registry are NuGet packages on net10 but in-box BCL references on net48; csproj conditions Package vs Reference by TargetFramework. Microsoft.Data.SqlClient v6 supports both frameworks so single PackageReference. Net48Polyfills.cs provides IsExternalInit shim (records/init-only setters) and SupportedOSPlatformAttribute stub so the same Probe sources compile on both frameworks without per-callsite preprocessor guards — lets Roslyn's platform-compatibility analyzer stay useful on net10 without breaking net48 builds.
Existing GalaxyRepositoryLiveSmokeTests updated to delegate its skip decision to AvevaPrerequisites.CheckRepositoryOnlyAsync (legacy ZbReachableAsync kept as a compatibility adapter so the in-test 'if (!await ZbReachableAsync()) return;' pattern keeps working while the surrounding fixtures gradually migrate to Assert.Skip-with-reason). Slnx file registers the new project.
Tests — AvevaPrerequisitesLiveTests (8 new Integration cases, Category=LiveGalaxy): the helper correctly reports Framework install (registry pass), aaBootstrap Running (service pass), aaGR Running (service pass), MxAccess COM registered (com pass), ZB database reachable (sql pass), deployed-object count > 0 (warn-upgraded-to-pass because this box has 49 objects deployed), the AVEVA side is ready even when our own services (OtOpcUaGalaxyHost) aren't installed yet (IsAvevaSideReady=true), and the helper emits rows for OtOpcUaGalaxyHost + OtOpcUa + GLAuth even when not installed (regression guard — nobody can accidentally ship a check that omits our own services). Full Galaxy.Host.Tests Category=LiveGalaxy suite: 13 pass (5 prior smoke + 8 new prerequisites). Full solution build clean, 0 errors.
What's NOT in this PR: end-to-end Galaxy stack smoke (Proxy → Host pipe → MXAccess → real Galaxy tag). That's the next PR — this one is the gate the end-to-end smoke will call first to produce actionable skip messages instead of silent returns.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport.Probes;
|
||||
|
||||
/// <summary>
|
||||
/// Confirms MXAccess COM server registration by resolving the
|
||||
/// <c>LMXProxy.LMXProxyServer</c> ProgID to its CLSID, then checking that the CLSID's
|
||||
/// 32-bit <c>InprocServer32</c> entry points at a file that exists on disk.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A common failure mode on partial installs: ProgID is registered but the CLSID
|
||||
/// InprocServer32 DLL is missing (previous install uninstalled but registry orphan remains).
|
||||
/// This probe surfaces that case with an actionable message instead of the
|
||||
/// <c>0x80040154 REGDB_E_CLASSNOTREG</c> you'd see from a late COM activation failure.
|
||||
/// </remarks>
|
||||
public static class MxAccessComProbe
|
||||
{
|
||||
public const string ProgId = "LMXProxy.LMXProxyServer";
|
||||
public const string VersionedProgId = "LMXProxy.LMXProxyServer.1";
|
||||
|
||||
public static PrerequisiteCheck Check()
|
||||
{
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return new PrerequisiteCheck("com:LMXProxy", PrerequisiteCategory.MxAccessCom,
|
||||
PrerequisiteStatus.Skip, "COM registration probes only run on Windows.");
|
||||
}
|
||||
return CheckWindows();
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static PrerequisiteCheck CheckWindows()
|
||||
{
|
||||
try
|
||||
{
|
||||
var (clsid, dll) = RegistryProbe.ResolveProgIdToInproc(ProgId);
|
||||
if (clsid is null)
|
||||
{
|
||||
return new PrerequisiteCheck("com:LMXProxy", PrerequisiteCategory.MxAccessCom,
|
||||
PrerequisiteStatus.Fail,
|
||||
$"ProgID {ProgId} not registered — MXAccess COM server isn't installed. " +
|
||||
$"Install System Platform's MXAccess component and re-run.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(dll))
|
||||
{
|
||||
return new PrerequisiteCheck("com:LMXProxy", PrerequisiteCategory.MxAccessCom,
|
||||
PrerequisiteStatus.Fail,
|
||||
$"ProgID {ProgId} → CLSID {clsid} but InprocServer32 is empty. " +
|
||||
$"Registry is orphaned; re-register with: regsvr32 /s LmxProxy.dll (from an elevated cmd in the Framework bin dir).");
|
||||
}
|
||||
|
||||
// Resolve the recorded path — sometimes registered as a bare filename that the COM
|
||||
// runtime resolves via the current process's DLL-search path. Accept either an
|
||||
// absolute path that exists, or a bare filename whose resolution we can't verify
|
||||
// without loading it (treat as Pass-with-note).
|
||||
if (Path.IsPathRooted(dll))
|
||||
{
|
||||
if (!File.Exists(dll))
|
||||
{
|
||||
return new PrerequisiteCheck("com:LMXProxy", PrerequisiteCategory.MxAccessCom,
|
||||
PrerequisiteStatus.Fail,
|
||||
$"ProgID {ProgId} → CLSID {clsid} → InprocServer32 {dll}, but the file is missing. " +
|
||||
$"Re-install the Framework or restore from backup.");
|
||||
}
|
||||
return new PrerequisiteCheck("com:LMXProxy", PrerequisiteCategory.MxAccessCom,
|
||||
PrerequisiteStatus.Pass,
|
||||
$"ProgID {ProgId} → {dll} (file exists).");
|
||||
}
|
||||
|
||||
return new PrerequisiteCheck("com:LMXProxy", PrerequisiteCategory.MxAccessCom,
|
||||
PrerequisiteStatus.Pass,
|
||||
$"ProgID {ProgId} → {dll} (bare filename — relies on PATH resolution at COM activation time).");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new PrerequisiteCheck("com:LMXProxy", PrerequisiteCategory.MxAccessCom,
|
||||
PrerequisiteStatus.Warn,
|
||||
$"Probe failed: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Warn when running as a 64-bit process — MXAccess COM activation will fail with
|
||||
/// <c>0x80040154</c> regardless of registration state. The production drivers run net48
|
||||
/// x86; xunit hosts run 64-bit by default so this often surfaces first.
|
||||
/// </summary>
|
||||
public static PrerequisiteCheck CheckProcessBitness()
|
||||
{
|
||||
if (Environment.Is64BitProcess)
|
||||
{
|
||||
return new PrerequisiteCheck("env:ProcessBitness", PrerequisiteCategory.Environment,
|
||||
PrerequisiteStatus.Warn,
|
||||
"Test host is 64-bit. Direct MXAccess COM activation would fail with REGDB_E_CLASSNOTREG (0x80040154); " +
|
||||
"the production driver workaround is to run Galaxy.Host as a 32-bit process. Tests that only " +
|
||||
"talk to the Host service over the named pipe aren't affected.");
|
||||
}
|
||||
return new PrerequisiteCheck("env:ProcessBitness", PrerequisiteCategory.Environment,
|
||||
PrerequisiteStatus.Pass, "Test host is 32-bit.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System.IO.Pipes;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport.Probes;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the <c>OtOpcUaGalaxyHost</c> named-pipe endpoint is accepting connections —
|
||||
/// the handshake the Proxy performs at boot. A clean pipe connect without sending any
|
||||
/// framed message proves the Host service is listening; we disconnect immediately so we
|
||||
/// don't consume a session slot.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Default pipe name matches the installer script's <c>OTOPCUA_GALAXY_PIPE</c> default.
|
||||
/// Override when the Host service was installed with a non-default name (custom deployments).
|
||||
/// </remarks>
|
||||
public static class NamedPipeProbe
|
||||
{
|
||||
public const string DefaultGalaxyHostPipeName = "OtOpcUaGalaxy";
|
||||
|
||||
public static async Task<PrerequisiteCheck> CheckGalaxyHostPipeAsync(
|
||||
string? pipeName = null, CancellationToken ct = default)
|
||||
{
|
||||
pipeName ??= DefaultGalaxyHostPipeName;
|
||||
try
|
||||
{
|
||||
using var client = new NamedPipeClientStream(
|
||||
serverName: ".",
|
||||
pipeName: pipeName,
|
||||
direction: PipeDirection.InOut,
|
||||
options: PipeOptions.Asynchronous);
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(2));
|
||||
await client.ConnectAsync(cts.Token);
|
||||
|
||||
return new PrerequisiteCheck("pipe:OtOpcUaGalaxyHost", PrerequisiteCategory.OtOpcUaService,
|
||||
PrerequisiteStatus.Pass,
|
||||
$@"Pipe \\.\pipe\{pipeName} accepted a connection — OtOpcUaGalaxyHost is listening.");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return new PrerequisiteCheck("pipe:OtOpcUaGalaxyHost", PrerequisiteCategory.OtOpcUaService,
|
||||
PrerequisiteStatus.Fail,
|
||||
$@"Pipe \\.\pipe\{pipeName} not connectable within 2s — OtOpcUaGalaxyHost service isn't running. " +
|
||||
"Start with: sc.exe start OtOpcUaGalaxyHost");
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
return new PrerequisiteCheck("pipe:OtOpcUaGalaxyHost", PrerequisiteCategory.OtOpcUaService,
|
||||
PrerequisiteStatus.Fail,
|
||||
$@"Pipe \\.\pipe\{pipeName} connect timed out — service may be starting or stuck. " +
|
||||
"Check: sc.exe query OtOpcUaGalaxyHost");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new PrerequisiteCheck("pipe:OtOpcUaGalaxyHost", PrerequisiteCategory.OtOpcUaService,
|
||||
PrerequisiteStatus.Fail,
|
||||
$@"Pipe \\.\pipe\{pipeName} connect failed: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport.Probes;
|
||||
|
||||
/// <summary>
|
||||
/// Reads HKLM registry keys to confirm ArchestrA Framework / System Platform install
|
||||
/// markers. Matches the registered paths documented in
|
||||
/// <c>docs/v2/implementation/</c> — System Platform is 32-bit so keys live under
|
||||
/// <c>HKLM\SOFTWARE\WOW6432Node\ArchestrA\...</c>.
|
||||
/// </summary>
|
||||
public static class RegistryProbe
|
||||
{
|
||||
// Canonical install roots per the research on our dev box (System Platform 2020 R2).
|
||||
public const string ArchestrARootKey = @"SOFTWARE\WOW6432Node\ArchestrA";
|
||||
public const string FrameworkKey = @"SOFTWARE\WOW6432Node\ArchestrA\Framework";
|
||||
public const string PlatformKey = @"SOFTWARE\WOW6432Node\ArchestrA\Framework\Platform";
|
||||
public const string MsiInstallKey = @"SOFTWARE\WOW6432Node\ArchestrA\MSIInstall";
|
||||
|
||||
public static PrerequisiteCheck CheckFrameworkInstalled()
|
||||
{
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return new PrerequisiteCheck("registry:ArchestrA.Framework", PrerequisiteCategory.AvevaInstall,
|
||||
PrerequisiteStatus.Skip, "Registry probes only run on Windows.");
|
||||
}
|
||||
return FrameworkInstalledWindows();
|
||||
}
|
||||
|
||||
public static PrerequisiteCheck CheckPlatformDeployed()
|
||||
{
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return new PrerequisiteCheck("registry:ArchestrA.Platform", PrerequisiteCategory.AvevaInstall,
|
||||
PrerequisiteStatus.Skip, "Registry probes only run on Windows.");
|
||||
}
|
||||
return PlatformDeployedWindows();
|
||||
}
|
||||
|
||||
public static PrerequisiteCheck CheckRebootPending()
|
||||
{
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return new PrerequisiteCheck("registry:ArchestrA.RebootPending", PrerequisiteCategory.AvevaInstall,
|
||||
PrerequisiteStatus.Skip, "Registry probes only run on Windows.");
|
||||
}
|
||||
return RebootPendingWindows();
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static PrerequisiteCheck FrameworkInstalledWindows()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = Registry.LocalMachine.OpenSubKey(FrameworkKey);
|
||||
if (key is null)
|
||||
{
|
||||
return new PrerequisiteCheck("registry:ArchestrA.Framework", PrerequisiteCategory.AvevaInstall,
|
||||
PrerequisiteStatus.Fail,
|
||||
$"Missing {FrameworkKey} — ArchestrA Framework isn't installed. Install AVEVA System Platform from the setup media.");
|
||||
}
|
||||
|
||||
var installPath = key.GetValue("InstallPath") as string;
|
||||
var rootPath = key.GetValue("RootPath") as string;
|
||||
if (string.IsNullOrWhiteSpace(installPath) || string.IsNullOrWhiteSpace(rootPath))
|
||||
{
|
||||
return new PrerequisiteCheck("registry:ArchestrA.Framework", PrerequisiteCategory.AvevaInstall,
|
||||
PrerequisiteStatus.Warn,
|
||||
$"Framework key exists but InstallPath/RootPath values missing — install may be incomplete.");
|
||||
}
|
||||
|
||||
return new PrerequisiteCheck("registry:ArchestrA.Framework", PrerequisiteCategory.AvevaInstall,
|
||||
PrerequisiteStatus.Pass,
|
||||
$"Installed at {installPath} (RootPath {rootPath}).");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new PrerequisiteCheck("registry:ArchestrA.Framework", PrerequisiteCategory.AvevaInstall,
|
||||
PrerequisiteStatus.Warn,
|
||||
$"Probe failed: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static PrerequisiteCheck PlatformDeployedWindows()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = Registry.LocalMachine.OpenSubKey(PlatformKey);
|
||||
var pfeConfig = key?.GetValue("PfeConfigOptions") as string;
|
||||
if (string.IsNullOrWhiteSpace(pfeConfig))
|
||||
{
|
||||
return new PrerequisiteCheck("registry:ArchestrA.Platform.Deployed", PrerequisiteCategory.AvevaInstall,
|
||||
PrerequisiteStatus.Warn,
|
||||
$"No Platform object deployed locally (Platform\\PfeConfigOptions empty). MXAccess will connect but subscriptions will fail. Deploy a Platform from the IDE.");
|
||||
}
|
||||
|
||||
// PfeConfigOptions format: "PlatformId=N,EngineId=N,EngineName=...,..."
|
||||
// A non-deployed state leaves PlatformId=0 or the key empty.
|
||||
if (pfeConfig.Contains("PlatformId=0,", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new PrerequisiteCheck("registry:ArchestrA.Platform.Deployed", PrerequisiteCategory.AvevaInstall,
|
||||
PrerequisiteStatus.Warn,
|
||||
$"Platform never deployed (PfeConfigOptions has PlatformId=0). Deploy a Platform from the IDE before running live tests.");
|
||||
}
|
||||
|
||||
return new PrerequisiteCheck("registry:ArchestrA.Platform.Deployed", PrerequisiteCategory.AvevaInstall,
|
||||
PrerequisiteStatus.Pass,
|
||||
$"Platform deployed ({pfeConfig}).");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new PrerequisiteCheck("registry:ArchestrA.Platform.Deployed", PrerequisiteCategory.AvevaInstall,
|
||||
PrerequisiteStatus.Warn,
|
||||
$"Probe failed: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static PrerequisiteCheck RebootPendingWindows()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = Registry.LocalMachine.OpenSubKey(MsiInstallKey);
|
||||
var rebootRequired = key?.GetValue("RebootRequired") as string;
|
||||
if (string.Equals(rebootRequired, "True", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new PrerequisiteCheck("registry:ArchestrA.RebootPending", PrerequisiteCategory.AvevaInstall,
|
||||
PrerequisiteStatus.Warn,
|
||||
"An ArchestrA patch has been installed but the machine hasn't rebooted. Post-patch behavior is undefined until a reboot.");
|
||||
}
|
||||
return new PrerequisiteCheck("registry:ArchestrA.RebootPending", PrerequisiteCategory.AvevaInstall,
|
||||
PrerequisiteStatus.Pass,
|
||||
"No pending reboot flagged.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new PrerequisiteCheck("registry:ArchestrA.RebootPending", PrerequisiteCategory.AvevaInstall,
|
||||
PrerequisiteStatus.Warn,
|
||||
$"Probe failed: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the registered <see cref="ComProgIdCheck"/> CLSID for the given ProgID and
|
||||
/// resolve the 32-bit <c>InprocServer32</c> file path. Returns null when either is missing.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
internal static (string? Clsid, string? InprocDllPath) ResolveProgIdToInproc(string progId)
|
||||
{
|
||||
using var progIdKey = Registry.ClassesRoot.OpenSubKey($@"{progId}\CLSID");
|
||||
var clsid = progIdKey?.GetValue(null) as string;
|
||||
if (string.IsNullOrWhiteSpace(clsid)) return (null, null);
|
||||
|
||||
// 32-bit COM server under Wow6432Node\CLSID\{guid}\InprocServer32 default value.
|
||||
using var inproc = Registry.LocalMachine.OpenSubKey(
|
||||
$@"SOFTWARE\Classes\WOW6432Node\CLSID\{clsid}\InprocServer32");
|
||||
var dll = inproc?.GetValue(null) as string;
|
||||
return (clsid, dll);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.ServiceProcess;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport.Probes;
|
||||
|
||||
/// <summary>
|
||||
/// Queries the Windows Service Control Manager to report whether a named service is
|
||||
/// installed, its current state, and its start type. Non-Windows hosts return Skip.
|
||||
/// </summary>
|
||||
public static class ServiceProbe
|
||||
{
|
||||
public static PrerequisiteCheck Check(
|
||||
string serviceName,
|
||||
PrerequisiteCategory category,
|
||||
bool hardRequired,
|
||||
string whatItDoes)
|
||||
{
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return new PrerequisiteCheck(
|
||||
Name: $"service:{serviceName}",
|
||||
Category: category,
|
||||
Status: PrerequisiteStatus.Skip,
|
||||
Detail: "Service probes only run on Windows.");
|
||||
}
|
||||
|
||||
return CheckWindows(serviceName, category, hardRequired, whatItDoes);
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static PrerequisiteCheck CheckWindows(
|
||||
string serviceName, PrerequisiteCategory category, bool hardRequired, string whatItDoes)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var sc = new ServiceController(serviceName);
|
||||
// Touch the Status to force the SCM lookup; if the service doesn't exist, this throws
|
||||
// InvalidOperationException with message "Service ... was not found on computer.".
|
||||
var status = sc.Status;
|
||||
var startType = sc.StartType;
|
||||
|
||||
return status switch
|
||||
{
|
||||
ServiceControllerStatus.Running => new PrerequisiteCheck(
|
||||
$"service:{serviceName}", category, PrerequisiteStatus.Pass,
|
||||
$"Running ({whatItDoes})"),
|
||||
|
||||
// DemandStart services (like NmxSvc) that are Stopped are not necessarily a
|
||||
// failure — the master service (aaBootstrap) brings them up on demand. Treat
|
||||
// Stopped+Demand as Warn so operators know the situation but tests still proceed.
|
||||
ServiceControllerStatus.Stopped when startType == ServiceStartMode.Manual =>
|
||||
new PrerequisiteCheck(
|
||||
$"service:{serviceName}", category, PrerequisiteStatus.Warn,
|
||||
$"Installed but Stopped (start type Manual — {whatItDoes}). " +
|
||||
"Will be pulled up on demand by the master service; fine for tests."),
|
||||
|
||||
ServiceControllerStatus.Stopped => Fail(
|
||||
$"Installed but Stopped. Start with: sc.exe start {serviceName} ({whatItDoes})"),
|
||||
|
||||
_ => new PrerequisiteCheck(
|
||||
$"service:{serviceName}", category, PrerequisiteStatus.Warn,
|
||||
$"Transitional state {status} ({whatItDoes}) — try again in a few seconds."),
|
||||
};
|
||||
|
||||
PrerequisiteCheck Fail(string detail) => new(
|
||||
$"service:{serviceName}", category,
|
||||
hardRequired ? PrerequisiteStatus.Fail : PrerequisiteStatus.Warn,
|
||||
detail);
|
||||
}
|
||||
catch (InvalidOperationException ex) when (ex.Message.Contains("was not found", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new PrerequisiteCheck(
|
||||
$"service:{serviceName}", category,
|
||||
hardRequired ? PrerequisiteStatus.Fail : PrerequisiteStatus.Warn,
|
||||
$"Not installed ({whatItDoes}). Install the relevant System Platform component and retry.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new PrerequisiteCheck(
|
||||
$"service:{serviceName}", category, PrerequisiteStatus.Warn,
|
||||
$"Probe failed ({ex.GetType().Name}: {ex.Message}) — treat as unknown.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport.Probes;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the Galaxy Repository SQL side: SQL Server reachable, <c>ZB</c> database
|
||||
/// present, and at least one deployed object exists (so live tests have something to read).
|
||||
/// Reuses the Windows-auth connection string the repo code defaults to.
|
||||
/// </summary>
|
||||
public static class SqlProbe
|
||||
{
|
||||
public const string DefaultConnectionString =
|
||||
"Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;Connect Timeout=3;";
|
||||
|
||||
public static async Task<PrerequisiteCheck> CheckZbDatabaseAsync(
|
||||
string? connectionString = null, CancellationToken ct = default)
|
||||
{
|
||||
connectionString ??= DefaultConnectionString;
|
||||
try
|
||||
{
|
||||
using var conn = new SqlConnection(connectionString);
|
||||
await conn.OpenAsync(ct);
|
||||
|
||||
// DB_ID returns null when the database doesn't exist on the connected server — distinct
|
||||
// failure mode from "server unreachable", deserves a distinct message.
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = "SELECT DB_ID('ZB')";
|
||||
var dbIdObj = await cmd.ExecuteScalarAsync(ct);
|
||||
if (dbIdObj is null || dbIdObj is DBNull)
|
||||
{
|
||||
return new PrerequisiteCheck("sql:ZB", PrerequisiteCategory.GalaxyRepository,
|
||||
PrerequisiteStatus.Fail,
|
||||
"SQL Server reachable but database ZB does not exist. " +
|
||||
"Create the Galaxy from the IDE or restore a .cab backup.");
|
||||
}
|
||||
|
||||
return new PrerequisiteCheck("sql:ZB", PrerequisiteCategory.GalaxyRepository,
|
||||
PrerequisiteStatus.Pass, "Connected; ZB database exists.");
|
||||
}
|
||||
catch (SqlException ex)
|
||||
{
|
||||
return new PrerequisiteCheck("sql:ZB", PrerequisiteCategory.GalaxyRepository,
|
||||
PrerequisiteStatus.Fail,
|
||||
$"SQL Server unreachable: {ex.Message}. Ensure MSSQLSERVER service is running (sc.exe start MSSQLSERVER) and TCP 1433 is open.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new PrerequisiteCheck("sql:ZB", PrerequisiteCategory.GalaxyRepository,
|
||||
PrerequisiteStatus.Fail,
|
||||
$"Unexpected probe error: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the count of deployed Galaxy objects (<c>deployed_version > 0</c>). Zero
|
||||
/// isn't a hard failure — lets someone boot a fresh Galaxy and still get meaningful
|
||||
/// test-suite output — but it IS a warning because any live-read smoke will have
|
||||
/// nothing to read.
|
||||
/// </summary>
|
||||
public static async Task<PrerequisiteCheck> CheckDeployedObjectCountAsync(
|
||||
string? connectionString = null, CancellationToken ct = default)
|
||||
{
|
||||
connectionString ??= DefaultConnectionString;
|
||||
try
|
||||
{
|
||||
using var conn = new SqlConnection(connectionString);
|
||||
await conn.OpenAsync(ct);
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = "SELECT COUNT(*) FROM gobject WHERE deployed_version > 0";
|
||||
var countObj = await cmd.ExecuteScalarAsync(ct);
|
||||
var count = countObj is int i ? i : 0;
|
||||
|
||||
return count > 0
|
||||
? new PrerequisiteCheck("sql:ZB.deployedObjects", PrerequisiteCategory.GalaxyRepository,
|
||||
PrerequisiteStatus.Pass, $"{count} objects deployed — live reads have data to return.")
|
||||
: new PrerequisiteCheck("sql:ZB.deployedObjects", PrerequisiteCategory.GalaxyRepository,
|
||||
PrerequisiteStatus.Warn,
|
||||
"ZB contains no deployed objects. Discovery smoke tests will return empty hierarchies; " +
|
||||
"deploy at least a Platform + AppEngine from the IDE to exercise the read path.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new PrerequisiteCheck("sql:ZB.deployedObjects", PrerequisiteCategory.GalaxyRepository,
|
||||
PrerequisiteStatus.Warn,
|
||||
$"Couldn't count deployed objects: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user