chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
Group all 69 projects into category subfolders under src/ and tests/ so the Rider Solution Explorer mirrors the module structure. Folders: Core, Server, Drivers (with a nested Driver CLIs subfolder), Client, Tooling. - Move every project folder on disk with git mv (history preserved as renames). - Recompute relative paths in 57 .csproj files: cross-category ProjectReferences, the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external mxaccessgw refs in Driver.Galaxy and its test project. - Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders. - Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL, integration, install). Build green (0 errors); unit tests pass. Docs left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
using System;
|
||||
using System.Security.Principal;
|
||||
using System.Threading;
|
||||
using Serilog;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Ipc;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware;
|
||||
|
||||
/// <summary>
|
||||
/// Entry point for the Wonderware Historian sidecar. Reads pipe name, allowed-SID,
|
||||
/// shared secret, and historian connection config from environment (the supervisor
|
||||
/// passes them at spawn time per <c>driver-stability.md</c>). Hosts a named-pipe server
|
||||
/// dispatching the five sidecar contracts (PR 3.3) to the Wonderware Historian SDK.
|
||||
/// </summary>
|
||||
public static class Program
|
||||
{
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Information()
|
||||
.WriteTo.File(
|
||||
@"%ProgramData%\OtOpcUa\historian-wonderware-.log".Replace("%ProgramData%", Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData)),
|
||||
rollingInterval: RollingInterval.Day)
|
||||
.CreateLogger();
|
||||
|
||||
try
|
||||
{
|
||||
var pipeName = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_PIPE")
|
||||
?? throw new InvalidOperationException("OTOPCUA_HISTORIAN_PIPE not set — supervisor must pass the sidecar pipe name");
|
||||
var allowedSidValue = Environment.GetEnvironmentVariable("OTOPCUA_ALLOWED_SID")
|
||||
?? throw new InvalidOperationException("OTOPCUA_ALLOWED_SID not set — supervisor must pass the server principal SID");
|
||||
var sharedSecret = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_SECRET")
|
||||
?? throw new InvalidOperationException("OTOPCUA_HISTORIAN_SECRET not set — supervisor must pass the per-process secret at spawn time");
|
||||
|
||||
var allowedSid = new SecurityIdentifier(allowedSidValue);
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
|
||||
|
||||
// Sidecar can boot in "pipe-only" mode (no real Wonderware Historian SDK
|
||||
// initialization) for smoke + IPC tests. Production sets ENABLED=true so the
|
||||
// SDK opens its connection up front.
|
||||
var historianEnabled = string.Equals(
|
||||
Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_ENABLED"),
|
||||
"true", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!historianEnabled)
|
||||
{
|
||||
Log.Information("Wonderware historian sidecar starting in pipe-only mode (OTOPCUA_HISTORIAN_ENABLED!=true) — pipe={Pipe} allowedSid={Sid}", pipeName, allowedSidValue);
|
||||
cts.Token.WaitHandle.WaitOne();
|
||||
Log.Information("Wonderware historian sidecar stopping cleanly");
|
||||
return 0;
|
||||
}
|
||||
|
||||
using var historian = BuildHistorian();
|
||||
var alarmWriter = BuildAlarmWriter();
|
||||
var handler = new HistorianFrameHandler(historian, Log.Logger, alarmWriter);
|
||||
using var server = new PipeServer(pipeName, allowedSid, sharedSecret, Log.Logger);
|
||||
|
||||
Log.Information("Wonderware historian sidecar serving — pipe={Pipe} allowedSid={Sid}", pipeName, allowedSidValue);
|
||||
try { server.RunAsync(handler, cts.Token).GetAwaiter().GetResult(); }
|
||||
catch (OperationCanceledException) { /* clean shutdown via Ctrl-C */ }
|
||||
|
||||
Log.Information("Wonderware historian sidecar stopped cleanly");
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Fatal(ex, "Wonderware historian sidecar fatal");
|
||||
return 2;
|
||||
}
|
||||
finally { Log.CloseAndFlush(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the Wonderware Historian data source from environment variables. Mirrors
|
||||
/// the env-var contract that <c>Driver.Galaxy.Host</c> used in v1; PR 3.W reaffirms
|
||||
/// this contract in install scripts.
|
||||
/// </summary>
|
||||
private static HistorianDataSource BuildHistorian()
|
||||
{
|
||||
var cfg = new HistorianConfiguration
|
||||
{
|
||||
Enabled = true,
|
||||
ServerName = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_SERVER") ?? "localhost",
|
||||
Port = TryParseInt("OTOPCUA_HISTORIAN_PORT", 32568),
|
||||
IntegratedSecurity = !string.Equals(Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_INTEGRATED"), "false", StringComparison.OrdinalIgnoreCase),
|
||||
UserName = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_USER"),
|
||||
Password = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_PASS"),
|
||||
CommandTimeoutSeconds = TryParseInt("OTOPCUA_HISTORIAN_TIMEOUT_SEC", 30),
|
||||
MaxValuesPerRead = TryParseInt("OTOPCUA_HISTORIAN_MAX_VALUES", 10000),
|
||||
FailureCooldownSeconds = TryParseInt("OTOPCUA_HISTORIAN_COOLDOWN_SEC", 60),
|
||||
};
|
||||
|
||||
var servers = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_SERVERS");
|
||||
if (!string.IsNullOrWhiteSpace(servers))
|
||||
cfg.ServerNames = new System.Collections.Generic.List<string>(
|
||||
servers.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries));
|
||||
|
||||
Log.Information("Sidecar Historian config — {NodeCount} node(s), port={Port}",
|
||||
cfg.ServerNames.Count > 0 ? cfg.ServerNames.Count : 1, cfg.Port);
|
||||
return new HistorianDataSource(cfg);
|
||||
}
|
||||
|
||||
private static int TryParseInt(string envName, int defaultValue)
|
||||
{
|
||||
var raw = Environment.GetEnvironmentVariable(envName);
|
||||
return int.TryParse(raw, out var parsed) ? parsed : defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs the alarm-event writer when the alarm-write toggle is on, otherwise
|
||||
/// returns <c>null</c> so <see cref="HistorianFrameHandler"/> falls back to the
|
||||
/// "not configured" reply for any incoming <c>WriteAlarmEvents</c> frame.
|
||||
/// Default is <c>true</c> when <c>OTOPCUA_HISTORIAN_ENABLED=true</c>; explicitly
|
||||
/// set <c>OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED=false</c> to keep a read-only
|
||||
/// deployment that still loads the SDK for reads.
|
||||
/// </summary>
|
||||
internal static IAlarmEventWriter? BuildAlarmWriter()
|
||||
{
|
||||
var raw = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED");
|
||||
var enabled = string.IsNullOrWhiteSpace(raw)
|
||||
? true
|
||||
: !string.Equals(raw, "false", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!enabled)
|
||||
{
|
||||
Log.Information("Alarm-event writer disabled (OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED=false); historian sidecar will reject WriteAlarmEvents frames.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var cfg = BuildAlarmWriterConfig();
|
||||
var backend = new SdkAlarmHistorianWriteBackend(cfg);
|
||||
Log.Information("Alarm-event writer enabled — backend=SdkAlarmHistorianWriteBackend server={Server}", cfg.ServerName);
|
||||
return new AahClientManagedAlarmEventWriter(backend);
|
||||
}
|
||||
|
||||
private static HistorianConfiguration BuildAlarmWriterConfig()
|
||||
{
|
||||
return new HistorianConfiguration
|
||||
{
|
||||
Enabled = true,
|
||||
ServerName = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_SERVER") ?? "localhost",
|
||||
Port = TryParseInt("OTOPCUA_HISTORIAN_PORT", 32568),
|
||||
IntegratedSecurity = !string.Equals(Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_INTEGRATED"), "false", StringComparison.OrdinalIgnoreCase),
|
||||
UserName = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_USER"),
|
||||
Password = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_PASS"),
|
||||
CommandTimeoutSeconds = TryParseInt("OTOPCUA_HISTORIAN_TIMEOUT_SEC", 30),
|
||||
FailureCooldownSeconds = TryParseInt("OTOPCUA_HISTORIAN_COOLDOWN_SEC", 60),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user