d7455577a8
Phase 1A of the ClusterClient→gRPC migration needs a wire contract for the seven messages SiteCommunicationActor forwards to /user/central-communication. This lands the contract and its mapper only — hosting (T1A.2) and the site-side transport seam (T1A.3) follow. `Protos/central_control.proto` (package scadabridge.centralcontrol.v1, service CentralControlService) declares SubmitNotification, QueryNotificationStatus, IngestAuditEvents, IngestCachedTelemetry, ReconcileSite, ReportSiteHealth and Heartbeat. Note the direction is the inverse of SiteStreamService: here the site dials and central serves. Decisions worth recording: - The two ingest RPCs IMPORT sitestream.proto and reuse AuditEventBatch / CachedTelemetryBatch / IngestAck rather than redeclaring them. The site telemetry actor already builds those messages, so a second copy would fork one wire contract into two kept in lockstep by hand. ForwardState / IngestedAtUtc stay off-wire exactly as they are today. - Heartbeat replies google.protobuf.Empty — it is fire-and-forget and must never surface a fault onto the heartbeat timer path. - The three NULLABLE SiteHealthReport collections travel inside single-field wrapper messages (ConnectionEndpointMapDto / TagQualityMapDto / NodeStatusListDto). proto3 cannot express presence on repeated/map fields, but null and empty genuinely differ here — SiteHealthCollector emits `ClusterNodes: _clusterNodes?.ToList()` and the central health surface reads null as "not reported", not as "reported empty". Same reasoning drives the BoolValue/Int64Value/DoubleValue wrappers on LocalDbReplicationConnected, LocalDbOplogBacklog and the two age gauges, whose docs are explicit that null is not zero/false. - ConnectionHealthEnum reserves 0 for UNSPECIFIED instead of mapping Connected onto it, and the decoder resolves anything unknown to ConnectionHealth.Error. An unrecognised connection state must not render as "healthy". - Guid? execution ids travel as "D" strings with empty meaning null; a malformed non-empty value throws rather than being laundered into "no correlation". - DateTimeOffset normalizes to a UTC instant (a protobuf Timestamp has no offset). Lossless in practice — every producer stamps UTC — and documented + asserted rather than left implicit. SiteCallDtoMapper gains a ToDto(SiteCall) overload. Its doc comment previously asserted such a method "would be dead code"; that held only while ClusterClient was the sole path from IngestCachedTelemetryCommand (which carries SiteCall, not SiteCallOperational) to central. Comment corrected alongside. Golden tests round-trip every message through a real protobuf encode/decode — DTO → proto → bytes → proto → DTO — with a fully-populated case and a null/empty/minimal case for every optional member. Verified to have teeth by mutation: dropping a scalar, collapsing an empty nullable collection to absent, and nulling a gauge each fail a test. Codegen stays CHECKED IN under CentralControlGrpc/ (protoc segfaults in the linux_arm64 Docker image); no active <Protobuf> item is committed. docker/regen-proto.sh is generalized to `regen-proto.sh [sitestream| centralcontrol|all]` — it now injects the ItemGroup rather than unwrapping a comment, so it no longer depends on there being exactly one Protobuf line, and it restores the csproj verbatim on every exit path.
119 lines
4.7 KiB
Bash
Executable File
119 lines
4.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Regenerates the gRPC C# files from the Communication project's .proto files.
|
|
#
|
|
# Background: protoc (linux/arm64) segfaults inside our Docker build container
|
|
# (Grpc.Tools 2.71.0). As a workaround the generated C# is checked into
|
|
# src/ZB.MOM.WW.ScadaBridge.Communication/ — SiteStreamGrpc/ for sitestream.proto
|
|
# and CentralControlGrpc/ for central_control.proto — and the Protobuf ItemGroup
|
|
# in the .csproj is commented out, so Docker just compiles the checked-in files.
|
|
#
|
|
# Run this script ON YOUR DEV MACHINE whenever a .proto changes:
|
|
#
|
|
# docker/regen-proto.sh [sitestream|centralcontrol|all] (default: all)
|
|
#
|
|
# 1. Injects a Protobuf ItemGroup for the selected proto(s) so Grpc.Tools runs.
|
|
# 2. Deletes the stale checked-in C# so a failed regen is obvious.
|
|
# 3. dotnet build (regen writes fresh files to obj/).
|
|
# 4. Copies the regenerated files back into the source tree.
|
|
# 5. Restores the original csproj so no active Protobuf item is left behind.
|
|
#
|
|
# Only the SELECTED protos get a Protobuf item. Enabling one whose generated C#
|
|
# is still checked in would define every generated type twice, which is why the
|
|
# per-proto selection exists. central_control.proto imports sitestream.proto,
|
|
# but protoc resolves that from the project-relative path — the import needs no
|
|
# Protobuf item of its own.
|
|
#
|
|
# Once we move to a Dockerfile base image that ships a working linux/arm64
|
|
# protoc, this script can be retired and Docker can regen the protos on every
|
|
# build like every other normal .NET project.
|
|
|
|
set -euo pipefail
|
|
|
|
TARGET="${1:-all}"
|
|
case "$TARGET" in
|
|
sitestream|centralcontrol|all) ;;
|
|
*) echo "usage: $0 [sitestream|centralcontrol|all]" >&2; exit 2 ;;
|
|
esac
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
COMM_DIR="$REPO_ROOT/src/ZB.MOM.WW.ScadaBridge.Communication"
|
|
CSPROJ="$COMM_DIR/ZB.MOM.WW.ScadaBridge.Communication.csproj"
|
|
GEN="$COMM_DIR/obj/Debug/net10.0/Protos"
|
|
|
|
echo "=== Regenerating gRPC files ($TARGET) ==="
|
|
|
|
if [[ ! -f "$CSPROJ" ]]; then
|
|
echo "ERROR: csproj not found at $CSPROJ" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Backup so we can always restore the comment state on failure. Leaving the
|
|
# csproj with an active Protobuf item is the one outcome that breaks Docker, so
|
|
# every exit path restores this copy.
|
|
BACKUP="$(mktemp)"
|
|
cp "$CSPROJ" "$BACKUP"
|
|
trap 'cp "$BACKUP" "$CSPROJ"; rm -f "$BACKUP"; echo "Restored csproj from backup."' ERR
|
|
|
|
# 1. Inject an ItemGroup holding just the selected protos, immediately before
|
|
# the closing </Project>. The documented commented-out block is left alone.
|
|
python3 - "$CSPROJ" "$TARGET" <<'PY'
|
|
import pathlib, sys
|
|
|
|
csproj, target = pathlib.Path(sys.argv[1]), sys.argv[2]
|
|
|
|
protos = []
|
|
if target in ("sitestream", "all"):
|
|
protos.append("sitestream.proto")
|
|
if target in ("centralcontrol", "all"):
|
|
protos.append("central_control.proto")
|
|
|
|
items = "\n".join(
|
|
f' <Protobuf Include="Protos\\{p}" GrpcServices="Both" />' for p in protos)
|
|
block = f" <ItemGroup>\n{items}\n </ItemGroup>\n\n</Project>"
|
|
|
|
src = csproj.read_text()
|
|
if "</Project>" not in src:
|
|
raise SystemExit("Couldn't find </Project> to inject the Protobuf ItemGroup before.")
|
|
csproj.write_text(src.replace("</Project>", block, 1))
|
|
PY
|
|
|
|
# 2. Delete the stale files so any failure to regen is obvious.
|
|
if [[ "$TARGET" == "sitestream" || "$TARGET" == "all" ]]; then
|
|
rm -f "$COMM_DIR/SiteStreamGrpc/Sitestream.cs" "$COMM_DIR/SiteStreamGrpc/SitestreamGrpc.cs"
|
|
fi
|
|
if [[ "$TARGET" == "centralcontrol" || "$TARGET" == "all" ]]; then
|
|
rm -f "$COMM_DIR/CentralControlGrpc/CentralControl.cs" \
|
|
"$COMM_DIR/CentralControlGrpc/CentralControlGrpc.cs"
|
|
fi
|
|
|
|
# 3. Regenerate by building.
|
|
echo "Building Communication project (regen)..."
|
|
dotnet build "$CSPROJ" --nologo -v minimal | tail -5
|
|
|
|
# 4. Copy generated files back into the source tree.
|
|
if [[ "$TARGET" == "sitestream" || "$TARGET" == "all" ]]; then
|
|
mkdir -p "$COMM_DIR/SiteStreamGrpc"
|
|
cp "$GEN/Sitestream.cs" "$GEN/SitestreamGrpc.cs" "$COMM_DIR/SiteStreamGrpc/"
|
|
echo "Copied regenerated files to SiteStreamGrpc/"
|
|
fi
|
|
if [[ "$TARGET" == "centralcontrol" || "$TARGET" == "all" ]]; then
|
|
mkdir -p "$COMM_DIR/CentralControlGrpc"
|
|
cp "$GEN/CentralControl.cs" "$GEN/CentralControlGrpc.cs" "$COMM_DIR/CentralControlGrpc/"
|
|
echo "Copied regenerated files to CentralControlGrpc/"
|
|
fi
|
|
|
|
# 5. Restore the backed-up csproj — i.e. drop the injected ItemGroup — so Docker
|
|
# builds keep working.
|
|
cp "$BACKUP" "$CSPROJ"
|
|
rm -f "$BACKUP"
|
|
trap - ERR
|
|
|
|
echo ""
|
|
echo "Done. Review and commit:"
|
|
echo " git diff src/ZB.MOM.WW.ScadaBridge.Communication/Protos/"
|
|
echo " git diff src/ZB.MOM.WW.ScadaBridge.Communication/SiteStreamGrpc/"
|
|
echo " git diff src/ZB.MOM.WW.ScadaBridge.Communication/CentralControlGrpc/"
|
|
echo " git diff -- src/ZB.MOM.WW.ScadaBridge.Communication/*.csproj # must be EMPTY"
|