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:
Joseph Doherty
2026-05-17 01:55:28 -04:00
parent 69f02fed7f
commit a25593a9c6
1044 changed files with 365 additions and 343 deletions
@@ -0,0 +1,250 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MessagePack;
using Serilog;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Ipc;
/// <summary>
/// Sidecar-side dispatcher. Each post-Hello frame routes by <see cref="MessageKind"/> to
/// the right historian operation and the result frame is written back through the same
/// pipe. Per-call exceptions are caught and surfaced as <c>Success=false, Error=...</c>
/// replies so a single bad request doesn't kill the connection.
/// </summary>
public sealed class HistorianFrameHandler : IFrameHandler
{
private readonly IHistorianDataSource _historian;
private readonly IAlarmEventWriter? _alarmWriter;
private readonly ILogger _logger;
public HistorianFrameHandler(
IHistorianDataSource historian,
ILogger logger,
IAlarmEventWriter? alarmWriter = null)
{
_historian = historian ?? throw new ArgumentNullException(nameof(historian));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_alarmWriter = alarmWriter;
}
public Task HandleAsync(MessageKind kind, byte[] body, FrameWriter writer, CancellationToken ct)
=> kind switch
{
MessageKind.ReadRawRequest => HandleReadRawAsync(body, writer, ct),
MessageKind.ReadProcessedRequest => HandleReadProcessedAsync(body, writer, ct),
MessageKind.ReadAtTimeRequest => HandleReadAtTimeAsync(body, writer, ct),
MessageKind.ReadEventsRequest => HandleReadEventsAsync(body, writer, ct),
MessageKind.WriteAlarmEventsRequest => HandleWriteAlarmEventsAsync(body, writer, ct),
_ => UnknownAsync(kind),
};
private Task UnknownAsync(MessageKind kind)
{
_logger.Warning("Sidecar received unsupported frame kind {Kind}; dropping", kind);
return Task.CompletedTask;
}
private async Task HandleReadRawAsync(byte[] body, FrameWriter writer, CancellationToken ct)
{
var req = MessagePackSerializer.Deserialize<ReadRawRequest>(body);
var reply = new ReadRawReply { CorrelationId = req.CorrelationId };
try
{
var samples = await _historian.ReadRawAsync(
req.TagName,
new DateTime(req.StartUtcTicks, DateTimeKind.Utc),
new DateTime(req.EndUtcTicks, DateTimeKind.Utc),
req.MaxValues,
ct).ConfigureAwait(false);
reply.Success = true;
reply.Samples = ToWire(samples);
}
catch (Exception ex)
{
_logger.Warning(ex, "Sidecar ReadRaw failed for {Tag}", req.TagName);
reply.Success = false;
reply.Error = ex.Message;
}
await writer.WriteAsync(MessageKind.ReadRawReply, reply, ct).ConfigureAwait(false);
}
private async Task HandleReadProcessedAsync(byte[] body, FrameWriter writer, CancellationToken ct)
{
var req = MessagePackSerializer.Deserialize<ReadProcessedRequest>(body);
var reply = new ReadProcessedReply { CorrelationId = req.CorrelationId };
try
{
var buckets = await _historian.ReadAggregateAsync(
req.TagName,
new DateTime(req.StartUtcTicks, DateTimeKind.Utc),
new DateTime(req.EndUtcTicks, DateTimeKind.Utc),
req.IntervalMs,
req.AggregateColumn,
ct).ConfigureAwait(false);
reply.Success = true;
reply.Buckets = ToWire(buckets);
}
catch (Exception ex)
{
_logger.Warning(ex, "Sidecar ReadProcessed failed for {Tag}", req.TagName);
reply.Success = false;
reply.Error = ex.Message;
}
await writer.WriteAsync(MessageKind.ReadProcessedReply, reply, ct).ConfigureAwait(false);
}
private async Task HandleReadAtTimeAsync(byte[] body, FrameWriter writer, CancellationToken ct)
{
var req = MessagePackSerializer.Deserialize<ReadAtTimeRequest>(body);
var reply = new ReadAtTimeReply { CorrelationId = req.CorrelationId };
try
{
var timestamps = new DateTime[req.TimestampsUtcTicks.Length];
for (var i = 0; i < timestamps.Length; i++)
timestamps[i] = new DateTime(req.TimestampsUtcTicks[i], DateTimeKind.Utc);
var samples = await _historian.ReadAtTimeAsync(req.TagName, timestamps, ct).ConfigureAwait(false);
reply.Success = true;
reply.Samples = ToWire(samples);
}
catch (Exception ex)
{
_logger.Warning(ex, "Sidecar ReadAtTime failed for {Tag}", req.TagName);
reply.Success = false;
reply.Error = ex.Message;
}
await writer.WriteAsync(MessageKind.ReadAtTimeReply, reply, ct).ConfigureAwait(false);
}
private async Task HandleReadEventsAsync(byte[] body, FrameWriter writer, CancellationToken ct)
{
var req = MessagePackSerializer.Deserialize<ReadEventsRequest>(body);
var reply = new ReadEventsReply { CorrelationId = req.CorrelationId };
try
{
var events = await _historian.ReadEventsAsync(
req.SourceName,
new DateTime(req.StartUtcTicks, DateTimeKind.Utc),
new DateTime(req.EndUtcTicks, DateTimeKind.Utc),
req.MaxEvents,
ct).ConfigureAwait(false);
reply.Success = true;
reply.Events = ToWire(events);
}
catch (Exception ex)
{
_logger.Warning(ex, "Sidecar ReadEvents failed for source {Source}", req.SourceName);
reply.Success = false;
reply.Error = ex.Message;
}
await writer.WriteAsync(MessageKind.ReadEventsReply, reply, ct).ConfigureAwait(false);
}
private async Task HandleWriteAlarmEventsAsync(byte[] body, FrameWriter writer, CancellationToken ct)
{
var req = MessagePackSerializer.Deserialize<WriteAlarmEventsRequest>(body);
var reply = new WriteAlarmEventsReply { CorrelationId = req.CorrelationId };
if (_alarmWriter is null)
{
reply.Success = false;
reply.Error = "Sidecar not configured with an alarm-event writer.";
reply.PerEventOk = new bool[req.Events.Length];
await writer.WriteAsync(MessageKind.WriteAlarmEventsReply, reply, ct).ConfigureAwait(false);
return;
}
try
{
var perEvent = await _alarmWriter.WriteAsync(req.Events, ct).ConfigureAwait(false);
reply.PerEventOk = perEvent;
reply.Success = true;
// Whole-batch Success stays true even when some events failed — per-event
// PerEventOk slots carry the granular result; the SQLite drain worker treats
// false slots as retry-please candidates.
}
catch (Exception ex)
{
_logger.Warning(ex, "Sidecar WriteAlarmEvents failed");
reply.Success = false;
reply.Error = ex.Message;
reply.PerEventOk = new bool[req.Events.Length];
}
await writer.WriteAsync(MessageKind.WriteAlarmEventsReply, reply, ct).ConfigureAwait(false);
}
private static HistorianSampleDto[] ToWire(List<HistorianSample> samples)
{
var dtos = new HistorianSampleDto[samples.Count];
for (var i = 0; i < samples.Count; i++)
{
var s = samples[i];
dtos[i] = new HistorianSampleDto
{
ValueBytes = s.Value is null ? null : MessagePackSerializer.Serialize(s.Value),
Quality = s.Quality,
TimestampUtcTicks = s.TimestampUtc.Ticks,
};
}
return dtos;
}
private static HistorianAggregateSampleDto[] ToWire(List<HistorianAggregateSample> samples)
{
var dtos = new HistorianAggregateSampleDto[samples.Count];
for (var i = 0; i < samples.Count; i++)
{
dtos[i] = new HistorianAggregateSampleDto
{
Value = samples[i].Value,
TimestampUtcTicks = samples[i].TimestampUtc.Ticks,
};
}
return dtos;
}
private static HistorianEventDto[] ToWire(List<Backend.HistorianEventDto> events)
{
var dtos = new HistorianEventDto[events.Count];
for (var i = 0; i < events.Count; i++)
{
var e = events[i];
dtos[i] = new HistorianEventDto
{
EventId = e.Id.ToString(),
Source = e.Source,
EventTimeUtcTicks = e.EventTime.Ticks,
ReceivedTimeUtcTicks = e.ReceivedTime.Ticks,
DisplayText = e.DisplayText,
Severity = e.Severity,
};
}
return dtos;
}
}
/// <summary>
/// Strategy for persisting alarm events into the Wonderware Alarm &amp; Events log. PR 3.W
/// supplies a real implementation that drives the aahClient SDK; PR 3.3 ships the
/// contract + a default null implementation so the sidecar can boot without one.
/// </summary>
public interface IAlarmEventWriter
{
/// <summary>
/// Writes a batch of alarm events. Returns one boolean per input event indicating
/// persisted vs. retry-please. The SQLite store-and-forward sink retries failed
/// slots on the next drain tick.
/// </summary>
Task<bool[]> WriteAsync(AlarmHistorianEventDto[] events, CancellationToken cancellationToken);
}