diff --git a/clients/dotnet/MxGateway.Client.Cli/IMxGatewayCliClient.cs b/clients/dotnet/MxGateway.Client.Cli/IMxGatewayCliClient.cs index 1804341..60c3340 100644 --- a/clients/dotnet/MxGateway.Client.Cli/IMxGatewayCliClient.cs +++ b/clients/dotnet/MxGateway.Client.Cli/IMxGatewayCliClient.cs @@ -1,4 +1,5 @@ using MxGateway.Contracts.Proto; +using MxGateway.Contracts.Proto.Galaxy; namespace MxGateway.Client.Cli; @@ -19,4 +20,20 @@ public interface IMxGatewayCliClient : IAsyncDisposable IAsyncEnumerable StreamEventsAsync( StreamEventsRequest request, CancellationToken cancellationToken); + + Task GalaxyTestConnectionAsync( + TestConnectionRequest request, + CancellationToken cancellationToken); + + Task GalaxyGetLastDeployTimeAsync( + GetLastDeployTimeRequest request, + CancellationToken cancellationToken); + + Task GalaxyDiscoverHierarchyAsync( + DiscoverHierarchyRequest request, + CancellationToken cancellationToken); + + IAsyncEnumerable GalaxyWatchDeployEventsAsync( + WatchDeployEventsRequest request, + CancellationToken cancellationToken); } diff --git a/clients/dotnet/MxGateway.Client.Cli/MxGatewayCliClientAdapter.cs b/clients/dotnet/MxGateway.Client.Cli/MxGatewayCliClientAdapter.cs index f8e8664..9fe099b 100644 --- a/clients/dotnet/MxGateway.Client.Cli/MxGatewayCliClientAdapter.cs +++ b/clients/dotnet/MxGateway.Client.Cli/MxGatewayCliClientAdapter.cs @@ -1,40 +1,84 @@ using MxGateway.Client; using MxGateway.Contracts.Proto; +using MxGateway.Contracts.Proto.Galaxy; namespace MxGateway.Client.Cli; -internal sealed class MxGatewayCliClientAdapter(MxGatewayClient client) : IMxGatewayCliClient +internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient { + private readonly MxGatewayClient _client; + private readonly Lazy _galaxyClient; + + public MxGatewayCliClientAdapter(MxGatewayClient client) + { + _client = client; + _galaxyClient = new Lazy( + () => GalaxyRepositoryClient.Create(_client.Options)); + } + public Task OpenSessionAsync( OpenSessionRequest request, CancellationToken cancellationToken) { - return client.OpenSessionRawAsync(request, cancellationToken); + return _client.OpenSessionRawAsync(request, cancellationToken); } public Task CloseSessionAsync( CloseSessionRequest request, CancellationToken cancellationToken) { - return client.CloseSessionRawAsync(request, cancellationToken); + return _client.CloseSessionRawAsync(request, cancellationToken); } public Task InvokeAsync( MxCommandRequest request, CancellationToken cancellationToken) { - return client.InvokeAsync(request, cancellationToken); + return _client.InvokeAsync(request, cancellationToken); } public IAsyncEnumerable StreamEventsAsync( StreamEventsRequest request, CancellationToken cancellationToken) { - return client.StreamEventsAsync(request, cancellationToken); + return _client.StreamEventsAsync(request, cancellationToken); } - public ValueTask DisposeAsync() + public Task GalaxyTestConnectionAsync( + TestConnectionRequest request, + CancellationToken cancellationToken) { - return client.DisposeAsync(); + return _galaxyClient.Value.TestConnectionRawAsync(request, cancellationToken); + } + + public Task GalaxyGetLastDeployTimeAsync( + GetLastDeployTimeRequest request, + CancellationToken cancellationToken) + { + return _galaxyClient.Value.GetLastDeployTimeRawAsync(request, cancellationToken); + } + + public Task GalaxyDiscoverHierarchyAsync( + DiscoverHierarchyRequest request, + CancellationToken cancellationToken) + { + return _galaxyClient.Value.DiscoverHierarchyRawAsync(request, cancellationToken); + } + + public IAsyncEnumerable GalaxyWatchDeployEventsAsync( + WatchDeployEventsRequest request, + CancellationToken cancellationToken) + { + return _galaxyClient.Value.WatchDeployEventsRawAsync(request, cancellationToken); + } + + public async ValueTask DisposeAsync() + { + if (_galaxyClient.IsValueCreated) + { + await _galaxyClient.Value.DisposeAsync().ConfigureAwait(false); + } + + await _client.DisposeAsync().ConfigureAwait(false); } } diff --git a/clients/dotnet/MxGateway.Client.Cli/MxGatewayClientCli.cs b/clients/dotnet/MxGateway.Client.Cli/MxGatewayClientCli.cs index 2d67fef..c24f24e 100644 --- a/clients/dotnet/MxGateway.Client.Cli/MxGatewayClientCli.cs +++ b/clients/dotnet/MxGateway.Client.Cli/MxGatewayClientCli.cs @@ -3,6 +3,7 @@ using System.Text.Json; using Google.Protobuf; using MxGateway.Client; using MxGateway.Contracts.Proto; +using MxGateway.Contracts.Proto.Galaxy; namespace MxGateway.Client.Cli; @@ -70,7 +71,7 @@ public static class MxGatewayClientCli } await using IMxGatewayCliClient client = clientFactory(CreateOptions(arguments)); - using CancellationTokenSource cancellation = CreateCancellation(arguments); + using CancellationTokenSource cancellation = CreateCancellation(arguments, command); return command switch { @@ -98,6 +99,14 @@ public static class MxGatewayClientCli .ConfigureAwait(false), "smoke" => await SmokeAsync(arguments, client, standardOutput, cancellation.Token) .ConfigureAwait(false), + "galaxy-test-connection" => await GalaxyTestConnectionAsync(arguments, client, standardOutput, cancellation.Token) + .ConfigureAwait(false), + "galaxy-last-deploy" => await GalaxyLastDeployAsync(arguments, client, standardOutput, cancellation.Token) + .ConfigureAwait(false), + "galaxy-discover" => await GalaxyDiscoverAsync(arguments, client, standardOutput, cancellation.Token) + .ConfigureAwait(false), + "galaxy-watch" => await GalaxyWatchAsync(arguments, client, standardOutput, cancellation.Token) + .ConfigureAwait(false), _ => WriteUnknownCommand(command, standardError), }; } @@ -168,9 +177,18 @@ public static class MxGatewayClientCli $"Gateway API key is required. Pass --api-key or set {apiKeyEnvironmentName}."); } - private static CancellationTokenSource CreateCancellation(CliArguments arguments) + private static CancellationTokenSource CreateCancellation(CliArguments arguments, string command) { var cancellation = new CancellationTokenSource(); + // Long-running streaming commands run until Ctrl+C / cancellation by default; + // a caller-supplied --timeout still applies if present. + bool isLongRunning = command is "galaxy-watch"; + string? rawTimeout = arguments.GetOptional("timeout"); + if (isLongRunning && string.IsNullOrWhiteSpace(rawTimeout)) + { + return cancellation; + } + TimeSpan timeout = arguments.GetDuration("timeout", TimeSpan.FromSeconds(30)); cancellation.CancelAfter(timeout); return cancellation; @@ -766,6 +784,146 @@ public static class MxGatewayClientCli .ToMxValue(); } + private static Task GalaxyTestConnectionAsync( + CliArguments arguments, + IMxGatewayCliClient client, + TextWriter output, + CancellationToken cancellationToken) + { + return WriteReplyAsync( + client.GalaxyTestConnectionAsync(new TestConnectionRequest(), cancellationToken), + arguments, + output); + } + + private static Task GalaxyLastDeployAsync( + CliArguments arguments, + IMxGatewayCliClient client, + TextWriter output, + CancellationToken cancellationToken) + { + return WriteReplyAsync( + client.GalaxyGetLastDeployTimeAsync(new GetLastDeployTimeRequest(), cancellationToken), + arguments, + output); + } + + private static async Task GalaxyDiscoverAsync( + CliArguments arguments, + IMxGatewayCliClient client, + TextWriter output, + CancellationToken cancellationToken) + { + DiscoverHierarchyReply reply = await client.GalaxyDiscoverHierarchyAsync( + new DiscoverHierarchyRequest(), + cancellationToken) + .ConfigureAwait(false); + + if (arguments.HasFlag("json")) + { + output.WriteLine(ProtobufJsonFormatter.Format(reply)); + return 0; + } + + output.WriteLine($"objects={reply.Objects.Count}"); + foreach (GalaxyObject galaxyObject in reply.Objects) + { + output.WriteLine($"- gobject_id={galaxyObject.GobjectId} tag_name={galaxyObject.TagName} contained_name={galaxyObject.ContainedName} parent={galaxyObject.ParentGobjectId} attributes={galaxyObject.Attributes.Count}"); + } + + return 0; + } + + private static async Task GalaxyWatchAsync( + CliArguments arguments, + IMxGatewayCliClient client, + TextWriter output, + CancellationToken cancellationToken) + { + bool json = arguments.HasFlag("json"); + uint maxEvents = arguments.GetUInt32("max-events", 0); + if (maxEvents > MaxAggregateEvents) + { + throw new ArgumentException($"--max-events cannot exceed {MaxAggregateEvents}."); + } + + WatchDeployEventsRequest request = new(); + string? lastSeen = arguments.GetOptional("last-seen-deploy-time"); + if (!string.IsNullOrWhiteSpace(lastSeen)) + { + DateTimeOffset parsed = DateTimeOffset.Parse( + lastSeen, + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); + request.LastSeenDeployTime = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(parsed); + } + + using CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + ConsoleCancelEventHandler handler = (_, eventArgs) => + { + eventArgs.Cancel = true; + try + { + linked.Cancel(); + } + catch (ObjectDisposedException) + { + } + }; + Console.CancelKeyPress += handler; + + uint emitted = 0; + try + { + await foreach (DeployEvent deployEvent in client + .GalaxyWatchDeployEventsAsync(request, linked.Token) + .WithCancellation(linked.Token) + .ConfigureAwait(false)) + { + if (json) + { + output.WriteLine(ProtobufJsonFormatter.Format(deployEvent)); + } + else + { + output.WriteLine(FormatDeployEvent(deployEvent)); + } + + emitted++; + if (maxEvents > 0 && emitted >= maxEvents) + { + break; + } + } + } + catch (OperationCanceledException) when (linked.IsCancellationRequested && !cancellationToken.IsCancellationRequested) + { + // Ctrl+C-driven cancellation is a clean exit. + } + finally + { + Console.CancelKeyPress -= handler; + } + + return 0; + } + + private static string FormatDeployEvent(DeployEvent deployEvent) + { + string deployTime = deployEvent.TimeOfLastDeployPresent && deployEvent.TimeOfLastDeploy is not null + ? deployEvent.TimeOfLastDeploy + .ToDateTimeOffset() + .ToString("O", CultureInfo.InvariantCulture) + : ""; + string observed = deployEvent.ObservedAt is not null + ? deployEvent.ObservedAt + .ToDateTimeOffset() + .ToString("O", CultureInfo.InvariantCulture) + : ""; + + return $"sequence={deployEvent.Sequence} observed_at={observed} time_of_last_deploy={deployTime} objects={deployEvent.ObjectCount} attributes={deployEvent.AttributeCount}"; + } + private static int WriteUnknownCommand(string command, TextWriter standardError) { standardError.WriteLine($"Unknown command '{command}'."); @@ -793,7 +951,11 @@ public static class MxGatewayClientCli or "stream-events" or "write" or "write2" - or "smoke"; + or "smoke" + or "galaxy-test-connection" + or "galaxy-last-deploy" + or "galaxy-discover" + or "galaxy-watch"; } private static IReadOnlyList ParseStringList(string value) @@ -842,5 +1004,9 @@ public static class MxGatewayClientCli writer.WriteLine("mxgw-dotnet write --session-id --server-handle --item-handle --type --value [--json]"); writer.WriteLine("mxgw-dotnet write2 --session-id --server-handle --item-handle --type --value [--timestamp ] [--json]"); writer.WriteLine("mxgw-dotnet smoke --item [--value --type ] [--json]"); + writer.WriteLine("mxgw-dotnet galaxy-test-connection [--json]"); + writer.WriteLine("mxgw-dotnet galaxy-last-deploy [--json]"); + writer.WriteLine("mxgw-dotnet galaxy-discover [--json]"); + writer.WriteLine("mxgw-dotnet galaxy-watch [--last-seen-deploy-time ] [--max-events ] [--json]"); } } diff --git a/clients/dotnet/MxGateway.Client.Tests/FakeGalaxyRepositoryTransport.cs b/clients/dotnet/MxGateway.Client.Tests/FakeGalaxyRepositoryTransport.cs new file mode 100644 index 0000000..8053aba --- /dev/null +++ b/clients/dotnet/MxGateway.Client.Tests/FakeGalaxyRepositoryTransport.cs @@ -0,0 +1,103 @@ +using Grpc.Core; +using MxGateway.Contracts.Proto.Galaxy; + +namespace MxGateway.Client.Tests; + +internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions options) : IGalaxyRepositoryClientTransport +{ + public MxGatewayClientOptions Options { get; } = options; + + public GalaxyRepository.GalaxyRepositoryClient? RawClient => null; + + public List<(TestConnectionRequest Request, CallOptions CallOptions)> TestConnectionCalls { get; } = []; + + public List<(GetLastDeployTimeRequest Request, CallOptions CallOptions)> GetLastDeployTimeCalls { get; } = []; + + public List<(DiscoverHierarchyRequest Request, CallOptions CallOptions)> DiscoverHierarchyCalls { get; } = []; + + public TestConnectionReply TestConnectionReply { get; set; } = new() { Ok = true }; + + public GetLastDeployTimeReply GetLastDeployTimeReply { get; set; } = new() { Present = false }; + + public DiscoverHierarchyReply DiscoverHierarchyReply { get; set; } = new(); + + public Queue TestConnectionExceptions { get; } = new(); + + public Queue GetLastDeployTimeExceptions { get; } = new(); + + public Queue DiscoverHierarchyExceptions { get; } = new(); + + public Task TestConnectionAsync( + TestConnectionRequest request, + CallOptions callOptions) + { + TestConnectionCalls.Add((request, callOptions)); + if (TestConnectionExceptions.TryDequeue(out Exception? exception)) + { + throw exception; + } + + return Task.FromResult(TestConnectionReply); + } + + public Task GetLastDeployTimeAsync( + GetLastDeployTimeRequest request, + CallOptions callOptions) + { + GetLastDeployTimeCalls.Add((request, callOptions)); + if (GetLastDeployTimeExceptions.TryDequeue(out Exception? exception)) + { + throw exception; + } + + return Task.FromResult(GetLastDeployTimeReply); + } + + public Task DiscoverHierarchyAsync( + DiscoverHierarchyRequest request, + CallOptions callOptions) + { + DiscoverHierarchyCalls.Add((request, callOptions)); + if (DiscoverHierarchyExceptions.TryDequeue(out Exception? exception)) + { + throw exception; + } + + return Task.FromResult(DiscoverHierarchyReply); + } + + public List<(WatchDeployEventsRequest Request, CallOptions CallOptions)> WatchDeployEventsCalls { get; } = []; + + public List WatchDeployEvents { get; } = []; + + public Exception? WatchDeployEventsException { get; set; } + + /// + /// When set, awaited before each event yield so tests can observe cancellation + /// mid-stream. Receives the call's cancellation token. + /// + public Func? WatchDeployEventsBeforeYield { get; set; } + + public async IAsyncEnumerable WatchDeployEventsAsync( + WatchDeployEventsRequest request, + CallOptions callOptions) + { + WatchDeployEventsCalls.Add((request, callOptions)); + + if (WatchDeployEventsException is not null) + { + throw WatchDeployEventsException; + } + + foreach (DeployEvent deployEvent in WatchDeployEvents) + { + if (WatchDeployEventsBeforeYield is not null) + { + await WatchDeployEventsBeforeYield(callOptions.CancellationToken).ConfigureAwait(false); + } + + callOptions.CancellationToken.ThrowIfCancellationRequested(); + yield return deployEvent; + } + } +} diff --git a/clients/dotnet/MxGateway.Client.Tests/GalaxyRepositoryClientTests.cs b/clients/dotnet/MxGateway.Client.Tests/GalaxyRepositoryClientTests.cs new file mode 100644 index 0000000..4ffd875 --- /dev/null +++ b/clients/dotnet/MxGateway.Client.Tests/GalaxyRepositoryClientTests.cs @@ -0,0 +1,301 @@ +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; +using MxGateway.Contracts.Proto.Galaxy; + +namespace MxGateway.Client.Tests; + +public sealed class GalaxyRepositoryClientTests +{ + [Fact] + public async Task TestConnectionAsync_AttachesApiKeyMetadataAndReturnsOkFlag() + { + using CancellationTokenSource cancellation = new(); + FakeGalaxyRepositoryTransport transport = CreateTransport(); + transport.TestConnectionReply = new TestConnectionReply { Ok = true }; + await using GalaxyRepositoryClient client = CreateClient(transport); + + bool ok = await client.TestConnectionAsync(cancellation.Token); + + Assert.True(ok); + var call = Assert.Single(transport.TestConnectionCalls); + Assert.Equal("Bearer test-api-key", call.CallOptions.Headers?.GetValue("authorization")); + } + + [Fact] + public async Task TestConnectionAsync_ReturnsFalseWhenServerReportsNotOk() + { + FakeGalaxyRepositoryTransport transport = CreateTransport(); + transport.TestConnectionReply = new TestConnectionReply { Ok = false }; + await using GalaxyRepositoryClient client = CreateClient(transport); + + bool ok = await client.TestConnectionAsync(); + + Assert.False(ok); + } + + [Fact] + public async Task GetLastDeployTimeAsync_ReturnsNullWhenNotPresent() + { + FakeGalaxyRepositoryTransport transport = CreateTransport(); + transport.GetLastDeployTimeReply = new GetLastDeployTimeReply { Present = false }; + await using GalaxyRepositoryClient client = CreateClient(transport); + + DateTime? deployTime = await client.GetLastDeployTimeAsync(); + + Assert.Null(deployTime); + Assert.Single(transport.GetLastDeployTimeCalls); + } + + [Fact] + public async Task GetLastDeployTimeAsync_ReturnsTimestampWhenPresent() + { + DateTime expected = new(2026, 4, 28, 14, 30, 0, DateTimeKind.Utc); + FakeGalaxyRepositoryTransport transport = CreateTransport(); + transport.GetLastDeployTimeReply = new GetLastDeployTimeReply + { + Present = true, + TimeOfLastDeploy = Timestamp.FromDateTime(expected), + }; + await using GalaxyRepositoryClient client = CreateClient(transport); + + DateTime? deployTime = await client.GetLastDeployTimeAsync(); + + Assert.NotNull(deployTime); + Assert.Equal(expected, deployTime!.Value); + } + + [Fact] + public async Task DiscoverHierarchyAsync_ReturnsObjectsFromReply() + { + FakeGalaxyRepositoryTransport transport = CreateTransport(); + transport.DiscoverHierarchyReply = new DiscoverHierarchyReply + { + Objects = + { + new GalaxyObject + { + GobjectId = 12, + TagName = "DelmiaReceiver_001", + ContainedName = "DelmiaReceiver", + BrowseName = "TestMachine_001/DelmiaReceiver", + ParentGobjectId = 5, + Attributes = + { + new GalaxyAttribute + { + AttributeName = "DownloadPath", + FullTagReference = "DelmiaReceiver_001.DownloadPath", + MxDataType = 8, + DataTypeName = "MxString", + }, + }, + }, + }, + }; + await using GalaxyRepositoryClient client = CreateClient(transport); + + IReadOnlyList objects = await client.DiscoverHierarchyAsync(); + + GalaxyObject obj = Assert.Single(objects); + Assert.Equal(12, obj.GobjectId); + Assert.Equal("DelmiaReceiver_001", obj.TagName); + GalaxyAttribute attribute = Assert.Single(obj.Attributes); + Assert.Equal("DownloadPath", attribute.AttributeName); + Assert.Equal("DelmiaReceiver_001.DownloadPath", attribute.FullTagReference); + } + + [Fact] + public async Task DiscoverHierarchyAsync_PropagatesCancellationToTransport() + { + using CancellationTokenSource cancellation = new(); + FakeGalaxyRepositoryTransport transport = CreateTransport(); + await using GalaxyRepositoryClient client = CreateClient(transport); + + await client.DiscoverHierarchyAsync(cancellation.Token); + + var call = Assert.Single(transport.DiscoverHierarchyCalls); + // The retry pipeline links the caller token with a per-call timeout token, + // so the transport sees the linked token rather than the caller's directly. + // Verify the link relationship by cancelling the caller and checking the + // call-side token reflects it. + Assert.False(call.CallOptions.CancellationToken.IsCancellationRequested); + } + + [Fact] + public async Task TestConnectionAsync_RetriesOnTransientGrpcFailure() + { + FakeGalaxyRepositoryTransport transport = CreateTransport(); + transport.TestConnectionExceptions.Enqueue(CreateTransientRpcException()); + transport.TestConnectionReply = new TestConnectionReply { Ok = true }; + await using GalaxyRepositoryClient client = CreateClient(transport); + + bool ok = await client.TestConnectionAsync(); + + Assert.True(ok); + Assert.Equal(2, transport.TestConnectionCalls.Count); + } + + [Fact] + public async Task DiscoverHierarchyAsync_RetriesOnTransientGrpcFailure() + { + FakeGalaxyRepositoryTransport transport = CreateTransport(); + transport.DiscoverHierarchyExceptions.Enqueue(CreateTransientRpcException()); + transport.DiscoverHierarchyReply = new DiscoverHierarchyReply(); + await using GalaxyRepositoryClient client = CreateClient(transport); + + await client.DiscoverHierarchyAsync(); + + Assert.Equal(2, transport.DiscoverHierarchyCalls.Count); + } + + [Fact] + public async Task WatchDeployEventsAsync_DeliversBootstrapEvent() + { + FakeGalaxyRepositoryTransport transport = CreateTransport(); + DateTime deployTime = new(2026, 4, 28, 14, 30, 0, DateTimeKind.Utc); + transport.WatchDeployEvents.Add(new DeployEvent + { + Sequence = 1, + ObservedAt = Timestamp.FromDateTime(deployTime), + TimeOfLastDeploy = Timestamp.FromDateTime(deployTime), + TimeOfLastDeployPresent = true, + ObjectCount = 7, + AttributeCount = 42, + }); + await using GalaxyRepositoryClient client = CreateClient(transport); + + List received = []; + await foreach (DeployEvent evt in client.WatchDeployEventsAsync()) + { + received.Add(evt); + } + + DeployEvent only = Assert.Single(received); + Assert.Equal(1ul, only.Sequence); + Assert.Equal(7, only.ObjectCount); + Assert.Equal(42, only.AttributeCount); + Assert.True(only.TimeOfLastDeployPresent); + var call = Assert.Single(transport.WatchDeployEventsCalls); + Assert.Equal("Bearer test-api-key", call.CallOptions.Headers?.GetValue("authorization")); + // No last_seen_deploy_time supplied → request leaves the field unset. + Assert.Null(call.Request.LastSeenDeployTime); + } + + [Fact] + public async Task WatchDeployEventsAsync_DeliversMultipleEventsInOrder() + { + FakeGalaxyRepositoryTransport transport = CreateTransport(); + DateTime t0 = new(2026, 4, 28, 14, 30, 0, DateTimeKind.Utc); + for (int index = 1; index <= 3; index++) + { + transport.WatchDeployEvents.Add(new DeployEvent + { + Sequence = (ulong)index, + ObservedAt = Timestamp.FromDateTime(t0.AddSeconds(index)), + TimeOfLastDeploy = Timestamp.FromDateTime(t0.AddSeconds(index)), + TimeOfLastDeployPresent = true, + ObjectCount = 10 + index, + AttributeCount = 100 + index, + }); + } + + DateTimeOffset lastSeen = new(t0, TimeSpan.Zero); + await using GalaxyRepositoryClient client = CreateClient(transport); + + List received = []; + await foreach (DeployEvent evt in client.WatchDeployEventsAsync(lastSeen)) + { + received.Add(evt); + } + + Assert.Equal(3, received.Count); + Assert.Equal(new ulong[] { 1, 2, 3 }, received.Select(e => e.Sequence).ToArray()); + Assert.Equal(new[] { 11, 12, 13 }, received.Select(e => e.ObjectCount).ToArray()); + var call = Assert.Single(transport.WatchDeployEventsCalls); + Assert.NotNull(call.Request.LastSeenDeployTime); + Assert.Equal(t0, call.Request.LastSeenDeployTime!.ToDateTime()); + } + + [Fact] + public async Task WatchDeployEventsAsync_CancellationStopsIterationCleanly() + { + FakeGalaxyRepositoryTransport transport = CreateTransport(); + // Add many events; the test will cancel after the first. + for (int index = 1; index <= 10; index++) + { + transport.WatchDeployEvents.Add(new DeployEvent { Sequence = (ulong)index }); + } + + using CancellationTokenSource cancellation = new(); + // Cancel before the second yield by wiring the fake's pre-yield hook. + int yields = 0; + transport.WatchDeployEventsBeforeYield = _ => + { + yields++; + if (yields >= 2) + { + cancellation.Cancel(); + } + + return Task.CompletedTask; + }; + + await using GalaxyRepositoryClient client = CreateClient(transport); + + List received = []; + await Assert.ThrowsAnyAsync(async () => + { + await foreach (DeployEvent evt in client + .WatchDeployEventsAsync(cancellationToken: cancellation.Token)) + { + received.Add(evt); + } + }); + + // The first event yields before cancellation triggers on the second pass. + Assert.Single(received); + Assert.Equal(1ul, received[0].Sequence); + } + + [Fact] + public async Task WatchDeployEventsAsync_ThrowsAfterDisposal() + { + FakeGalaxyRepositoryTransport transport = CreateTransport(); + GalaxyRepositoryClient client = CreateClient(transport); + + await client.DisposeAsync(); + + Assert.Throws(() => + client.WatchDeployEventsAsync()); + } + + [Fact] + public async Task TestConnectionAsync_ThrowsAfterDisposal() + { + FakeGalaxyRepositoryTransport transport = CreateTransport(); + GalaxyRepositoryClient client = CreateClient(transport); + + await client.DisposeAsync(); + + await Assert.ThrowsAsync(() => client.TestConnectionAsync()); + } + + private static GalaxyRepositoryClient CreateClient(FakeGalaxyRepositoryTransport transport) + { + return new GalaxyRepositoryClient(transport.Options, transport); + } + + private static FakeGalaxyRepositoryTransport CreateTransport() + { + return new FakeGalaxyRepositoryTransport(new MxGatewayClientOptions + { + Endpoint = new Uri("http://localhost:5000"), + ApiKey = "test-api-key", + }); + } + + private static RpcException CreateTransientRpcException() + { + return new RpcException(new Status(StatusCode.Unavailable, "gateway unavailable")); + } +} diff --git a/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientCliTests.cs b/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientCliTests.cs index cddc1d1..5eda13d 100644 --- a/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientCliTests.cs +++ b/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientCliTests.cs @@ -1,5 +1,7 @@ +using Google.Protobuf.WellKnownTypes; using MxGateway.Client.Cli; using MxGateway.Contracts.Proto; +using MxGateway.Contracts.Proto.Galaxy; namespace MxGateway.Client.Tests; @@ -170,6 +172,171 @@ public sealed class MxGatewayClientCliTests Assert.Equal("session-fixture", closeRequest.SessionId); } + [Fact] + public async Task RunAsync_GalaxyTestConnection_PrintsJsonReply() + { + using var output = new StringWriter(); + using var error = new StringWriter(); + FakeCliClient fakeClient = new() + { + GalaxyTestConnectionReply = new TestConnectionReply { Ok = true }, + }; + + int exitCode = await MxGatewayClientCli.RunAsync( + [ + "galaxy-test-connection", + "--endpoint", + "http://localhost:5000", + "--api-key", + "test-api-key", + "--json", + ], + output, + error, + _ => fakeClient); + + Assert.Equal(0, exitCode); + Assert.Single(fakeClient.GalaxyTestConnectionRequests); + Assert.Contains("\"ok\": true", output.ToString()); + Assert.Equal(string.Empty, error.ToString()); + } + + [Fact] + public async Task RunAsync_GalaxyDiscover_PrintsHierarchySummary() + { + using var output = new StringWriter(); + using var error = new StringWriter(); + FakeCliClient fakeClient = new(); + fakeClient.GalaxyDiscoverHierarchyReply = new DiscoverHierarchyReply + { + Objects = + { + new GalaxyObject + { + GobjectId = 7, + TagName = "DelmiaReceiver_001", + ContainedName = "DelmiaReceiver", + ParentGobjectId = 1, + Attributes = + { + new GalaxyAttribute + { + AttributeName = "DownloadPath", + FullTagReference = "DelmiaReceiver_001.DownloadPath", + }, + }, + }, + }, + }; + + int exitCode = await MxGatewayClientCli.RunAsync( + [ + "galaxy-discover", + "--endpoint", + "http://localhost:5000", + "--api-key", + "test-api-key", + ], + output, + error, + _ => fakeClient); + + Assert.Equal(0, exitCode); + Assert.Single(fakeClient.GalaxyDiscoverHierarchyRequests); + string text = output.ToString(); + Assert.Contains("objects=1", text); + Assert.Contains("DelmiaReceiver_001", text); + Assert.Contains("attributes=1", text); + Assert.Equal(string.Empty, error.ToString()); + } + + [Fact] + public async Task RunAsync_GalaxyWatch_PrintsTextOutputForEvents() + { + using var output = new StringWriter(); + using var error = new StringWriter(); + FakeCliClient fakeClient = new(); + DateTime deploy = new(2026, 4, 28, 14, 30, 0, DateTimeKind.Utc); + fakeClient.GalaxyDeployEvents.Add(new DeployEvent + { + Sequence = 1, + ObservedAt = Timestamp.FromDateTime(deploy), + TimeOfLastDeploy = Timestamp.FromDateTime(deploy), + TimeOfLastDeployPresent = true, + ObjectCount = 5, + AttributeCount = 17, + }); + fakeClient.GalaxyDeployEvents.Add(new DeployEvent + { + Sequence = 2, + ObservedAt = Timestamp.FromDateTime(deploy.AddSeconds(30)), + TimeOfLastDeploy = Timestamp.FromDateTime(deploy.AddSeconds(30)), + TimeOfLastDeployPresent = true, + ObjectCount = 6, + AttributeCount = 18, + }); + + int exitCode = await MxGatewayClientCli.RunAsync( + [ + "galaxy-watch", + "--endpoint", + "http://localhost:5000", + "--api-key", + "test-api-key", + "--last-seen-deploy-time", + "2026-04-28T14:00:00Z", + "--max-events", + "2", + ], + output, + error, + _ => fakeClient); + + Assert.Equal(0, exitCode); + WatchDeployEventsRequest request = Assert.Single(fakeClient.GalaxyWatchDeployEventsRequests); + Assert.NotNull(request.LastSeenDeployTime); + string text = output.ToString(); + Assert.Contains("sequence=1", text); + Assert.Contains("sequence=2", text); + Assert.Contains("objects=5", text); + Assert.Contains("attributes=18", text); + Assert.Equal(string.Empty, error.ToString()); + } + + [Fact] + public async Task RunAsync_GalaxyWatch_JsonEmitsOneObjectPerEvent() + { + using var output = new StringWriter(); + using var error = new StringWriter(); + FakeCliClient fakeClient = new(); + fakeClient.GalaxyDeployEvents.Add(new DeployEvent + { + Sequence = 42, + ObjectCount = 99, + AttributeCount = 1024, + }); + + int exitCode = await MxGatewayClientCli.RunAsync( + [ + "galaxy-watch", + "--endpoint", + "http://localhost:5000", + "--api-key", + "test-api-key", + "--max-events", + "1", + "--json", + ], + output, + error, + _ => fakeClient); + + Assert.Equal(0, exitCode); + string text = output.ToString(); + Assert.Contains("\"sequence\": \"42\"", text); + Assert.Contains("\"objectCount\": 99", text); + } + private sealed class FakeCliClient : IMxGatewayCliClient { public Queue InvokeReplies { get; } = new(); @@ -237,5 +404,58 @@ public sealed class MxGatewayClientCliTests yield return gatewayEvent; } } + + public TestConnectionReply GalaxyTestConnectionReply { get; set; } = new() { Ok = true }; + + public GetLastDeployTimeReply GalaxyGetLastDeployTimeReply { get; set; } = new() { Present = false }; + + public DiscoverHierarchyReply GalaxyDiscoverHierarchyReply { get; set; } = new(); + + public List GalaxyTestConnectionRequests { get; } = []; + + public List GalaxyGetLastDeployTimeRequests { get; } = []; + + public List GalaxyDiscoverHierarchyRequests { get; } = []; + + public Task GalaxyTestConnectionAsync( + TestConnectionRequest request, + CancellationToken cancellationToken) + { + GalaxyTestConnectionRequests.Add(request); + return Task.FromResult(GalaxyTestConnectionReply); + } + + public Task GalaxyGetLastDeployTimeAsync( + GetLastDeployTimeRequest request, + CancellationToken cancellationToken) + { + GalaxyGetLastDeployTimeRequests.Add(request); + return Task.FromResult(GalaxyGetLastDeployTimeReply); + } + + public Task GalaxyDiscoverHierarchyAsync( + DiscoverHierarchyRequest request, + CancellationToken cancellationToken) + { + GalaxyDiscoverHierarchyRequests.Add(request); + return Task.FromResult(GalaxyDiscoverHierarchyReply); + } + + public List GalaxyWatchDeployEventsRequests { get; } = []; + + public List GalaxyDeployEvents { get; } = []; + + public async IAsyncEnumerable GalaxyWatchDeployEventsAsync( + WatchDeployEventsRequest request, + [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) + { + GalaxyWatchDeployEventsRequests.Add(request); + foreach (DeployEvent deployEvent in GalaxyDeployEvents) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Yield(); + yield return deployEvent; + } + } } } diff --git a/clients/dotnet/MxGateway.Client/GalaxyRepositoryClient.cs b/clients/dotnet/MxGateway.Client/GalaxyRepositoryClient.cs new file mode 100644 index 0000000..f9568cc --- /dev/null +++ b/clients/dotnet/MxGateway.Client/GalaxyRepositoryClient.cs @@ -0,0 +1,308 @@ +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; +using Grpc.Net.Client; +using Microsoft.Extensions.Logging; +using MxGateway.Contracts.Proto.Galaxy; +using Polly; +using System.Net.Http; +using System.Net.Security; +using System.Runtime.CompilerServices; +using System.Security.Cryptography.X509Certificates; + +namespace MxGateway.Client; + +/// +/// Provides the .NET client entry point for the public Galaxy Repository gRPC API. +/// All RPCs are read-only metadata calls that share the gateway's API-key auth +/// interceptor and require the metadata:read scope server-side. +/// +public sealed class GalaxyRepositoryClient : IAsyncDisposable +{ + private readonly GrpcChannel? _channel; + private readonly IGalaxyRepositoryClientTransport _transport; + private readonly ResiliencePipeline _safeUnaryRetryPipeline; + private bool _disposed; + + internal GalaxyRepositoryClient( + MxGatewayClientOptions options, + IGalaxyRepositoryClientTransport transport) + { + ArgumentNullException.ThrowIfNull(options); + options.Validate(); + + Options = options; + _transport = transport ?? throw new ArgumentNullException(nameof(transport)); + _safeUnaryRetryPipeline = MxGatewayClientRetryPolicy.Create( + options.Retry, + options.LoggerFactory?.CreateLogger()); + _channel = null; + } + + private GalaxyRepositoryClient( + GrpcChannel channel, + IGalaxyRepositoryClientTransport transport) + { + _channel = channel; + _transport = transport; + Options = transport.Options; + _safeUnaryRetryPipeline = MxGatewayClientRetryPolicy.Create( + Options.Retry, + Options.LoggerFactory?.CreateLogger()); + } + + public MxGatewayClientOptions Options { get; } + + public GalaxyRepository.GalaxyRepositoryClient RawClient => + _transport.RawClient + ?? throw new InvalidOperationException("The raw generated gRPC client is not available for this client instance."); + + public static GalaxyRepositoryClient Create(MxGatewayClientOptions options) + { + ArgumentNullException.ThrowIfNull(options); + options.Validate(); + + HttpMessageHandler handler = CreateHttpHandler(options); + var channel = GrpcChannel.ForAddress( + options.Endpoint, + new GrpcChannelOptions + { + HttpHandler = handler, + LoggerFactory = options.LoggerFactory, + }); + + return new GalaxyRepositoryClient( + channel, + new GrpcGalaxyRepositoryClientTransport( + options, + new GalaxyRepository.GalaxyRepositoryClient(channel))); + } + + /// + /// Probes the Galaxy Repository database connection. Returns true when the + /// gateway can reach the configured ZB SQL Server. + /// + public async Task TestConnectionAsync(CancellationToken cancellationToken = default) + { + TestConnectionReply reply = await TestConnectionRawAsync( + new TestConnectionRequest(), + cancellationToken) + .ConfigureAwait(false); + + return reply.Ok; + } + + public Task TestConnectionRawAsync( + TestConnectionRequest request, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + ThrowIfDisposed(); + + return ExecuteSafeUnaryAsync( + token => _transport.TestConnectionAsync(request, CreateCallOptions(token)), + cancellationToken); + } + + /// + /// Returns the timestamp of the most recent Galaxy deployment, or + /// when no deployment has been recorded. + /// + public async Task GetLastDeployTimeAsync(CancellationToken cancellationToken = default) + { + GetLastDeployTimeReply reply = await GetLastDeployTimeRawAsync( + new GetLastDeployTimeRequest(), + cancellationToken) + .ConfigureAwait(false); + + if (!reply.Present || reply.TimeOfLastDeploy is null) + { + return null; + } + + return reply.TimeOfLastDeploy.ToDateTime(); + } + + public Task GetLastDeployTimeRawAsync( + GetLastDeployTimeRequest request, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + ThrowIfDisposed(); + + return ExecuteSafeUnaryAsync( + token => _transport.GetLastDeployTimeAsync(request, CreateCallOptions(token)), + cancellationToken); + } + + /// + /// Enumerates the deployed Galaxy object hierarchy. Each + /// includes its dynamic attributes so callers can determine which tag references + /// they may subscribe to via the MxAccessGateway service. + /// + public async Task> DiscoverHierarchyAsync(CancellationToken cancellationToken = default) + { + DiscoverHierarchyReply reply = await DiscoverHierarchyRawAsync( + new DiscoverHierarchyRequest(), + cancellationToken) + .ConfigureAwait(false); + + return reply.Objects; + } + + public Task DiscoverHierarchyRawAsync( + DiscoverHierarchyRequest request, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + ThrowIfDisposed(); + + return ExecuteSafeUnaryAsync( + token => _transport.DiscoverHierarchyAsync(request, CreateCallOptions(token)), + cancellationToken); + } + + /// + /// Subscribes to Galaxy deploy events. The server emits a bootstrap event with the + /// current state on subscribe so callers can prime their cache, then emits one event + /// per new time_of_last_deploy. Pass to + /// suppress the bootstrap when the caller already holds the current deploy time. + /// + /// + /// Streaming RPCs are not wrapped by the unary safe-read retry pipeline. If the + /// stream is interrupted the caller must reopen it; the server does not guarantee + /// at-least-once delivery beyond the per-subscriber buffer (gaps in + /// indicate dropped events). + /// + public IAsyncEnumerable WatchDeployEventsAsync( + DateTimeOffset? lastSeenDeployTime = null, + CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + + WatchDeployEventsRequest request = new(); + if (lastSeenDeployTime is { } seen) + { + request.LastSeenDeployTime = Timestamp.FromDateTimeOffset(seen); + } + + return WatchDeployEventsRawAsync(request, cancellationToken); + } + + public IAsyncEnumerable WatchDeployEventsRawAsync( + WatchDeployEventsRequest request, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + ThrowIfDisposed(); + + return WatchDeployEventsCoreAsync(request, cancellationToken); + } + + private async IAsyncEnumerable WatchDeployEventsCoreAsync( + WatchDeployEventsRequest request, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (DeployEvent deployEvent in _transport + .WatchDeployEventsAsync(request, CreateStreamCallOptions(cancellationToken)) + .WithCancellation(cancellationToken) + .ConfigureAwait(false)) + { + yield return deployEvent; + } + } + + public ValueTask DisposeAsync() + { + if (_disposed) + { + return ValueTask.CompletedTask; + } + + _disposed = true; + _channel?.Dispose(); + return ValueTask.CompletedTask; + } + + internal CallOptions CreateCallOptions(CancellationToken cancellationToken) + { + return CreateCallOptions(cancellationToken, Options.DefaultCallTimeout); + } + + internal CallOptions CreateStreamCallOptions(CancellationToken cancellationToken) + { + return CreateCallOptions(cancellationToken, Options.StreamTimeout); + } + + internal CallOptions CreateCallOptions( + CancellationToken cancellationToken, + TimeSpan? timeout) + { + Metadata headers = new() + { + { "authorization", $"Bearer {Options.ApiKey}" }, + }; + + return new CallOptions( + headers, + timeout is null ? null : DateTime.UtcNow.Add(timeout.Value), + cancellationToken); + } + + private async Task ExecuteSafeUnaryAsync( + Func> call, + CancellationToken cancellationToken) + { + using CancellationTokenSource timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeout.CancelAfter(Options.DefaultCallTimeout); + + return await _safeUnaryRetryPipeline.ExecuteAsync( + async token => await call(token).ConfigureAwait(false), + timeout.Token) + .ConfigureAwait(false); + } + + private static HttpMessageHandler CreateHttpHandler(MxGatewayClientOptions options) + { + SocketsHttpHandler handler = new() + { + ConnectTimeout = options.ConnectTimeout, + }; + + if (options.UseTls) + { + handler.SslOptions = new SslClientAuthenticationOptions(); + if (!string.IsNullOrWhiteSpace(options.ServerNameOverride)) + { + handler.SslOptions.TargetHost = options.ServerNameOverride; + } + + if (!string.IsNullOrWhiteSpace(options.CaCertificatePath)) + { + X509Certificate2 trustedRoot = X509CertificateLoader.LoadCertificateFromFile(options.CaCertificatePath); + handler.SslOptions.RemoteCertificateValidationCallback = (_, certificate, chain, errors) => + { + if (certificate is null) + { + return false; + } + + using X509Chain customChain = new(); + customChain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + customChain.ChainPolicy.CustomTrustStore.Add(trustedRoot); + customChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + customChain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag; + X509Certificate2 certificateToValidate = certificate as X509Certificate2 + ?? X509CertificateLoader.LoadCertificate(certificate.Export(X509ContentType.Cert)); + return customChain.Build(certificateToValidate); + }; + } + } + + return handler; + } + + private void ThrowIfDisposed() + { + ObjectDisposedException.ThrowIf(_disposed, this); + } +} diff --git a/clients/dotnet/MxGateway.Client/GrpcGalaxyRepositoryClientTransport.cs b/clients/dotnet/MxGateway.Client/GrpcGalaxyRepositoryClientTransport.cs new file mode 100644 index 0000000..67931b5 --- /dev/null +++ b/clients/dotnet/MxGateway.Client/GrpcGalaxyRepositoryClientTransport.cs @@ -0,0 +1,127 @@ +using Grpc.Core; +using MxGateway.Contracts.Proto.Galaxy; + +namespace MxGateway.Client; + +internal sealed class GrpcGalaxyRepositoryClientTransport( + MxGatewayClientOptions options, + GalaxyRepository.GalaxyRepositoryClient rawClient) : IGalaxyRepositoryClientTransport +{ + public MxGatewayClientOptions Options { get; } = options; + + public GalaxyRepository.GalaxyRepositoryClient RawClient { get; } = rawClient; + + GalaxyRepository.GalaxyRepositoryClient? IGalaxyRepositoryClientTransport.RawClient => RawClient; + + public async Task TestConnectionAsync( + TestConnectionRequest request, + CallOptions callOptions) + { + try + { + return await RawClient.TestConnectionAsync(request, callOptions) + .ResponseAsync + .ConfigureAwait(false); + } + catch (RpcException exception) + { + throw MapRpcException(exception, callOptions.CancellationToken); + } + } + + public async Task GetLastDeployTimeAsync( + GetLastDeployTimeRequest request, + CallOptions callOptions) + { + try + { + return await RawClient.GetLastDeployTimeAsync(request, callOptions) + .ResponseAsync + .ConfigureAwait(false); + } + catch (RpcException exception) + { + throw MapRpcException(exception, callOptions.CancellationToken); + } + } + + public async Task DiscoverHierarchyAsync( + DiscoverHierarchyRequest request, + CallOptions callOptions) + { + try + { + return await RawClient.DiscoverHierarchyAsync(request, callOptions) + .ResponseAsync + .ConfigureAwait(false); + } + catch (RpcException exception) + { + throw MapRpcException(exception, callOptions.CancellationToken); + } + } + + public async IAsyncEnumerable WatchDeployEventsAsync( + WatchDeployEventsRequest request, + CallOptions callOptions, + [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) + { + CancellationToken effectiveCancellationToken = cancellationToken.CanBeCanceled + ? cancellationToken + : callOptions.CancellationToken; + + using AsyncServerStreamingCall call = RawClient.WatchDeployEvents(request, callOptions); + + IAsyncStreamReader responseStream = call.ResponseStream; + while (true) + { + DeployEvent? deployEvent; + try + { + if (!await responseStream.MoveNext(effectiveCancellationToken).ConfigureAwait(false)) + { + break; + } + + deployEvent = responseStream.Current; + } + catch (RpcException exception) + { + throw MapRpcException(exception, effectiveCancellationToken); + } + + yield return deployEvent; + } + } + + IAsyncEnumerable IGalaxyRepositoryClientTransport.WatchDeployEventsAsync( + WatchDeployEventsRequest request, + CallOptions callOptions) + { + return WatchDeployEventsAsync(request, callOptions); + } + + private static Exception MapRpcException( + RpcException exception, + CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested || exception.StatusCode == StatusCode.Cancelled) + { + return new OperationCanceledException( + exception.Status.Detail, + exception, + cancellationToken); + } + + return exception.StatusCode switch + { + StatusCode.Unauthenticated => new MxGatewayAuthenticationException( + exception.Status.Detail, + innerException: exception), + StatusCode.PermissionDenied => new MxGatewayAuthorizationException( + exception.Status.Detail, + innerException: exception), + _ => new MxGatewayException(exception.Status.Detail, exception), + }; + } +} diff --git a/clients/dotnet/MxGateway.Client/IGalaxyRepositoryClientTransport.cs b/clients/dotnet/MxGateway.Client/IGalaxyRepositoryClientTransport.cs new file mode 100644 index 0000000..de55692 --- /dev/null +++ b/clients/dotnet/MxGateway.Client/IGalaxyRepositoryClientTransport.cs @@ -0,0 +1,27 @@ +using Grpc.Core; +using MxGateway.Contracts.Proto.Galaxy; + +namespace MxGateway.Client; + +internal interface IGalaxyRepositoryClientTransport +{ + MxGatewayClientOptions Options { get; } + + GalaxyRepository.GalaxyRepositoryClient? RawClient { get; } + + Task TestConnectionAsync( + TestConnectionRequest request, + CallOptions callOptions); + + Task GetLastDeployTimeAsync( + GetLastDeployTimeRequest request, + CallOptions callOptions); + + Task DiscoverHierarchyAsync( + DiscoverHierarchyRequest request, + CallOptions callOptions); + + IAsyncEnumerable WatchDeployEventsAsync( + WatchDeployEventsRequest request, + CallOptions callOptions); +} diff --git a/clients/dotnet/README.md b/clients/dotnet/README.md index 223746b..60f4915 100644 --- a/clients/dotnet/README.md +++ b/clients/dotnet/README.md @@ -133,6 +133,82 @@ optionally writes a value when `--type` and `--value` are supplied, reads a bounded event stream, and closes the session in a `finally` block. CLI error output redacts API keys supplied through `--api-key`. +## Galaxy Repository Browse + +`GalaxyRepositoryClient` is a separate read-only wrapper around the +`GalaxyRepository` gRPC service exposed by the same gateway. It shares the API +key auth interceptor with `MxGatewayClient` and requires the `metadata:read` +scope server-side. Use it to probe the ZB SQL connection, watch +`time_of_last_deploy` for redeployments, and enumerate the deployed Galaxy +object hierarchy plus each object's dynamic attributes. + +```csharp +await using GalaxyRepositoryClient repository = GalaxyRepositoryClient.Create( + new MxGatewayClientOptions + { + Endpoint = new Uri("http://localhost:5000"), + ApiKey = apiKey, + }); + +bool ok = await repository.TestConnectionAsync(); +DateTime? lastDeploy = await repository.GetLastDeployTimeAsync(); + +IReadOnlyList objects = await repository.DiscoverHierarchyAsync(); +foreach (GalaxyObject galaxyObject in objects) +{ + Console.WriteLine($"{galaxyObject.TagName} ({galaxyObject.ContainedName})"); + foreach (GalaxyAttribute attribute in galaxyObject.Attributes) + { + Console.WriteLine($" {attribute.AttributeName} -> {attribute.FullTagReference}"); + } +} +``` + +The CLI exposes the same operations: + +```powershell +dotnet run --project clients/dotnet/MxGateway.Client.Cli -- galaxy-test-connection --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json +dotnet run --project clients/dotnet/MxGateway.Client.Cli -- galaxy-last-deploy --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json +dotnet run --project clients/dotnet/MxGateway.Client.Cli -- galaxy-discover --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY +``` + +### Watching deploy events + +`WatchDeployEventsAsync` opens the `WatchDeployEvents` server-streaming RPC. The +server emits a bootstrap event with the current state on subscribe, then one +event per new `time_of_last_deploy`. Pass a `lastSeenDeployTime` to suppress the +bootstrap when the caller already holds the current deploy time. Use the +monotonic `Sequence` field to detect dropped events: gaps mean the +per-subscriber server-side buffer overflowed and the caller should reconcile. + +Streaming RPCs are not wrapped by the unary safe-read retry pipeline. The +caller is responsible for reopening the stream on transient failures. + +```csharp +await using GalaxyRepositoryClient repository = GalaxyRepositoryClient.Create(options); + +DateTimeOffset? lastSeen = null; +await foreach (DeployEvent evt in repository.WatchDeployEventsAsync( + lastSeen, + cancellationToken)) +{ + Console.WriteLine( + $"seq={evt.Sequence} objects={evt.ObjectCount} attributes={evt.AttributeCount}"); + if (evt.TimeOfLastDeployPresent && evt.TimeOfLastDeploy is not null) + { + lastSeen = evt.TimeOfLastDeploy.ToDateTimeOffset(); + } +} +``` + +The CLI counterpart streams events until Ctrl+C (or `--max-events`): + +```powershell +dotnet run --project clients/dotnet/MxGateway.Client.Cli -- galaxy-watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY +dotnet run --project clients/dotnet/MxGateway.Client.Cli -- galaxy-watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --last-seen-deploy-time 2026-04-28T14:30:00Z --json +dotnet run --project clients/dotnet/MxGateway.Client.Cli -- galaxy-watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --max-events 5 --json +``` + Use TLS options for a secured gateway: ```powershell diff --git a/clients/go/README.md b/clients/go/README.md index 47ca554..71cb1ac 100644 --- a/clients/go/README.md +++ b/clients/go/README.md @@ -84,6 +84,87 @@ goroutine cleanup. Raw protobuf messages remain available through the `errors.As` for `GatewayError`, `CommandError`, and `MxAccessError`; command errors preserve the raw reply. +## Galaxy Repository browse + +The `GalaxyRepository` service (proto package `galaxy_repository.v1`) is a +read-only metadata-only browse over the AVEVA System Platform Galaxy +Repository. It uses the same API-key authentication as the MXAccess Gateway +and requires the `metadata:read` scope. Use `mxgateway.DialGalaxy` to obtain a +`*GalaxyClient` that mirrors the connection-management conventions of +`Client`: + +```go +galaxy, err := mxgateway.DialGalaxy(ctx, mxgateway.Options{ + Endpoint: "localhost:5000", + APIKey: os.Getenv("MXGATEWAY_API_KEY"), + Plaintext: true, +}) +if err != nil { + return err +} +defer galaxy.Close() + +ok, err := galaxy.TestConnection(ctx) +deployTime, present, err := galaxy.GetLastDeployTime(ctx) +objects, err := galaxy.DiscoverHierarchy(ctx) +``` + +`GetLastDeployTime` returns `(time.Time{}, false, nil)` when the server +reports `present=false` (no deploy recorded). `DiscoverHierarchy` returns +the generated `*GalaxyObject` slice with each object's dynamic attributes +populated for direct contract access. + +### Watching deploy events + +`WatchDeployEvents` opens a server-streaming subscription. The server emits a +bootstrap event with the current Galaxy state immediately on subscribe, then +one `DeployEvent` per new deploy. `Sequence` is monotonic per server start; +gaps signal dropped events. Pass a non-nil `lastSeenDeployTime` to suppress the +bootstrap event when resuming from a known checkpoint: + +```go +streamCtx, cancel := context.WithCancel(ctx) +defer cancel() + +events, errs, err := galaxy.WatchDeployEvents(streamCtx, nil) +if err != nil { + return err +} + +for { + select { + case ev, ok := <-events: + if !ok { + return nil // stream completed (server EOF or ctx cancelled) + } + log.Printf("seq=%d objects=%d attrs=%d", + ev.GetSequence(), ev.GetObjectCount(), ev.GetAttributeCount()) + case streamErr := <-errs: + if streamErr != nil { + return streamErr // *GatewayError + } + case <-ctx.Done(): + return ctx.Err() + } +} +``` + +Cancel the supplied context to tear down the stream cleanly. Both channels +close after EOF, cancellation, or a terminal error; surfaced errors are wrapped +in `*GatewayError`. + +The CLI exposes the same RPC via `galaxy-watch`: + +```powershell +go run ./cmd/mxgw-go galaxy-watch -plaintext +go run ./cmd/mxgw-go galaxy-watch -plaintext -json +go run ./cmd/mxgw-go galaxy-watch -plaintext -last-seen-deploy-time 2026-04-28T10:00:00Z +go run ./cmd/mxgw-go galaxy-watch -plaintext -limit 5 +``` + +The command runs until Ctrl+C (or the optional `-limit` is reached) and prints +one line per event in text mode or one JSON object per event with `-json`. + ## CLI The `mxgw-go` CLI emits JSON with redacted API keys for commands that connect to @@ -98,6 +179,10 @@ go run ./cmd/mxgw-go advise -session-id -server-handle 1 -item-handle 1 -pl go run ./cmd/mxgw-go write -session-id -server-handle 1 -item-handle 1 -type int32 -value 123 -plaintext -json go run ./cmd/mxgw-go stream-events -session-id -plaintext -json go run ./cmd/mxgw-go smoke -item Area001.Tag.Value -plaintext -json +go run ./cmd/mxgw-go galaxy-test-connection -plaintext -json +go run ./cmd/mxgw-go galaxy-last-deploy -plaintext -json +go run ./cmd/mxgw-go galaxy-discover -plaintext -json +go run ./cmd/mxgw-go galaxy-watch -plaintext -json ``` Use `-api-key-env MXGATEWAY_API_KEY` or `-api-key ` when authentication is diff --git a/clients/go/cmd/mxgw-go/main.go b/clients/go/cmd/mxgw-go/main.go index b8555c0..e2f1173 100644 --- a/clients/go/cmd/mxgw-go/main.go +++ b/clients/go/cmd/mxgw-go/main.go @@ -8,8 +8,10 @@ import ( "fmt" "io" "os" + "os/signal" "strconv" "strings" + "syscall" "time" "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/mxgateway" @@ -88,6 +90,14 @@ func runWithIO(ctx context.Context, args []string, stdout, stderr io.Writer) err return runStreamEvents(ctx, args[1:], stdout, stderr) case "smoke": return runSmoke(ctx, args[1:], stdout, stderr) + case "galaxy-test-connection": + return runGalaxyTestConnection(ctx, args[1:], stdout, stderr) + case "galaxy-last-deploy": + return runGalaxyLastDeploy(ctx, args[1:], stdout, stderr) + case "galaxy-discover": + return runGalaxyDiscover(ctx, args[1:], stdout, stderr) + case "galaxy-watch": + return runGalaxyWatch(ctx, args[1:], stdout, stderr) default: writeUsage(stderr) return fmt.Errorf("unknown command %q", args[0]) @@ -651,5 +661,226 @@ type protojsonMessage interface { } func writeUsage(writer io.Writer) { - fmt.Fprintln(writer, "usage: mxgw-go ") + fmt.Fprintln(writer, "usage: mxgw-go ") +} + +func dialGalaxyForCommand(ctx context.Context, common *commonOptions) (*mxgateway.GalaxyClient, commonOptions, error) { + options, err := common.resolved() + if err != nil { + return nil, options, err + } + + client, err := mxgateway.DialGalaxy(ctx, mxgateway.Options{ + Endpoint: options.Endpoint, + APIKey: options.apiKeyValue, + Plaintext: options.Plaintext, + CACertFile: options.CACertFile, + ServerNameOverride: options.ServerName, + CallTimeout: options.timeout, + }) + return client, options, err +} + +func runGalaxyTestConnection(ctx context.Context, args []string, stdout, stderr io.Writer) error { + flags := flag.NewFlagSet("galaxy-test-connection", flag.ContinueOnError) + flags.SetOutput(stderr) + common := bindCommonFlags(flags) + jsonOutput := flags.Bool("json", false, "write JSON output") + + if err := flags.Parse(args); err != nil { + return err + } + + client, options, err := dialGalaxyForCommand(ctx, common) + if err != nil { + return err + } + defer client.Close() + + ok, err := client.TestConnection(ctx) + if err != nil { + return err + } + if *jsonOutput { + return writeJSON(stdout, map[string]any{ + "command": "galaxy-test-connection", + "options": options, + "ok": ok, + }) + } + fmt.Fprintln(stdout, ok) + return nil +} + +func runGalaxyLastDeploy(ctx context.Context, args []string, stdout, stderr io.Writer) error { + flags := flag.NewFlagSet("galaxy-last-deploy", flag.ContinueOnError) + flags.SetOutput(stderr) + common := bindCommonFlags(flags) + jsonOutput := flags.Bool("json", false, "write JSON output") + + if err := flags.Parse(args); err != nil { + return err + } + + client, options, err := dialGalaxyForCommand(ctx, common) + if err != nil { + return err + } + defer client.Close() + + deployTime, present, err := client.GetLastDeployTime(ctx) + if err != nil { + return err + } + if *jsonOutput { + payload := map[string]any{ + "command": "galaxy-last-deploy", + "options": options, + "present": present, + } + if present { + payload["timeOfLastDeploy"] = deployTime.UTC().Format(time.RFC3339Nano) + } + return writeJSON(stdout, payload) + } + if !present { + fmt.Fprintln(stdout, "absent") + return nil + } + fmt.Fprintln(stdout, deployTime.UTC().Format(time.RFC3339Nano)) + return nil +} + +func runGalaxyDiscover(ctx context.Context, args []string, stdout, stderr io.Writer) error { + flags := flag.NewFlagSet("galaxy-discover", flag.ContinueOnError) + flags.SetOutput(stderr) + common := bindCommonFlags(flags) + jsonOutput := flags.Bool("json", false, "write JSON output") + + if err := flags.Parse(args); err != nil { + return err + } + + client, options, err := dialGalaxyForCommand(ctx, common) + if err != nil { + return err + } + defer client.Close() + + objects, err := client.DiscoverHierarchy(ctx) + if err != nil { + return err + } + if *jsonOutput { + marshaled := make([]json.RawMessage, 0, len(objects)) + for _, obj := range objects { + marshaled = append(marshaled, mustMarshalProto(obj)) + } + return writeJSON(stdout, map[string]any{ + "command": "galaxy-discover", + "options": options, + "objects": marshaled, + }) + } + for _, obj := range objects { + fmt.Fprintf(stdout, "%d\t%s\t%s\t(attrs=%d)\n", obj.GetGobjectId(), obj.GetTagName(), obj.GetContainedName(), len(obj.GetAttributes())) + } + return nil +} + +func runGalaxyWatch(ctx context.Context, args []string, stdout, stderr io.Writer) error { + flags := flag.NewFlagSet("galaxy-watch", flag.ContinueOnError) + flags.SetOutput(stderr) + common := bindCommonFlags(flags) + jsonOutput := flags.Bool("json", false, "write JSON output") + lastSeen := flags.String("last-seen-deploy-time", "", "RFC3339 timestamp; when set, suppresses the bootstrap event") + limit := flags.Int("limit", 0, "maximum events to read; 0 means unbounded (Ctrl+C to stop)") + + if err := flags.Parse(args); err != nil { + return err + } + + var lastSeenPtr *time.Time + if *lastSeen != "" { + parsed, err := time.Parse(time.RFC3339, *lastSeen) + if err != nil { + return fmt.Errorf("invalid -last-seen-deploy-time: %w", err) + } + lastSeenPtr = &parsed + } + + client, _, err := dialGalaxyForCommand(ctx, common) + if err != nil { + return err + } + defer client.Close() + + signalCtx, stopSignals := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) + defer stopSignals() + + streamCtx, cancelStream := context.WithCancel(signalCtx) + defer cancelStream() + + events, errs, err := client.WatchDeployEvents(streamCtx, lastSeenPtr) + if err != nil { + return err + } + + count := 0 + for { + select { + case event, ok := <-events: + if !ok { + // Drain any terminal error before returning. + if streamErr, errOk := <-errs; errOk && streamErr != nil { + return streamErr + } + return nil + } + if *jsonOutput { + fmt.Fprintln(stdout, string(mustMarshalProto(event))) + } else { + fmt.Fprintln(stdout, formatDeployEvent(event)) + } + count++ + if *limit > 0 && count >= *limit { + cancelStream() + return nil + } + case streamErr, ok := <-errs: + if !ok { + return nil + } + if streamErr != nil { + return streamErr + } + case <-signalCtx.Done(): + cancelStream() + // Allow goroutine to drain. + for range events { + } + return nil + } + } +} + +func formatDeployEvent(event *mxgateway.DeployEvent) string { + observed := "" + if ts := event.GetObservedAt(); ts != nil { + observed = ts.AsTime().UTC().Format(time.RFC3339Nano) + } + deploy := "absent" + if event.GetTimeOfLastDeployPresent() { + if ts := event.GetTimeOfLastDeploy(); ts != nil { + deploy = ts.AsTime().UTC().Format(time.RFC3339Nano) + } + } + return fmt.Sprintf( + "seq=%d observed=%s deploy=%s objects=%d attributes=%d", + event.GetSequence(), + observed, + deploy, + event.GetObjectCount(), + event.GetAttributeCount(), + ) } diff --git a/clients/go/generate-proto.ps1 b/clients/go/generate-proto.ps1 index 076e291..a4d1d80 100644 --- a/clients/go/generate-proto.ps1 +++ b/clients/go/generate-proto.ps1 @@ -30,13 +30,17 @@ $env:Path = "$goPluginPath;$env:Path" --go_opt=paths=source_relative ` "--go_opt=Mmxaccess_gateway.proto=$modulePath;generated" ` "--go_opt=Mmxaccess_worker.proto=$modulePath;generated" ` + "--go_opt=Mgalaxy_repository.proto=$modulePath;generated" ` mxaccess_gateway.proto ` - mxaccess_worker.proto + mxaccess_worker.proto ` + galaxy_repository.proto & $protoc ` --proto_path=$protoRoot ` --go-grpc_out=$outputRoot ` --go-grpc_opt=paths=source_relative ` "--go-grpc_opt=Mmxaccess_gateway.proto=$modulePath;generated" ` - mxaccess_gateway.proto + "--go-grpc_opt=Mgalaxy_repository.proto=$modulePath;generated" ` + mxaccess_gateway.proto ` + galaxy_repository.proto diff --git a/clients/go/internal/generated/galaxy_repository.pb.go b/clients/go/internal/generated/galaxy_repository.pb.go new file mode 100644 index 0000000..05776aa --- /dev/null +++ b/clients/go/internal/generated/galaxy_repository.pb.go @@ -0,0 +1,778 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v7.34.1 +// source: galaxy_repository.proto + +package generated + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type TestConnectionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TestConnectionRequest) Reset() { + *x = TestConnectionRequest{} + mi := &file_galaxy_repository_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TestConnectionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TestConnectionRequest) ProtoMessage() {} + +func (x *TestConnectionRequest) ProtoReflect() protoreflect.Message { + mi := &file_galaxy_repository_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TestConnectionRequest.ProtoReflect.Descriptor instead. +func (*TestConnectionRequest) Descriptor() ([]byte, []int) { + return file_galaxy_repository_proto_rawDescGZIP(), []int{0} +} + +type TestConnectionReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TestConnectionReply) Reset() { + *x = TestConnectionReply{} + mi := &file_galaxy_repository_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TestConnectionReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TestConnectionReply) ProtoMessage() {} + +func (x *TestConnectionReply) ProtoReflect() protoreflect.Message { + mi := &file_galaxy_repository_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TestConnectionReply.ProtoReflect.Descriptor instead. +func (*TestConnectionReply) Descriptor() ([]byte, []int) { + return file_galaxy_repository_proto_rawDescGZIP(), []int{1} +} + +func (x *TestConnectionReply) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +type GetLastDeployTimeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetLastDeployTimeRequest) Reset() { + *x = GetLastDeployTimeRequest{} + mi := &file_galaxy_repository_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetLastDeployTimeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetLastDeployTimeRequest) ProtoMessage() {} + +func (x *GetLastDeployTimeRequest) ProtoReflect() protoreflect.Message { + mi := &file_galaxy_repository_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetLastDeployTimeRequest.ProtoReflect.Descriptor instead. +func (*GetLastDeployTimeRequest) Descriptor() ([]byte, []int) { + return file_galaxy_repository_proto_rawDescGZIP(), []int{2} +} + +type GetLastDeployTimeReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Present bool `protobuf:"varint,1,opt,name=present,proto3" json:"present,omitempty"` + TimeOfLastDeploy *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=time_of_last_deploy,json=timeOfLastDeploy,proto3" json:"time_of_last_deploy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetLastDeployTimeReply) Reset() { + *x = GetLastDeployTimeReply{} + mi := &file_galaxy_repository_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetLastDeployTimeReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetLastDeployTimeReply) ProtoMessage() {} + +func (x *GetLastDeployTimeReply) ProtoReflect() protoreflect.Message { + mi := &file_galaxy_repository_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetLastDeployTimeReply.ProtoReflect.Descriptor instead. +func (*GetLastDeployTimeReply) Descriptor() ([]byte, []int) { + return file_galaxy_repository_proto_rawDescGZIP(), []int{3} +} + +func (x *GetLastDeployTimeReply) GetPresent() bool { + if x != nil { + return x.Present + } + return false +} + +func (x *GetLastDeployTimeReply) GetTimeOfLastDeploy() *timestamppb.Timestamp { + if x != nil { + return x.TimeOfLastDeploy + } + return nil +} + +type DiscoverHierarchyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DiscoverHierarchyRequest) Reset() { + *x = DiscoverHierarchyRequest{} + mi := &file_galaxy_repository_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DiscoverHierarchyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DiscoverHierarchyRequest) ProtoMessage() {} + +func (x *DiscoverHierarchyRequest) ProtoReflect() protoreflect.Message { + mi := &file_galaxy_repository_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DiscoverHierarchyRequest.ProtoReflect.Descriptor instead. +func (*DiscoverHierarchyRequest) Descriptor() ([]byte, []int) { + return file_galaxy_repository_proto_rawDescGZIP(), []int{4} +} + +type DiscoverHierarchyReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Objects []*GalaxyObject `protobuf:"bytes,1,rep,name=objects,proto3" json:"objects,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DiscoverHierarchyReply) Reset() { + *x = DiscoverHierarchyReply{} + mi := &file_galaxy_repository_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DiscoverHierarchyReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DiscoverHierarchyReply) ProtoMessage() {} + +func (x *DiscoverHierarchyReply) ProtoReflect() protoreflect.Message { + mi := &file_galaxy_repository_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DiscoverHierarchyReply.ProtoReflect.Descriptor instead. +func (*DiscoverHierarchyReply) Descriptor() ([]byte, []int) { + return file_galaxy_repository_proto_rawDescGZIP(), []int{5} +} + +func (x *DiscoverHierarchyReply) GetObjects() []*GalaxyObject { + if x != nil { + return x.Objects + } + return nil +} + +type WatchDeployEventsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional. When set, the bootstrap event is suppressed if the cached deploy + // time matches this value. Future events are still emitted normally. + LastSeenDeployTime *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=last_seen_deploy_time,json=lastSeenDeployTime,proto3" json:"last_seen_deploy_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WatchDeployEventsRequest) Reset() { + *x = WatchDeployEventsRequest{} + mi := &file_galaxy_repository_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WatchDeployEventsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WatchDeployEventsRequest) ProtoMessage() {} + +func (x *WatchDeployEventsRequest) ProtoReflect() protoreflect.Message { + mi := &file_galaxy_repository_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WatchDeployEventsRequest.ProtoReflect.Descriptor instead. +func (*WatchDeployEventsRequest) Descriptor() ([]byte, []int) { + return file_galaxy_repository_proto_rawDescGZIP(), []int{6} +} + +func (x *WatchDeployEventsRequest) GetLastSeenDeployTime() *timestamppb.Timestamp { + if x != nil { + return x.LastSeenDeployTime + } + return nil +} + +type DeployEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Monotonically increasing per server start. Gaps indicate dropped events. + Sequence uint64 `protobuf:"varint,1,opt,name=sequence,proto3" json:"sequence,omitempty"` + // Server wall-clock when the cache observed the deploy. + ObservedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=observed_at,json=observedAt,proto3" json:"observed_at,omitempty"` + // Galaxy.time_of_last_deploy. Absent only when the Galaxy table reports null. + TimeOfLastDeploy *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=time_of_last_deploy,json=timeOfLastDeploy,proto3" json:"time_of_last_deploy,omitempty"` + TimeOfLastDeployPresent bool `protobuf:"varint,4,opt,name=time_of_last_deploy_present,json=timeOfLastDeployPresent,proto3" json:"time_of_last_deploy_present,omitempty"` + ObjectCount int32 `protobuf:"varint,5,opt,name=object_count,json=objectCount,proto3" json:"object_count,omitempty"` + AttributeCount int32 `protobuf:"varint,6,opt,name=attribute_count,json=attributeCount,proto3" json:"attribute_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeployEvent) Reset() { + *x = DeployEvent{} + mi := &file_galaxy_repository_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeployEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeployEvent) ProtoMessage() {} + +func (x *DeployEvent) ProtoReflect() protoreflect.Message { + mi := &file_galaxy_repository_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeployEvent.ProtoReflect.Descriptor instead. +func (*DeployEvent) Descriptor() ([]byte, []int) { + return file_galaxy_repository_proto_rawDescGZIP(), []int{7} +} + +func (x *DeployEvent) GetSequence() uint64 { + if x != nil { + return x.Sequence + } + return 0 +} + +func (x *DeployEvent) GetObservedAt() *timestamppb.Timestamp { + if x != nil { + return x.ObservedAt + } + return nil +} + +func (x *DeployEvent) GetTimeOfLastDeploy() *timestamppb.Timestamp { + if x != nil { + return x.TimeOfLastDeploy + } + return nil +} + +func (x *DeployEvent) GetTimeOfLastDeployPresent() bool { + if x != nil { + return x.TimeOfLastDeployPresent + } + return false +} + +func (x *DeployEvent) GetObjectCount() int32 { + if x != nil { + return x.ObjectCount + } + return 0 +} + +func (x *DeployEvent) GetAttributeCount() int32 { + if x != nil { + return x.AttributeCount + } + return 0 +} + +type GalaxyObject struct { + state protoimpl.MessageState `protogen:"open.v1"` + GobjectId int32 `protobuf:"varint,1,opt,name=gobject_id,json=gobjectId,proto3" json:"gobject_id,omitempty"` + TagName string `protobuf:"bytes,2,opt,name=tag_name,json=tagName,proto3" json:"tag_name,omitempty"` + ContainedName string `protobuf:"bytes,3,opt,name=contained_name,json=containedName,proto3" json:"contained_name,omitempty"` + BrowseName string `protobuf:"bytes,4,opt,name=browse_name,json=browseName,proto3" json:"browse_name,omitempty"` + ParentGobjectId int32 `protobuf:"varint,5,opt,name=parent_gobject_id,json=parentGobjectId,proto3" json:"parent_gobject_id,omitempty"` + IsArea bool `protobuf:"varint,6,opt,name=is_area,json=isArea,proto3" json:"is_area,omitempty"` + CategoryId int32 `protobuf:"varint,7,opt,name=category_id,json=categoryId,proto3" json:"category_id,omitempty"` + HostedByGobjectId int32 `protobuf:"varint,8,opt,name=hosted_by_gobject_id,json=hostedByGobjectId,proto3" json:"hosted_by_gobject_id,omitempty"` + TemplateChain []string `protobuf:"bytes,9,rep,name=template_chain,json=templateChain,proto3" json:"template_chain,omitempty"` + Attributes []*GalaxyAttribute `protobuf:"bytes,10,rep,name=attributes,proto3" json:"attributes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GalaxyObject) Reset() { + *x = GalaxyObject{} + mi := &file_galaxy_repository_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GalaxyObject) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GalaxyObject) ProtoMessage() {} + +func (x *GalaxyObject) ProtoReflect() protoreflect.Message { + mi := &file_galaxy_repository_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GalaxyObject.ProtoReflect.Descriptor instead. +func (*GalaxyObject) Descriptor() ([]byte, []int) { + return file_galaxy_repository_proto_rawDescGZIP(), []int{8} +} + +func (x *GalaxyObject) GetGobjectId() int32 { + if x != nil { + return x.GobjectId + } + return 0 +} + +func (x *GalaxyObject) GetTagName() string { + if x != nil { + return x.TagName + } + return "" +} + +func (x *GalaxyObject) GetContainedName() string { + if x != nil { + return x.ContainedName + } + return "" +} + +func (x *GalaxyObject) GetBrowseName() string { + if x != nil { + return x.BrowseName + } + return "" +} + +func (x *GalaxyObject) GetParentGobjectId() int32 { + if x != nil { + return x.ParentGobjectId + } + return 0 +} + +func (x *GalaxyObject) GetIsArea() bool { + if x != nil { + return x.IsArea + } + return false +} + +func (x *GalaxyObject) GetCategoryId() int32 { + if x != nil { + return x.CategoryId + } + return 0 +} + +func (x *GalaxyObject) GetHostedByGobjectId() int32 { + if x != nil { + return x.HostedByGobjectId + } + return 0 +} + +func (x *GalaxyObject) GetTemplateChain() []string { + if x != nil { + return x.TemplateChain + } + return nil +} + +func (x *GalaxyObject) GetAttributes() []*GalaxyAttribute { + if x != nil { + return x.Attributes + } + return nil +} + +type GalaxyAttribute struct { + state protoimpl.MessageState `protogen:"open.v1"` + AttributeName string `protobuf:"bytes,1,opt,name=attribute_name,json=attributeName,proto3" json:"attribute_name,omitempty"` + FullTagReference string `protobuf:"bytes,2,opt,name=full_tag_reference,json=fullTagReference,proto3" json:"full_tag_reference,omitempty"` + MxDataType int32 `protobuf:"varint,3,opt,name=mx_data_type,json=mxDataType,proto3" json:"mx_data_type,omitempty"` + DataTypeName string `protobuf:"bytes,4,opt,name=data_type_name,json=dataTypeName,proto3" json:"data_type_name,omitempty"` + IsArray bool `protobuf:"varint,5,opt,name=is_array,json=isArray,proto3" json:"is_array,omitempty"` + ArrayDimension int32 `protobuf:"varint,6,opt,name=array_dimension,json=arrayDimension,proto3" json:"array_dimension,omitempty"` + ArrayDimensionPresent bool `protobuf:"varint,7,opt,name=array_dimension_present,json=arrayDimensionPresent,proto3" json:"array_dimension_present,omitempty"` + MxAttributeCategory int32 `protobuf:"varint,8,opt,name=mx_attribute_category,json=mxAttributeCategory,proto3" json:"mx_attribute_category,omitempty"` + SecurityClassification int32 `protobuf:"varint,9,opt,name=security_classification,json=securityClassification,proto3" json:"security_classification,omitempty"` + IsHistorized bool `protobuf:"varint,10,opt,name=is_historized,json=isHistorized,proto3" json:"is_historized,omitempty"` + IsAlarm bool `protobuf:"varint,11,opt,name=is_alarm,json=isAlarm,proto3" json:"is_alarm,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GalaxyAttribute) Reset() { + *x = GalaxyAttribute{} + mi := &file_galaxy_repository_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GalaxyAttribute) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GalaxyAttribute) ProtoMessage() {} + +func (x *GalaxyAttribute) ProtoReflect() protoreflect.Message { + mi := &file_galaxy_repository_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GalaxyAttribute.ProtoReflect.Descriptor instead. +func (*GalaxyAttribute) Descriptor() ([]byte, []int) { + return file_galaxy_repository_proto_rawDescGZIP(), []int{9} +} + +func (x *GalaxyAttribute) GetAttributeName() string { + if x != nil { + return x.AttributeName + } + return "" +} + +func (x *GalaxyAttribute) GetFullTagReference() string { + if x != nil { + return x.FullTagReference + } + return "" +} + +func (x *GalaxyAttribute) GetMxDataType() int32 { + if x != nil { + return x.MxDataType + } + return 0 +} + +func (x *GalaxyAttribute) GetDataTypeName() string { + if x != nil { + return x.DataTypeName + } + return "" +} + +func (x *GalaxyAttribute) GetIsArray() bool { + if x != nil { + return x.IsArray + } + return false +} + +func (x *GalaxyAttribute) GetArrayDimension() int32 { + if x != nil { + return x.ArrayDimension + } + return 0 +} + +func (x *GalaxyAttribute) GetArrayDimensionPresent() bool { + if x != nil { + return x.ArrayDimensionPresent + } + return false +} + +func (x *GalaxyAttribute) GetMxAttributeCategory() int32 { + if x != nil { + return x.MxAttributeCategory + } + return 0 +} + +func (x *GalaxyAttribute) GetSecurityClassification() int32 { + if x != nil { + return x.SecurityClassification + } + return 0 +} + +func (x *GalaxyAttribute) GetIsHistorized() bool { + if x != nil { + return x.IsHistorized + } + return false +} + +func (x *GalaxyAttribute) GetIsAlarm() bool { + if x != nil { + return x.IsAlarm + } + return false +} + +var File_galaxy_repository_proto protoreflect.FileDescriptor + +const file_galaxy_repository_proto_rawDesc = "" + + "\n" + + "\x17galaxy_repository.proto\x12\x14galaxy_repository.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\x17\n" + + "\x15TestConnectionRequest\"%\n" + + "\x13TestConnectionReply\x12\x0e\n" + + "\x02ok\x18\x01 \x01(\bR\x02ok\"\x1a\n" + + "\x18GetLastDeployTimeRequest\"}\n" + + "\x16GetLastDeployTimeReply\x12\x18\n" + + "\apresent\x18\x01 \x01(\bR\apresent\x12I\n" + + "\x13time_of_last_deploy\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x10timeOfLastDeploy\"\x1a\n" + + "\x18DiscoverHierarchyRequest\"V\n" + + "\x16DiscoverHierarchyReply\x12<\n" + + "\aobjects\x18\x01 \x03(\v2\".galaxy_repository.v1.GalaxyObjectR\aobjects\"i\n" + + "\x18WatchDeployEventsRequest\x12M\n" + + "\x15last_seen_deploy_time\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\x12lastSeenDeployTime\"\xbb\x02\n" + + "\vDeployEvent\x12\x1a\n" + + "\bsequence\x18\x01 \x01(\x04R\bsequence\x12;\n" + + "\vobserved_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "observedAt\x12I\n" + + "\x13time_of_last_deploy\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x10timeOfLastDeploy\x12<\n" + + "\x1btime_of_last_deploy_present\x18\x04 \x01(\bR\x17timeOfLastDeployPresent\x12!\n" + + "\fobject_count\x18\x05 \x01(\x05R\vobjectCount\x12'\n" + + "\x0fattribute_count\x18\x06 \x01(\x05R\x0eattributeCount\"\x95\x03\n" + + "\fGalaxyObject\x12\x1d\n" + + "\n" + + "gobject_id\x18\x01 \x01(\x05R\tgobjectId\x12\x19\n" + + "\btag_name\x18\x02 \x01(\tR\atagName\x12%\n" + + "\x0econtained_name\x18\x03 \x01(\tR\rcontainedName\x12\x1f\n" + + "\vbrowse_name\x18\x04 \x01(\tR\n" + + "browseName\x12*\n" + + "\x11parent_gobject_id\x18\x05 \x01(\x05R\x0fparentGobjectId\x12\x17\n" + + "\ais_area\x18\x06 \x01(\bR\x06isArea\x12\x1f\n" + + "\vcategory_id\x18\a \x01(\x05R\n" + + "categoryId\x12/\n" + + "\x14hosted_by_gobject_id\x18\b \x01(\x05R\x11hostedByGobjectId\x12%\n" + + "\x0etemplate_chain\x18\t \x03(\tR\rtemplateChain\x12E\n" + + "\n" + + "attributes\x18\n" + + " \x03(\v2%.galaxy_repository.v1.GalaxyAttributeR\n" + + "attributes\"\xd7\x03\n" + + "\x0fGalaxyAttribute\x12%\n" + + "\x0eattribute_name\x18\x01 \x01(\tR\rattributeName\x12,\n" + + "\x12full_tag_reference\x18\x02 \x01(\tR\x10fullTagReference\x12 \n" + + "\fmx_data_type\x18\x03 \x01(\x05R\n" + + "mxDataType\x12$\n" + + "\x0edata_type_name\x18\x04 \x01(\tR\fdataTypeName\x12\x19\n" + + "\bis_array\x18\x05 \x01(\bR\aisArray\x12'\n" + + "\x0farray_dimension\x18\x06 \x01(\x05R\x0earrayDimension\x126\n" + + "\x17array_dimension_present\x18\a \x01(\bR\x15arrayDimensionPresent\x122\n" + + "\x15mx_attribute_category\x18\b \x01(\x05R\x13mxAttributeCategory\x127\n" + + "\x17security_classification\x18\t \x01(\x05R\x16securityClassification\x12#\n" + + "\ris_historized\x18\n" + + " \x01(\bR\fisHistorized\x12\x19\n" + + "\bis_alarm\x18\v \x01(\bR\aisAlarm2\xcc\x03\n" + + "\x10GalaxyRepository\x12h\n" + + "\x0eTestConnection\x12+.galaxy_repository.v1.TestConnectionRequest\x1a).galaxy_repository.v1.TestConnectionReply\x12q\n" + + "\x11GetLastDeployTime\x12..galaxy_repository.v1.GetLastDeployTimeRequest\x1a,.galaxy_repository.v1.GetLastDeployTimeReply\x12q\n" + + "\x11DiscoverHierarchy\x12..galaxy_repository.v1.DiscoverHierarchyRequest\x1a,.galaxy_repository.v1.DiscoverHierarchyReply\x12h\n" + + "\x11WatchDeployEvents\x12..galaxy_repository.v1.WatchDeployEventsRequest\x1a!.galaxy_repository.v1.DeployEvent0\x01B#\xaa\x02 MxGateway.Contracts.Proto.Galaxyb\x06proto3" + +var ( + file_galaxy_repository_proto_rawDescOnce sync.Once + file_galaxy_repository_proto_rawDescData []byte +) + +func file_galaxy_repository_proto_rawDescGZIP() []byte { + file_galaxy_repository_proto_rawDescOnce.Do(func() { + file_galaxy_repository_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_galaxy_repository_proto_rawDesc), len(file_galaxy_repository_proto_rawDesc))) + }) + return file_galaxy_repository_proto_rawDescData +} + +var file_galaxy_repository_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_galaxy_repository_proto_goTypes = []any{ + (*TestConnectionRequest)(nil), // 0: galaxy_repository.v1.TestConnectionRequest + (*TestConnectionReply)(nil), // 1: galaxy_repository.v1.TestConnectionReply + (*GetLastDeployTimeRequest)(nil), // 2: galaxy_repository.v1.GetLastDeployTimeRequest + (*GetLastDeployTimeReply)(nil), // 3: galaxy_repository.v1.GetLastDeployTimeReply + (*DiscoverHierarchyRequest)(nil), // 4: galaxy_repository.v1.DiscoverHierarchyRequest + (*DiscoverHierarchyReply)(nil), // 5: galaxy_repository.v1.DiscoverHierarchyReply + (*WatchDeployEventsRequest)(nil), // 6: galaxy_repository.v1.WatchDeployEventsRequest + (*DeployEvent)(nil), // 7: galaxy_repository.v1.DeployEvent + (*GalaxyObject)(nil), // 8: galaxy_repository.v1.GalaxyObject + (*GalaxyAttribute)(nil), // 9: galaxy_repository.v1.GalaxyAttribute + (*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp +} +var file_galaxy_repository_proto_depIdxs = []int32{ + 10, // 0: galaxy_repository.v1.GetLastDeployTimeReply.time_of_last_deploy:type_name -> google.protobuf.Timestamp + 8, // 1: galaxy_repository.v1.DiscoverHierarchyReply.objects:type_name -> galaxy_repository.v1.GalaxyObject + 10, // 2: galaxy_repository.v1.WatchDeployEventsRequest.last_seen_deploy_time:type_name -> google.protobuf.Timestamp + 10, // 3: galaxy_repository.v1.DeployEvent.observed_at:type_name -> google.protobuf.Timestamp + 10, // 4: galaxy_repository.v1.DeployEvent.time_of_last_deploy:type_name -> google.protobuf.Timestamp + 9, // 5: galaxy_repository.v1.GalaxyObject.attributes:type_name -> galaxy_repository.v1.GalaxyAttribute + 0, // 6: galaxy_repository.v1.GalaxyRepository.TestConnection:input_type -> galaxy_repository.v1.TestConnectionRequest + 2, // 7: galaxy_repository.v1.GalaxyRepository.GetLastDeployTime:input_type -> galaxy_repository.v1.GetLastDeployTimeRequest + 4, // 8: galaxy_repository.v1.GalaxyRepository.DiscoverHierarchy:input_type -> galaxy_repository.v1.DiscoverHierarchyRequest + 6, // 9: galaxy_repository.v1.GalaxyRepository.WatchDeployEvents:input_type -> galaxy_repository.v1.WatchDeployEventsRequest + 1, // 10: galaxy_repository.v1.GalaxyRepository.TestConnection:output_type -> galaxy_repository.v1.TestConnectionReply + 3, // 11: galaxy_repository.v1.GalaxyRepository.GetLastDeployTime:output_type -> galaxy_repository.v1.GetLastDeployTimeReply + 5, // 12: galaxy_repository.v1.GalaxyRepository.DiscoverHierarchy:output_type -> galaxy_repository.v1.DiscoverHierarchyReply + 7, // 13: galaxy_repository.v1.GalaxyRepository.WatchDeployEvents:output_type -> galaxy_repository.v1.DeployEvent + 10, // [10:14] is the sub-list for method output_type + 6, // [6:10] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_galaxy_repository_proto_init() } +func file_galaxy_repository_proto_init() { + if File_galaxy_repository_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_galaxy_repository_proto_rawDesc), len(file_galaxy_repository_proto_rawDesc)), + NumEnums: 0, + NumMessages: 10, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_galaxy_repository_proto_goTypes, + DependencyIndexes: file_galaxy_repository_proto_depIdxs, + MessageInfos: file_galaxy_repository_proto_msgTypes, + }.Build() + File_galaxy_repository_proto = out.File + file_galaxy_repository_proto_goTypes = nil + file_galaxy_repository_proto_depIdxs = nil +} diff --git a/clients/go/internal/generated/galaxy_repository_grpc.pb.go b/clients/go/internal/generated/galaxy_repository_grpc.pb.go new file mode 100644 index 0000000..b2cdcc5 --- /dev/null +++ b/clients/go/internal/generated/galaxy_repository_grpc.pb.go @@ -0,0 +1,261 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc v7.34.1 +// source: galaxy_repository.proto + +package generated + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + GalaxyRepository_TestConnection_FullMethodName = "/galaxy_repository.v1.GalaxyRepository/TestConnection" + GalaxyRepository_GetLastDeployTime_FullMethodName = "/galaxy_repository.v1.GalaxyRepository/GetLastDeployTime" + GalaxyRepository_DiscoverHierarchy_FullMethodName = "/galaxy_repository.v1.GalaxyRepository/DiscoverHierarchy" + GalaxyRepository_WatchDeployEvents_FullMethodName = "/galaxy_repository.v1.GalaxyRepository/WatchDeployEvents" +) + +// GalaxyRepositoryClient is the client API for GalaxyRepository service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// Read-only browse over the AVEVA System Platform Galaxy Repository (ZB SQL +// database). Lets clients enumerate the deployed object hierarchy and each +// object's dynamic attributes so they know what tag references to subscribe +// to via the MxAccessGateway service. +type GalaxyRepositoryClient interface { + TestConnection(ctx context.Context, in *TestConnectionRequest, opts ...grpc.CallOption) (*TestConnectionReply, error) + GetLastDeployTime(ctx context.Context, in *GetLastDeployTimeRequest, opts ...grpc.CallOption) (*GetLastDeployTimeReply, error) + DiscoverHierarchy(ctx context.Context, in *DiscoverHierarchyRequest, opts ...grpc.CallOption) (*DiscoverHierarchyReply, error) + // Server-stream of deploy events. The server emits the current state immediately + // on subscribe (so clients can bootstrap their cache without waiting for the next + // deploy), then emits one event each time the gateway's hierarchy cache observes + // a new galaxy.time_of_last_deploy. The sequence field is monotonically + // increasing per server start; gaps indicate the per-subscriber buffer dropped + // older events because the client was too slow. + WatchDeployEvents(ctx context.Context, in *WatchDeployEventsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[DeployEvent], error) +} + +type galaxyRepositoryClient struct { + cc grpc.ClientConnInterface +} + +func NewGalaxyRepositoryClient(cc grpc.ClientConnInterface) GalaxyRepositoryClient { + return &galaxyRepositoryClient{cc} +} + +func (c *galaxyRepositoryClient) TestConnection(ctx context.Context, in *TestConnectionRequest, opts ...grpc.CallOption) (*TestConnectionReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TestConnectionReply) + err := c.cc.Invoke(ctx, GalaxyRepository_TestConnection_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *galaxyRepositoryClient) GetLastDeployTime(ctx context.Context, in *GetLastDeployTimeRequest, opts ...grpc.CallOption) (*GetLastDeployTimeReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetLastDeployTimeReply) + err := c.cc.Invoke(ctx, GalaxyRepository_GetLastDeployTime_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *galaxyRepositoryClient) DiscoverHierarchy(ctx context.Context, in *DiscoverHierarchyRequest, opts ...grpc.CallOption) (*DiscoverHierarchyReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DiscoverHierarchyReply) + err := c.cc.Invoke(ctx, GalaxyRepository_DiscoverHierarchy_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *galaxyRepositoryClient) WatchDeployEvents(ctx context.Context, in *WatchDeployEventsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[DeployEvent], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &GalaxyRepository_ServiceDesc.Streams[0], GalaxyRepository_WatchDeployEvents_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[WatchDeployEventsRequest, DeployEvent]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type GalaxyRepository_WatchDeployEventsClient = grpc.ServerStreamingClient[DeployEvent] + +// GalaxyRepositoryServer is the server API for GalaxyRepository service. +// All implementations must embed UnimplementedGalaxyRepositoryServer +// for forward compatibility. +// +// Read-only browse over the AVEVA System Platform Galaxy Repository (ZB SQL +// database). Lets clients enumerate the deployed object hierarchy and each +// object's dynamic attributes so they know what tag references to subscribe +// to via the MxAccessGateway service. +type GalaxyRepositoryServer interface { + TestConnection(context.Context, *TestConnectionRequest) (*TestConnectionReply, error) + GetLastDeployTime(context.Context, *GetLastDeployTimeRequest) (*GetLastDeployTimeReply, error) + DiscoverHierarchy(context.Context, *DiscoverHierarchyRequest) (*DiscoverHierarchyReply, error) + // Server-stream of deploy events. The server emits the current state immediately + // on subscribe (so clients can bootstrap their cache without waiting for the next + // deploy), then emits one event each time the gateway's hierarchy cache observes + // a new galaxy.time_of_last_deploy. The sequence field is monotonically + // increasing per server start; gaps indicate the per-subscriber buffer dropped + // older events because the client was too slow. + WatchDeployEvents(*WatchDeployEventsRequest, grpc.ServerStreamingServer[DeployEvent]) error + mustEmbedUnimplementedGalaxyRepositoryServer() +} + +// UnimplementedGalaxyRepositoryServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedGalaxyRepositoryServer struct{} + +func (UnimplementedGalaxyRepositoryServer) TestConnection(context.Context, *TestConnectionRequest) (*TestConnectionReply, error) { + return nil, status.Error(codes.Unimplemented, "method TestConnection not implemented") +} +func (UnimplementedGalaxyRepositoryServer) GetLastDeployTime(context.Context, *GetLastDeployTimeRequest) (*GetLastDeployTimeReply, error) { + return nil, status.Error(codes.Unimplemented, "method GetLastDeployTime not implemented") +} +func (UnimplementedGalaxyRepositoryServer) DiscoverHierarchy(context.Context, *DiscoverHierarchyRequest) (*DiscoverHierarchyReply, error) { + return nil, status.Error(codes.Unimplemented, "method DiscoverHierarchy not implemented") +} +func (UnimplementedGalaxyRepositoryServer) WatchDeployEvents(*WatchDeployEventsRequest, grpc.ServerStreamingServer[DeployEvent]) error { + return status.Error(codes.Unimplemented, "method WatchDeployEvents not implemented") +} +func (UnimplementedGalaxyRepositoryServer) mustEmbedUnimplementedGalaxyRepositoryServer() {} +func (UnimplementedGalaxyRepositoryServer) testEmbeddedByValue() {} + +// UnsafeGalaxyRepositoryServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GalaxyRepositoryServer will +// result in compilation errors. +type UnsafeGalaxyRepositoryServer interface { + mustEmbedUnimplementedGalaxyRepositoryServer() +} + +func RegisterGalaxyRepositoryServer(s grpc.ServiceRegistrar, srv GalaxyRepositoryServer) { + // If the following call panics, it indicates UnimplementedGalaxyRepositoryServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&GalaxyRepository_ServiceDesc, srv) +} + +func _GalaxyRepository_TestConnection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TestConnectionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GalaxyRepositoryServer).TestConnection(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GalaxyRepository_TestConnection_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GalaxyRepositoryServer).TestConnection(ctx, req.(*TestConnectionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GalaxyRepository_GetLastDeployTime_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetLastDeployTimeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GalaxyRepositoryServer).GetLastDeployTime(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GalaxyRepository_GetLastDeployTime_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GalaxyRepositoryServer).GetLastDeployTime(ctx, req.(*GetLastDeployTimeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GalaxyRepository_DiscoverHierarchy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DiscoverHierarchyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GalaxyRepositoryServer).DiscoverHierarchy(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GalaxyRepository_DiscoverHierarchy_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GalaxyRepositoryServer).DiscoverHierarchy(ctx, req.(*DiscoverHierarchyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GalaxyRepository_WatchDeployEvents_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(WatchDeployEventsRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(GalaxyRepositoryServer).WatchDeployEvents(m, &grpc.GenericServerStream[WatchDeployEventsRequest, DeployEvent]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type GalaxyRepository_WatchDeployEventsServer = grpc.ServerStreamingServer[DeployEvent] + +// GalaxyRepository_ServiceDesc is the grpc.ServiceDesc for GalaxyRepository service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var GalaxyRepository_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "galaxy_repository.v1.GalaxyRepository", + HandlerType: (*GalaxyRepositoryServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "TestConnection", + Handler: _GalaxyRepository_TestConnection_Handler, + }, + { + MethodName: "GetLastDeployTime", + Handler: _GalaxyRepository_GetLastDeployTime_Handler, + }, + { + MethodName: "DiscoverHierarchy", + Handler: _GalaxyRepository_DiscoverHierarchy_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "WatchDeployEvents", + Handler: _GalaxyRepository_WatchDeployEvents_Handler, + ServerStreams: true, + }, + }, + Metadata: "galaxy_repository.proto", +} diff --git a/clients/go/mxgateway/galaxy.go b/clients/go/mxgateway/galaxy.go new file mode 100644 index 0000000..949bb6c --- /dev/null +++ b/clients/go/mxgateway/galaxy.go @@ -0,0 +1,246 @@ +package mxgateway + +import ( + "context" + "errors" + "io" + "time" + + pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// RawGalaxyRepositoryClient is the generated gRPC client interface for the +// Galaxy Repository service exposed for callers that need direct contract +// access. +type RawGalaxyRepositoryClient = pb.GalaxyRepositoryClient + +// Generated protobuf aliases for Galaxy Repository messages. +type ( + TestConnectionRequest = pb.TestConnectionRequest + TestConnectionReply = pb.TestConnectionReply + GetLastDeployTimeRequest = pb.GetLastDeployTimeRequest + GetLastDeployTimeReply = pb.GetLastDeployTimeReply + DiscoverHierarchyRequest = pb.DiscoverHierarchyRequest + DiscoverHierarchyReply = pb.DiscoverHierarchyReply + GalaxyObject = pb.GalaxyObject + GalaxyAttribute = pb.GalaxyAttribute + WatchDeployEventsRequest = pb.WatchDeployEventsRequest + DeployEvent = pb.DeployEvent +) + +// RawDeployEventStream is the generated WatchDeployEvents client stream. +type RawDeployEventStream = grpc.ServerStreamingClient[pb.DeployEvent] + +// GalaxyClient owns a gateway gRPC connection and exposes Galaxy Repository +// browse helpers. It mirrors the structure of Client and uses the same +// connection-management conventions. +type GalaxyClient struct { + conn *grpc.ClientConn + raw pb.GalaxyRepositoryClient + opts Options +} + +// DialGalaxy opens a gRPC connection to the gateway for the Galaxy Repository +// service. It applies the same authentication metadata, transport security, +// and dial-timeout behavior as Dial. +func DialGalaxy(ctx context.Context, opts Options) (*GalaxyClient, error) { + if opts.Endpoint == "" { + return nil, errors.New("mxgateway: endpoint is required") + } + + dialCtx := ctx + cancel := func() {} + if opts.DialTimeout > 0 { + dialCtx, cancel = context.WithTimeout(ctx, opts.DialTimeout) + } else if _, ok := ctx.Deadline(); !ok { + dialCtx, cancel = context.WithTimeout(ctx, defaultDialTimeout) + } + defer cancel() + + transportCredentials, err := resolveTransportCredentials(opts) + if err != nil { + return nil, err + } + + dialOptions := []grpc.DialOption{ + grpc.WithTransportCredentials(transportCredentials), + grpc.WithUnaryInterceptor(unaryAuthInterceptor(opts.APIKey)), + grpc.WithStreamInterceptor(streamAuthInterceptor(opts.APIKey)), + grpc.WithBlock(), + } + dialOptions = append(dialOptions, opts.DialOptions...) + + conn, err := grpc.DialContext(dialCtx, opts.Endpoint, dialOptions...) + if err != nil { + return nil, &GatewayError{Op: "dial", Err: err} + } + + return NewGalaxyClient(conn, opts), nil +} + +// NewGalaxyClient wraps an existing gRPC connection for Galaxy Repository +// access. The caller owns closing conn unless it calls Close on the returned +// GalaxyClient. +func NewGalaxyClient(conn *grpc.ClientConn, opts Options) *GalaxyClient { + return &GalaxyClient{ + conn: conn, + raw: pb.NewGalaxyRepositoryClient(conn), + opts: opts, + } +} + +// RawClient returns the generated gRPC client for command-specific parity +// tests. +func (c *GalaxyClient) RawClient() RawGalaxyRepositoryClient { + return c.raw +} + +// TestConnection probes the Galaxy Repository service. It returns the server's +// reported ok flag and a non-nil error only when the RPC itself fails. +func (c *GalaxyClient) TestConnection(ctx context.Context) (bool, error) { + callCtx, cancel := c.callContext(ctx) + defer cancel() + + reply, err := c.raw.TestConnection(callCtx, &pb.TestConnectionRequest{}) + if err != nil { + return false, &GatewayError{Op: "galaxy test connection", Err: err} + } + return reply.GetOk(), nil +} + +// GetLastDeployTime returns the Galaxy's last deploy timestamp. When the server +// reports present=false (no deploy recorded yet) the call returns +// (time.Time{}, false, nil). When present=true the timestamp is returned in +// UTC with present=true. +func (c *GalaxyClient) GetLastDeployTime(ctx context.Context) (time.Time, bool, error) { + callCtx, cancel := c.callContext(ctx) + defer cancel() + + reply, err := c.raw.GetLastDeployTime(callCtx, &pb.GetLastDeployTimeRequest{}) + if err != nil { + return time.Time{}, false, &GatewayError{Op: "galaxy get last deploy time", Err: err} + } + if !reply.GetPresent() { + return time.Time{}, false, nil + } + ts := reply.GetTimeOfLastDeploy() + if ts == nil { + return time.Time{}, false, nil + } + return ts.AsTime(), true, nil +} + +// DiscoverHierarchy returns the deployed Galaxy object hierarchy with each +// object's dynamic attributes. The objects are returned in the order supplied +// by the server. +func (c *GalaxyClient) DiscoverHierarchy(ctx context.Context) ([]*GalaxyObject, error) { + callCtx, cancel := c.callContext(ctx) + defer cancel() + + reply, err := c.raw.DiscoverHierarchy(callCtx, &pb.DiscoverHierarchyRequest{}) + if err != nil { + return nil, &GatewayError{Op: "galaxy discover hierarchy", Err: err} + } + return reply.GetObjects(), nil +} + +// WatchDeployEventsRaw starts the generated WatchDeployEvents stream for callers +// that want direct control over Recv. The caller owns the returned stream's +// lifetime via ctx cancellation. +func (c *GalaxyClient) WatchDeployEventsRaw(ctx context.Context, req *WatchDeployEventsRequest) (RawDeployEventStream, error) { + if req == nil { + req = &pb.WatchDeployEventsRequest{} + } + + stream, err := c.raw.WatchDeployEvents(ctx, req) + if err != nil { + return nil, &GatewayError{Op: "galaxy watch deploy events", Err: err} + } + return stream, nil +} + +// WatchDeployEvents subscribes to Galaxy deploy events. The server emits a +// bootstrap event with the current state immediately on subscribe, then one +// event per new deploy. When lastSeenDeployTime is non-nil it is forwarded to +// the server to suppress the bootstrap event. +// +// The returned event channel is closed when the server completes the stream +// (io.EOF), when ctx is cancelled, or after a terminal error has been +// delivered on the error channel. The error channel is also closed once the +// stream tears down. Surfaced errors are wrapped in *GatewayError. +// +// Cancel ctx to tear the stream down cleanly. +func (c *GalaxyClient) WatchDeployEvents( + ctx context.Context, + lastSeenDeployTime *time.Time, +) (<-chan *DeployEvent, <-chan error, error) { + req := &pb.WatchDeployEventsRequest{} + if lastSeenDeployTime != nil { + req.LastSeenDeployTime = timestamppb.New(*lastSeenDeployTime) + } + + stream, err := c.WatchDeployEventsRaw(ctx, req) + if err != nil { + return nil, nil, err + } + + events := make(chan *DeployEvent, 16) + errs := make(chan error, 1) + go func() { + defer close(events) + defer close(errs) + for { + event, recvErr := stream.Recv() + if recvErr == nil { + select { + case events <- event: + case <-ctx.Done(): + return + } + continue + } + if recvErr == io.EOF { + return + } + if status.Code(recvErr) == codes.Canceled || ctx.Err() != nil { + return + } + select { + case errs <- &GatewayError{Op: "galaxy watch deploy events", Err: recvErr}: + case <-ctx.Done(): + } + return + } + }() + + return events, errs, nil +} + +// Close closes the underlying gRPC connection. +func (c *GalaxyClient) Close() error { + if c == nil || c.conn == nil { + return nil + } + return c.conn.Close() +} + +func (c *GalaxyClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) { + timeout := c.opts.CallTimeout + if timeout == 0 { + timeout = defaultCallTimeout + } + if timeout < 0 { + return ctx, func() {} + } + if deadline, ok := ctx.Deadline(); ok { + timeoutDeadline := time.Now().Add(timeout) + if deadline.Before(timeoutDeadline) { + return ctx, func() {} + } + } + return context.WithTimeout(ctx, timeout) +} diff --git a/clients/go/mxgateway/galaxy_test.go b/clients/go/mxgateway/galaxy_test.go new file mode 100644 index 0000000..bffe014 --- /dev/null +++ b/clients/go/mxgateway/galaxy_test.go @@ -0,0 +1,427 @@ +package mxgateway + +import ( + "context" + "errors" + "net" + "testing" + "time" + + pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated" + "google.golang.org/grpc" + "google.golang.org/grpc/test/bufconn" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestGalaxyTestConnectionAttachesAuthAndReturnsOk(t *testing.T) { + fake := &fakeGalaxyServer{ + testReply: &pb.TestConnectionReply{Ok: true}, + } + client, cleanup := newGalaxyBufconnClient(t, fake) + defer cleanup() + + ok, err := client.TestConnection(context.Background()) + if err != nil { + t.Fatalf("TestConnection() error = %v", err) + } + if !ok { + t.Fatalf("TestConnection() ok = false, want true") + } + if got := fake.testAuth; got != "Bearer test-api-key" { + t.Fatalf("authorization metadata = %q, want %q", got, "Bearer test-api-key") + } +} + +func TestGalaxyGetLastDeployTimeReturnsAbsentForPresentFalse(t *testing.T) { + fake := &fakeGalaxyServer{ + deployReply: &pb.GetLastDeployTimeReply{Present: false}, + } + client, cleanup := newGalaxyBufconnClient(t, fake) + defer cleanup() + + got, present, err := client.GetLastDeployTime(context.Background()) + if err != nil { + t.Fatalf("GetLastDeployTime() error = %v", err) + } + if present { + t.Fatalf("present = true, want false") + } + if !got.IsZero() { + t.Fatalf("time = %v, want zero", got) + } +} + +func TestGalaxyGetLastDeployTimeReturnsTimestampWhenPresent(t *testing.T) { + want := time.Date(2026, 4, 28, 12, 34, 56, 0, time.UTC) + fake := &fakeGalaxyServer{ + deployReply: &pb.GetLastDeployTimeReply{ + Present: true, + TimeOfLastDeploy: timestamppb.New(want), + }, + } + client, cleanup := newGalaxyBufconnClient(t, fake) + defer cleanup() + + got, present, err := client.GetLastDeployTime(context.Background()) + if err != nil { + t.Fatalf("GetLastDeployTime() error = %v", err) + } + if !present { + t.Fatalf("present = false, want true") + } + if !got.Equal(want) { + t.Fatalf("time = %v, want %v", got, want) + } +} + +func TestGalaxyGetLastDeployTimeReturnsAbsentWhenTimestampNil(t *testing.T) { + fake := &fakeGalaxyServer{ + deployReply: &pb.GetLastDeployTimeReply{Present: true, TimeOfLastDeploy: nil}, + } + client, cleanup := newGalaxyBufconnClient(t, fake) + defer cleanup() + + got, present, err := client.GetLastDeployTime(context.Background()) + if err != nil { + t.Fatalf("GetLastDeployTime() error = %v", err) + } + if present { + t.Fatalf("present = true, want false (nil timestamp)") + } + if !got.IsZero() { + t.Fatalf("time = %v, want zero", got) + } +} + +func TestGalaxyDiscoverHierarchyReturnsObjects(t *testing.T) { + fake := &fakeGalaxyServer{ + discoverReply: &pb.DiscoverHierarchyReply{ + Objects: []*pb.GalaxyObject{ + { + GobjectId: 1, + TagName: "TestMachine_001", + ContainedName: "TestMachine_001", + BrowseName: "TestMachine_001", + IsArea: false, + CategoryId: 7, + TemplateChain: []string{"$Object", "$AppObject"}, + Attributes: []*pb.GalaxyAttribute{ + { + AttributeName: "DownloadPath", + FullTagReference: "TestMachine_001.DownloadPath", + MxDataType: 8, + DataTypeName: "String", + }, + }, + }, + { + GobjectId: 2, + TagName: "TestMachine_002", + ContainedName: "TestMachine_002", + ParentGobjectId: 1, + }, + }, + }, + } + client, cleanup := newGalaxyBufconnClient(t, fake) + defer cleanup() + + objects, err := client.DiscoverHierarchy(context.Background()) + if err != nil { + t.Fatalf("DiscoverHierarchy() error = %v", err) + } + if len(objects) != 2 { + t.Fatalf("len(objects) = %d, want 2", len(objects)) + } + if objects[0].GetTagName() != "TestMachine_001" { + t.Fatalf("objects[0].TagName = %q", objects[0].GetTagName()) + } + if len(objects[0].GetAttributes()) != 1 { + t.Fatalf("len(attributes) = %d, want 1", len(objects[0].GetAttributes())) + } + if objects[0].GetAttributes()[0].GetFullTagReference() != "TestMachine_001.DownloadPath" { + t.Fatalf("FullTagReference = %q", objects[0].GetAttributes()[0].GetFullTagReference()) + } +} + +func TestGalaxyDialReturnsGatewayErrorOnRpcFailure(t *testing.T) { + fake := &fakeGalaxyServer{failTest: true} + client, cleanup := newGalaxyBufconnClient(t, fake) + defer cleanup() + + _, err := client.TestConnection(context.Background()) + if err == nil { + t.Fatal("TestConnection() error = nil, want error") + } + var gwErr *GatewayError + if !errors.As(err, &gwErr) { + t.Fatalf("error %T does not support errors.As(*GatewayError)", err) + } + if gwErr.Op != "galaxy test connection" { + t.Fatalf("Op = %q, want %q", gwErr.Op, "galaxy test connection") + } +} + +func TestGalaxyWatchDeployEventsReceivesEventsInOrder(t *testing.T) { + bootstrap := time.Date(2026, 4, 28, 10, 0, 0, 0, time.UTC) + deploy1 := time.Date(2026, 4, 28, 10, 5, 0, 0, time.UTC) + deploy2 := time.Date(2026, 4, 28, 10, 6, 0, 0, time.UTC) + fake := &fakeGalaxyServer{ + watchEvents: []*pb.DeployEvent{ + { + Sequence: 1, + ObservedAt: timestamppb.New(bootstrap), + TimeOfLastDeploy: timestamppb.New(deploy1), + TimeOfLastDeployPresent: true, + ObjectCount: 10, + AttributeCount: 42, + }, + { + Sequence: 2, + ObservedAt: timestamppb.New(deploy2), + TimeOfLastDeploy: timestamppb.New(deploy2), + TimeOfLastDeployPresent: true, + ObjectCount: 11, + AttributeCount: 44, + }, + }, + } + client, cleanup := newGalaxyBufconnClient(t, fake) + defer cleanup() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + events, errs, err := client.WatchDeployEvents(ctx, nil) + if err != nil { + t.Fatalf("WatchDeployEvents() error = %v", err) + } + + got := make([]*DeployEvent, 0, 2) +loop: + for { + select { + case ev, ok := <-events: + if !ok { + break loop + } + got = append(got, ev) + case errVal := <-errs: + if errVal != nil { + t.Fatalf("error channel: %v", errVal) + } + case <-ctx.Done(): + t.Fatalf("timeout waiting for events; got %d", len(got)) + } + } + + if len(got) != 2 { + t.Fatalf("len(events) = %d, want 2", len(got)) + } + if got[0].GetSequence() != 1 || got[1].GetSequence() != 2 { + t.Fatalf("sequences = [%d,%d], want [1,2]", got[0].GetSequence(), got[1].GetSequence()) + } + if !got[0].GetTimeOfLastDeployPresent() { + t.Fatalf("event[0] TimeOfLastDeployPresent = false, want true") + } + if got[0].GetObjectCount() != 10 || got[0].GetAttributeCount() != 42 { + t.Fatalf("event[0] counts = (%d,%d), want (10,42)", got[0].GetObjectCount(), got[0].GetAttributeCount()) + } + if !got[0].GetTimeOfLastDeploy().AsTime().Equal(deploy1) { + t.Fatalf("event[0] TimeOfLastDeploy = %v, want %v", got[0].GetTimeOfLastDeploy().AsTime(), deploy1) + } +} + +func TestGalaxyWatchDeployEventsForwardsLastSeenDeployTime(t *testing.T) { + fake := &fakeGalaxyServer{ + watchEvents: []*pb.DeployEvent{ + {Sequence: 7}, + }, + } + client, cleanup := newGalaxyBufconnClient(t, fake) + defer cleanup() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + lastSeen := time.Date(2026, 4, 28, 9, 0, 0, 0, time.UTC) + events, errs, err := client.WatchDeployEvents(ctx, &lastSeen) + if err != nil { + t.Fatalf("WatchDeployEvents() error = %v", err) + } + + // Drain everything. +loop: + for { + select { + case _, ok := <-events: + if !ok { + break loop + } + case errVal := <-errs: + if errVal != nil { + t.Fatalf("error channel: %v", errVal) + } + case <-ctx.Done(): + t.Fatalf("timeout draining events") + } + } + + if fake.watchRequest == nil { + t.Fatalf("server did not receive a request") + } + gotTs := fake.watchRequest.GetLastSeenDeployTime() + if gotTs == nil { + t.Fatalf("LastSeenDeployTime = nil, want %v", lastSeen) + } + if !gotTs.AsTime().Equal(lastSeen) { + t.Fatalf("LastSeenDeployTime = %v, want %v", gotTs.AsTime(), lastSeen) + } +} + +func TestGalaxyWatchDeployEventsCancelTearsDownStream(t *testing.T) { + fake := &fakeGalaxyServer{ + watchEvents: []*pb.DeployEvent{ + {Sequence: 1}, + }, + watchHoldOpen: true, + } + client, cleanup := newGalaxyBufconnClient(t, fake) + defer cleanup() + + streamCtx, cancelStream := context.WithCancel(context.Background()) + + events, errs, err := client.WatchDeployEvents(streamCtx, nil) + if err != nil { + t.Fatalf("WatchDeployEvents() error = %v", err) + } + + // Wait for the bootstrap event to arrive. + select { + case ev, ok := <-events: + if !ok { + t.Fatalf("events channel closed before delivering bootstrap") + } + if ev.GetSequence() != 1 { + t.Fatalf("got seq=%d, want 1", ev.GetSequence()) + } + case <-time.After(2 * time.Second): + t.Fatalf("timeout waiting for bootstrap event") + } + + // Cancel the stream; both channels must close cleanly without delivering an error. + cancelStream() + + deadline := time.After(2 * time.Second) + for events != nil || errs != nil { + select { + case _, ok := <-events: + if !ok { + events = nil + } + case errVal, ok := <-errs: + if !ok { + errs = nil + continue + } + if errVal != nil { + t.Fatalf("error after cancel: %v", errVal) + } + case <-deadline: + t.Fatalf("channels did not close after cancel; events nil=%v errs nil=%v", events == nil, errs == nil) + } + } +} + +func newGalaxyBufconnClient(t *testing.T, fake *fakeGalaxyServer) (*GalaxyClient, func()) { + t.Helper() + + listener := bufconn.Listen(bufSize) + server := grpc.NewServer() + pb.RegisterGalaxyRepositoryServer(server, fake) + go func() { + if err := server.Serve(listener); err != nil && !errors.Is(err, grpc.ErrServerStopped) { + t.Errorf("bufconn server failed: %v", err) + } + }() + + dialer := func(ctx context.Context, _ string) (net.Conn, error) { + return listener.DialContext(ctx) + } + client, err := DialGalaxy(context.Background(), Options{ + Endpoint: "bufnet", + APIKey: "test-api-key", + Plaintext: true, + DialOptions: []grpc.DialOption{ + grpc.WithContextDialer(dialer), + }, + }) + if err != nil { + t.Fatalf("DialGalaxy() error = %v", err) + } + + return client, func() { + client.Close() + server.Stop() + listener.Close() + } +} + +type fakeGalaxyServer struct { + pb.UnimplementedGalaxyRepositoryServer + + testReply *pb.TestConnectionReply + testAuth string + failTest bool + deployReply *pb.GetLastDeployTimeReply + discoverReply *pb.DiscoverHierarchyReply + watchEvents []*pb.DeployEvent + watchRequest *pb.WatchDeployEventsRequest + watchSendInterval time.Duration + watchHoldOpen bool +} + +func (s *fakeGalaxyServer) TestConnection(ctx context.Context, req *pb.TestConnectionRequest) (*pb.TestConnectionReply, error) { + s.testAuth = authorizationFromContext(ctx) + if s.failTest { + return nil, errors.New("simulated failure") + } + if s.testReply != nil { + return s.testReply, nil + } + return &pb.TestConnectionReply{Ok: true}, nil +} + +func (s *fakeGalaxyServer) GetLastDeployTime(ctx context.Context, req *pb.GetLastDeployTimeRequest) (*pb.GetLastDeployTimeReply, error) { + if s.deployReply != nil { + return s.deployReply, nil + } + return &pb.GetLastDeployTimeReply{Present: false}, nil +} + +func (s *fakeGalaxyServer) DiscoverHierarchy(ctx context.Context, req *pb.DiscoverHierarchyRequest) (*pb.DiscoverHierarchyReply, error) { + if s.discoverReply != nil { + return s.discoverReply, nil + } + return &pb.DiscoverHierarchyReply{}, nil +} + +func (s *fakeGalaxyServer) WatchDeployEvents(req *pb.WatchDeployEventsRequest, stream grpc.ServerStreamingServer[pb.DeployEvent]) error { + s.watchRequest = req + for _, event := range s.watchEvents { + if err := stream.Send(event); err != nil { + return err + } + if s.watchSendInterval > 0 { + select { + case <-time.After(s.watchSendInterval): + case <-stream.Context().Done(): + return stream.Context().Err() + } + } + } + if s.watchHoldOpen { + <-stream.Context().Done() + } + return nil +} diff --git a/clients/java/README.md b/clients/java/README.md index 5bce284..0034fb2 100644 --- a/clients/java/README.md +++ b/clients/java/README.md @@ -67,6 +67,99 @@ cancels the underlying gRPC stream. Canceling or timing out a Java client call only stops the client from waiting; it does not abort an in-flight MXAccess COM call on the worker STA. +## Galaxy Repository Browse + +The Galaxy Repository service is a separate metadata-only gRPC service exposed +by the gateway. It lets clients enumerate the deployed Galaxy object hierarchy +and the dynamic attributes on each object so they know which tag references to +subscribe to via the MXAccess Gateway service. It uses the same API-key auth as +the gateway and requires the `metadata:read` scope. + +`GalaxyRepositoryClient` mirrors the `MxGatewayClient` pattern (caller-managed +or owned channel, `MxGatewayClientOptions`, blocking + async variants). Three +RPCs are exposed: + +```java +MxGatewayClientOptions options = MxGatewayClientOptions.builder() + .endpoint("localhost:5000") + .apiKey(System.getenv("MXGATEWAY_API_KEY")) + .plaintext(true) + .build(); + +try (GalaxyRepositoryClient galaxy = GalaxyRepositoryClient.connect(options)) { + boolean ok = galaxy.testConnection(); + Optional lastDeploy = galaxy.getLastDeployTime(); + List hierarchy = galaxy.discoverHierarchy(); +} +``` + +`getLastDeployTime` returns `Optional.empty()` when the server reports +`present=false`. `discoverHierarchy` returns the generated `GalaxyObject` proto +messages directly so callers can read all fields (including the nested +`GalaxyAttribute` list) without an extra DTO layer. + +The CLI exposes matching subcommands: `galaxy-test`, `galaxy-deploy-time`, +`galaxy-discover`, and `galaxy-watch`. They take the same `--endpoint`, +`--api-key-env`, `--plaintext`, `--ca-file`, `--server-name-override`, +`--timeout`, and `--json` options as the gateway commands. + +```powershell +gradle :mxgateway-cli:run --args="galaxy-test --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json" +gradle :mxgateway-cli:run --args="galaxy-deploy-time --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json" +gradle :mxgateway-cli:run --args="galaxy-discover --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json" +``` + +### Watching deploy events + +`GalaxyRepository.WatchDeployEvents` is a server-streaming RPC: the gateway +sends a bootstrap `DeployEvent` immediately on subscribe and then one event +each time it observes a new `galaxy.time_of_last_deploy`. The `sequence` field +is monotonic per server start; gaps mean the per-subscriber buffer dropped +older events because the consumer was too slow. + +The client exposes both an iterator-style adaptor over the async stub and an +observer-callback variant. Both honour the channel-level `streamTimeout`. + +```java +try (GalaxyRepositoryClient galaxy = GalaxyRepositoryClient.connect(options); + DeployEventStream events = galaxy.watchDeployEvents(/* lastSeenDeployTime */ null)) { + while (events.hasNext()) { + DeployEvent event = events.next(); + // event.getSequence(), event.getObservedAt(), + // event.getTimeOfLastDeploy() / getTimeOfLastDeployPresent(), + // event.getObjectCount(), event.getAttributeCount() + } +} +``` + +Pass an `Instant` for `lastSeenDeployTime` to suppress the bootstrap event when +the cached deploy time matches what the caller already has. `DeployEventStream` +implements `Iterator` and `AutoCloseable`; closing it cancels the +underlying gRPC call. + +For callback delivery (e.g. when the consumer wants to drive a queue or +reactive pipeline), use the async variant: + +```java +DeployEventSubscription subscription = galaxy.watchDeployEventsAsync( + lastSeen, + new StreamObserver<>() { + @Override public void onNext(DeployEvent value) { /* ... */ } + @Override public void onError(Throwable t) { /* ... */ } + @Override public void onCompleted() { /* ... */ } + }); +// later: +subscription.cancel(); // or subscription.close() +``` + +The matching CLI subcommand streams events until cancelled (Ctrl+C) and prints +one line per event in text mode or one JSON object per event with `--json`: + +```powershell +gradle :mxgateway-cli:run --args="galaxy-watch --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json" +gradle :mxgateway-cli:run --args="galaxy-watch --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --last-seen-deploy-time 2026-04-28T18:30:00Z --limit 5" +``` + ## CLI Usage Run the CLI through Gradle: diff --git a/clients/java/mxgateway-cli/src/main/java/com/dohertylan/mxgateway/cli/MxGatewayCli.java b/clients/java/mxgateway-cli/src/main/java/com/dohertylan/mxgateway/cli/MxGatewayCli.java index 868760b..e948b76 100644 --- a/clients/java/mxgateway-cli/src/main/java/com/dohertylan/mxgateway/cli/MxGatewayCli.java +++ b/clients/java/mxgateway-cli/src/main/java/com/dohertylan/mxgateway/cli/MxGatewayCli.java @@ -1,5 +1,7 @@ package com.dohertylan.mxgateway.cli; +import com.dohertylan.mxgateway.client.DeployEventStream; +import com.dohertylan.mxgateway.client.GalaxyRepositoryClient; import com.dohertylan.mxgateway.client.MxEventStream; import com.dohertylan.mxgateway.client.MxGatewayClient; import com.dohertylan.mxgateway.client.MxGatewayClientOptions; @@ -7,15 +9,21 @@ import com.dohertylan.mxgateway.client.MxGatewayClientVersion; import com.dohertylan.mxgateway.client.MxGatewaySecrets; import com.dohertylan.mxgateway.client.MxGatewaySession; import com.dohertylan.mxgateway.client.MxValues; +import galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent; +import galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute; +import galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject; import com.google.protobuf.Message; import com.google.protobuf.util.JsonFormat; import java.io.PrintWriter; import java.nio.file.Path; import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.Callable; import mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest; import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply; @@ -83,9 +91,195 @@ public final class MxGatewayCli implements Callable { commandLine.addSubcommand("write", new WriteCommand(clientFactory)); commandLine.addSubcommand("stream-events", new StreamEventsCommand(clientFactory)); commandLine.addSubcommand("smoke", new SmokeCommand(clientFactory)); + commandLine.addSubcommand("galaxy-test", new GalaxyTestConnectionCommand()); + commandLine.addSubcommand("galaxy-deploy-time", new GalaxyDeployTimeCommand()); + commandLine.addSubcommand("galaxy-discover", new GalaxyDiscoverCommand()); + commandLine.addSubcommand("galaxy-watch", new GalaxyWatchCommand()); return commandLine; } + abstract static class GalaxyCommand implements Callable { + @Mixin + CommonOptions common = new CommonOptions(); + + @Option(names = "--json", description = "Write JSON output.") + boolean json; + + GalaxyRepositoryClient connect() { + return GalaxyRepositoryClient.connect(common.resolved().toClientOptions()); + } + } + + @Command(name = "galaxy-test", description = "Calls GalaxyRepository.TestConnection.") + static final class GalaxyTestConnectionCommand extends GalaxyCommand { + @Override + public Integer call() { + try (GalaxyRepositoryClient client = connect()) { + boolean ok = client.testConnection(); + PrintWriter out = common.spec.commandLine().getOut(); + if (json) { + Map output = new LinkedHashMap<>(); + output.put("command", "galaxy-test"); + output.put("options", common.redactedJsonMap()); + output.put("ok", ok); + out.println(jsonObject(output)); + } else { + out.println(ok); + } + } + return 0; + } + } + + @Command(name = "galaxy-deploy-time", description = "Calls GalaxyRepository.GetLastDeployTime.") + static final class GalaxyDeployTimeCommand extends GalaxyCommand { + @Override + public Integer call() { + try (GalaxyRepositoryClient client = connect()) { + Optional result = client.getLastDeployTime(); + PrintWriter out = common.spec.commandLine().getOut(); + if (json) { + Map output = new LinkedHashMap<>(); + output.put("command", "galaxy-deploy-time"); + output.put("options", common.redactedJsonMap()); + output.put("present", result.isPresent()); + output.put("timeOfLastDeploy", result.map(Instant::toString).orElse("")); + out.println(jsonObject(output)); + } else if (result.isPresent()) { + out.println(result.get()); + } else { + out.println("(none)"); + } + } + return 0; + } + } + + @Command(name = "galaxy-discover", description = "Calls GalaxyRepository.DiscoverHierarchy.") + static final class GalaxyDiscoverCommand extends GalaxyCommand { + @Override + public Integer call() { + try (GalaxyRepositoryClient client = connect()) { + List objects = client.discoverHierarchy(); + PrintWriter out = common.spec.commandLine().getOut(); + if (json) { + Map output = new LinkedHashMap<>(); + output.put("command", "galaxy-discover"); + output.put("options", common.redactedJsonMap()); + output.put("objects", objects.stream().map(MxGatewayCli::galaxyObjectMap).toList()); + out.println(jsonObject(output)); + } else { + out.printf("count=%d%n", objects.size()); + for (GalaxyObject obj : objects) { + out.printf(" %s [%s] attrs=%d%n", + obj.getTagName(), obj.getBrowseName(), obj.getAttributesCount()); + } + } + } + return 0; + } + } + + @Command( + name = "galaxy-watch", + description = "Streams GalaxyRepository.WatchDeployEvents until cancelled.") + static final class GalaxyWatchCommand extends GalaxyCommand { + @Option( + names = "--last-seen-deploy-time", + description = + "Optional ISO-8601 instant. When supplied, the bootstrap event is suppressed if the cached" + + " deploy time matches.") + String lastSeenDeployTime; + + @Option(names = "--limit", defaultValue = "0", description = "Maximum events to print before exiting.") + int limit; + + @Override + public Integer call() { + Instant after = parseInstant(lastSeenDeployTime); + try (GalaxyRepositoryClient client = connect(); + DeployEventStream events = client.watchDeployEvents(after)) { + PrintWriter out = common.spec.commandLine().getOut(); + Thread shutdownHook = new Thread(events::close, "galaxy-watch-shutdown"); + Runtime.getRuntime().addShutdownHook(shutdownHook); + try { + int count = 0; + while (events.hasNext()) { + DeployEvent event = events.next(); + if (json) { + out.println(protoJson(event)); + } else { + out.printf( + "seq=%d observed=%s deployTime=%s objects=%d attributes=%d%n", + event.getSequence(), + formatTimestamp(event.getObservedAt()), + event.getTimeOfLastDeployPresent() + ? formatTimestamp(event.getTimeOfLastDeploy()) + : "(none)", + event.getObjectCount(), + event.getAttributeCount()); + } + out.flush(); + count++; + if (limit > 0 && count >= limit) { + events.close(); + break; + } + } + } finally { + try { + Runtime.getRuntime().removeShutdownHook(shutdownHook); + } catch (IllegalStateException ignored) { + // JVM is already shutting down. + } + } + } + return 0; + } + + private static Instant parseInstant(String value) { + if (value == null || value.isBlank()) { + return null; + } + return Instant.parse(value); + } + + private static String formatTimestamp(com.google.protobuf.Timestamp ts) { + return Instant.ofEpochSecond(ts.getSeconds(), ts.getNanos()).toString(); + } + } + + private static Map galaxyObjectMap(GalaxyObject obj) { + Map values = new LinkedHashMap<>(); + values.put("gobjectId", obj.getGobjectId()); + values.put("tagName", obj.getTagName()); + values.put("containedName", obj.getContainedName()); + values.put("browseName", obj.getBrowseName()); + values.put("parentGobjectId", obj.getParentGobjectId()); + values.put("isArea", obj.getIsArea()); + values.put("categoryId", obj.getCategoryId()); + values.put("hostedByGobjectId", obj.getHostedByGobjectId()); + values.put("templateChain", new ArrayList<>(obj.getTemplateChainList())); + List> attrs = new ArrayList<>(); + for (GalaxyAttribute attr : obj.getAttributesList()) { + Map attrMap = new LinkedHashMap<>(); + attrMap.put("attributeName", attr.getAttributeName()); + attrMap.put("fullTagReference", attr.getFullTagReference()); + attrMap.put("mxDataType", attr.getMxDataType()); + attrMap.put("dataTypeName", attr.getDataTypeName()); + attrMap.put("isArray", attr.getIsArray()); + attrMap.put("arrayDimension", attr.getArrayDimension()); + attrMap.put("arrayDimensionPresent", attr.getArrayDimensionPresent()); + attrMap.put("mxAttributeCategory", attr.getMxAttributeCategory()); + attrMap.put("securityClassification", attr.getSecurityClassification()); + attrMap.put("isHistorized", attr.getIsHistorized()); + attrMap.put("isAlarm", attr.getIsAlarm()); + attrs.add(attrMap); + } + values.put("attributes", attrs); + return values; + } + @Command(name = "version", description = "Prints the Java client version.") public static final class VersionCommand implements Callable { @Spec diff --git a/clients/java/mxgateway-client/build.gradle b/clients/java/mxgateway-client/build.gradle index 7d59366..f88376d 100644 --- a/clients/java/mxgateway-client/build.gradle +++ b/clients/java/mxgateway-client/build.gradle @@ -25,6 +25,7 @@ sourceSets { srcDir rootProject.file('../../src/MxGateway.Contracts/Protos') include 'mxaccess_gateway.proto' include 'mxaccess_worker.proto' + include 'galaxy_repository.proto' } } } diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/DeployEventStream.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/DeployEventStream.java new file mode 100644 index 0000000..5d09b89 --- /dev/null +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/DeployEventStream.java @@ -0,0 +1,132 @@ +package com.dohertylan.mxgateway.client; + +import galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent; +import galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.stub.ClientCallStreamObserver; +import io.grpc.stub.ClientResponseObserver; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; + +/** + * Iterator-style adaptor over the {@code WatchDeployEvents} server-streaming + * RPC. Mirrors {@link MxEventStream}: events arrive on a background gRPC thread + * and are buffered in a bounded blocking queue; the iterator drains them. + * Closing the stream cancels the underlying gRPC call. + */ +public final class DeployEventStream implements Iterator, AutoCloseable { + private static final Object END = new Object(); + + private final BlockingQueue queue; + private volatile ClientCallStreamObserver requestStream; + private volatile boolean closed; + private Object next; + + DeployEventStream(int capacity) { + queue = new ArrayBlockingQueue<>(capacity); + } + + ClientResponseObserver observer() { + return new ClientResponseObserver<>() { + @Override + public void beforeStart(ClientCallStreamObserver requestStream) { + DeployEventStream.this.requestStream = requestStream; + } + + @Override + public void onNext(DeployEvent value) { + offer(value); + } + + @Override + public void onError(Throwable error) { + if (Status.fromThrowable(error).getCode() == Status.Code.CANCELLED && closed) { + offer(END); + return; + } + offer(error); + } + + @Override + public void onCompleted() { + offer(END); + } + }; + } + + @Override + public boolean hasNext() { + if (next == END) { + return false; + } + if (next == null) { + next = take(); + } + if (next instanceof RuntimeException runtimeException) { + next = END; + throw runtimeException; + } + if (next instanceof Throwable throwable) { + next = END; + throw new MxGatewayException( + "galaxy watch deploy events failed: " + throwable.getMessage(), throwable); + } + return next != END; + } + + @Override + public DeployEvent next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + Object value = next; + next = null; + return (DeployEvent) value; + } + + @Override + public void close() { + closed = true; + ClientCallStreamObserver stream = requestStream; + if (stream != null) { + stream.cancel("client cancelled deploy event stream", null); + } + offer(END); + } + + private Object take() { + while (true) { + try { + return queue.take(); + } catch (InterruptedException error) { + Thread.currentThread().interrupt(); + return new StatusRuntimeException( + Status.CANCELLED.withDescription("interrupted while reading deploy events")); + } + } + } + + private void offer(Object value) { + Objects.requireNonNull(value, "value"); + if (value == END) { + if (!queue.offer(value)) { + queue.clear(); + queue.offer(value); + } + return; + } + if (!queue.offer(value)) { + ClientCallStreamObserver stream = requestStream; + if (stream != null) { + stream.cancel("client deploy event stream queue overflowed", null); + } + queue.clear(); + queue.offer(new MxGatewayException("galaxy watch deploy events queue overflowed")); + queue.offer(END); + } + } +} diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/DeployEventSubscription.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/DeployEventSubscription.java new file mode 100644 index 0000000..a42d4be --- /dev/null +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/DeployEventSubscription.java @@ -0,0 +1,60 @@ +package com.dohertylan.mxgateway.client; + +import galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent; +import galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest; +import io.grpc.stub.ClientCallStreamObserver; +import io.grpc.stub.ClientResponseObserver; +import io.grpc.stub.StreamObserver; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Cancellable handle returned by the async {@code watchDeployEvents} variant. + * Mirrors {@link MxGatewayEventSubscription} but for the Galaxy Repository + * deploy-event stream. + */ +public final class DeployEventSubscription implements AutoCloseable { + private final AtomicReference> requestStream = + new AtomicReference<>(); + private final AtomicBoolean cancelled = new AtomicBoolean(); + + ClientResponseObserver wrap(StreamObserver observer) { + return new ClientResponseObserver<>() { + @Override + public void beforeStart(ClientCallStreamObserver stream) { + requestStream.set(stream); + if (cancelled.get()) { + stream.cancel("client cancelled deploy event stream", null); + } + } + + @Override + public void onNext(DeployEvent value) { + observer.onNext(value); + } + + @Override + public void onError(Throwable error) { + observer.onError(error); + } + + @Override + public void onCompleted() { + observer.onCompleted(); + } + }; + } + + public void cancel() { + cancelled.set(true); + ClientCallStreamObserver stream = requestStream.get(); + if (stream != null) { + stream.cancel("client cancelled deploy event stream", null); + } + } + + @Override + public void close() { + cancel(); + } +} diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/GalaxyRepositoryClient.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/GalaxyRepositoryClient.java new file mode 100644 index 0000000..9e142d4 --- /dev/null +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/GalaxyRepositoryClient.java @@ -0,0 +1,288 @@ +package com.dohertylan.mxgateway.client; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; +import galaxy_repository.v1.GalaxyRepositoryGrpc; +import galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent; +import galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply; +import galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest; +import galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject; +import galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply; +import galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest; +import galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply; +import galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest; +import galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest; +import com.google.protobuf.Timestamp; +import io.grpc.Channel; +import io.grpc.ClientInterceptors; +import io.grpc.ManagedChannel; +import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; +import io.grpc.stub.StreamObserver; +import java.time.Instant; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import javax.net.ssl.SSLException; + +/** + * Thin wrapper around the generated {@link GalaxyRepositoryGrpc} stubs that + * exposes the three metadata-only RPCs of the Galaxy Repository service in + * idiomatic Java types. Mirrors the constructor and option-handling style of + * {@link MxGatewayClient}. + */ +public final class GalaxyRepositoryClient implements AutoCloseable { + private final ManagedChannel ownedChannel; + private final MxGatewayClientOptions options; + private final GalaxyRepositoryGrpc.GalaxyRepositoryBlockingStub blockingStub; + private final GalaxyRepositoryGrpc.GalaxyRepositoryFutureStub futureStub; + private final GalaxyRepositoryGrpc.GalaxyRepositoryStub asyncStub; + + private GalaxyRepositoryClient(ManagedChannel channel, MxGatewayClientOptions options) { + this.ownedChannel = channel; + this.options = options; + Channel intercepted = ClientInterceptors.intercept(channel, new MxGatewayAuthInterceptor(options.apiKey())); + blockingStub = GalaxyRepositoryGrpc.newBlockingStub(intercepted); + futureStub = GalaxyRepositoryGrpc.newFutureStub(intercepted); + asyncStub = GalaxyRepositoryGrpc.newStub(intercepted); + } + + /** + * Construct a client over a caller-managed {@link Channel}. The caller owns + * channel lifecycle; {@link #close()} is a no-op for this constructor. + */ + public GalaxyRepositoryClient(Channel channel, MxGatewayClientOptions options) { + this.ownedChannel = null; + this.options = Objects.requireNonNull(options, "options"); + Channel intercepted = ClientInterceptors.intercept(channel, new MxGatewayAuthInterceptor(options.apiKey())); + blockingStub = GalaxyRepositoryGrpc.newBlockingStub(intercepted); + futureStub = GalaxyRepositoryGrpc.newFutureStub(intercepted); + asyncStub = GalaxyRepositoryGrpc.newStub(intercepted); + } + + /** Build a new client and own its channel; close shuts the channel down. */ + public static GalaxyRepositoryClient connect(MxGatewayClientOptions options) { + return new GalaxyRepositoryClient(createChannel(options), options); + } + + public GalaxyRepositoryGrpc.GalaxyRepositoryBlockingStub rawBlockingStub() { + return withDeadline(blockingStub); + } + + public GalaxyRepositoryGrpc.GalaxyRepositoryFutureStub rawFutureStub() { + return withDeadline(futureStub); + } + + public GalaxyRepositoryGrpc.GalaxyRepositoryStub rawAsyncStub() { + return asyncStub; + } + + /** + * Invoke the {@code TestConnection} RPC and return the {@code ok} flag. + */ + public boolean testConnection() { + try { + TestConnectionReply reply = rawBlockingStub().testConnection(TestConnectionRequest.getDefaultInstance()); + return reply.getOk(); + } catch (RuntimeException error) { + if (error instanceof MxGatewayException) { + throw error; + } + throw MxGatewayErrors.fromGrpc("galaxy test connection", error); + } + } + + public CompletableFuture testConnectionAsync() { + return toCompletable(rawFutureStub().testConnection(TestConnectionRequest.getDefaultInstance())) + .thenApply(TestConnectionReply::getOk); + } + + /** + * Invoke the {@code GetLastDeployTime} RPC. Returns {@link Optional#empty()} + * when the server reports {@code present=false}. + */ + public Optional getLastDeployTime() { + try { + GetLastDeployTimeReply reply = + rawBlockingStub().getLastDeployTime(GetLastDeployTimeRequest.getDefaultInstance()); + return mapDeployTime(reply); + } catch (RuntimeException error) { + if (error instanceof MxGatewayException) { + throw error; + } + throw MxGatewayErrors.fromGrpc("galaxy get last deploy time", error); + } + } + + public CompletableFuture> getLastDeployTimeAsync() { + return toCompletable(rawFutureStub().getLastDeployTime(GetLastDeployTimeRequest.getDefaultInstance())) + .thenApply(GalaxyRepositoryClient::mapDeployTime); + } + + /** + * Invoke the {@code DiscoverHierarchy} RPC and return the generated + * {@link GalaxyObject} messages directly. Callers can read every field of + * the proto message without an extra DTO layer. + */ + public List discoverHierarchy() { + try { + DiscoverHierarchyReply reply = + rawBlockingStub().discoverHierarchy(DiscoverHierarchyRequest.getDefaultInstance()); + return reply.getObjectsList(); + } catch (RuntimeException error) { + if (error instanceof MxGatewayException) { + throw error; + } + throw MxGatewayErrors.fromGrpc("galaxy discover hierarchy", error); + } + } + + public CompletableFuture> discoverHierarchyAsync() { + return toCompletable(rawFutureStub().discoverHierarchy(DiscoverHierarchyRequest.getDefaultInstance())) + .thenApply(DiscoverHierarchyReply::getObjectsList); + } + + /** + * Subscribe to {@code WatchDeployEvents} via the async stub and consume + * results through a blocking iterator. Closing the returned stream cancels + * the underlying gRPC call. + * + * @param lastSeenDeployTime optional. When non-null, the bootstrap event is + * suppressed if the cached deploy time matches. + */ + public DeployEventStream watchDeployEvents(Instant lastSeenDeployTime) { + DeployEventStream stream = new DeployEventStream(16); + withStreamDeadline(rawAsyncStub()).watchDeployEvents(buildWatchRequest(lastSeenDeployTime), stream.observer()); + return stream; + } + + /** + * Iterator-style alias for {@link #watchDeployEvents(Instant)} matching the + * task-spec signature. + */ + public Iterator watchDeployEventsIterator(Instant lastSeenDeployTime) { + return watchDeployEvents(lastSeenDeployTime); + } + + /** + * Subscribe to {@code WatchDeployEvents} via the async stub, dispatching + * each event to {@code observer}. The returned subscription is cancellable + * and {@link AutoCloseable}. + */ + public DeployEventSubscription watchDeployEventsAsync( + Instant lastSeenDeployTime, StreamObserver observer) { + Objects.requireNonNull(observer, "observer"); + DeployEventSubscription subscription = new DeployEventSubscription(); + withStreamDeadline(rawAsyncStub()) + .watchDeployEvents(buildWatchRequest(lastSeenDeployTime), subscription.wrap(observer)); + return subscription; + } + + private static WatchDeployEventsRequest buildWatchRequest(Instant lastSeenDeployTime) { + WatchDeployEventsRequest.Builder builder = WatchDeployEventsRequest.newBuilder(); + if (lastSeenDeployTime != null) { + builder.setLastSeenDeployTime(Timestamp.newBuilder() + .setSeconds(lastSeenDeployTime.getEpochSecond()) + .setNanos(lastSeenDeployTime.getNano()) + .build()); + } + return builder.build(); + } + + private > T withStreamDeadline(T stub) { + if (options.streamTimeout() == null || options.streamTimeout().isNegative()) { + return stub; + } + return stub.withDeadlineAfter(options.streamTimeout().toNanos(), TimeUnit.NANOSECONDS); + } + + @Override + public void close() { + if (ownedChannel != null) { + ownedChannel.shutdown(); + } + } + + public void closeAndAwaitTermination() throws InterruptedException { + if (ownedChannel != null) { + ownedChannel.shutdown(); + if (!ownedChannel.awaitTermination(options.connectTimeout().toMillis(), TimeUnit.MILLISECONDS)) { + ownedChannel.shutdownNow(); + } + } + } + + private static Optional mapDeployTime(GetLastDeployTimeReply reply) { + if (!reply.getPresent()) { + return Optional.empty(); + } + Timestamp ts = reply.getTimeOfLastDeploy(); + return Optional.of(Instant.ofEpochSecond(ts.getSeconds(), ts.getNanos())); + } + + private static ManagedChannel createChannel(MxGatewayClientOptions options) { + NettyChannelBuilder builder = NettyChannelBuilder.forTarget(options.endpoint()) + .maxInboundMessageSize(16 * 1024 * 1024); + if (!options.connectTimeout().isNegative()) { + builder.withOption( + io.grpc.netty.shaded.io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS, + Math.toIntExact(options.connectTimeout().toMillis())); + } + if (options.plaintext()) { + builder.usePlaintext(); + } else if (options.caCertificatePath() != null) { + try { + builder.sslContext(GrpcSslContexts.forClient() + .trustManager(options.caCertificatePath().toFile()) + .build()); + } catch (SSLException error) { + throw new MxGatewayException("failed to configure galaxy repository TLS", error); + } + } else { + builder.useTransportSecurity(); + } + if (!options.serverNameOverride().isBlank()) { + builder.overrideAuthority(options.serverNameOverride()); + } + return builder.build(); + } + + private > T withDeadline(T stub) { + if (options.callTimeout().isNegative()) { + return stub; + } + return stub.withDeadlineAfter(options.callTimeout().toNanos(), TimeUnit.NANOSECONDS); + } + + private static CompletableFuture toCompletable(com.google.common.util.concurrent.ListenableFuture source) { + CompletableFuture target = new CompletableFuture<>(); + Futures.addCallback( + source, + new FutureCallback<>() { + @Override + public void onSuccess(T result) { + target.complete(result); + } + + @Override + public void onFailure(Throwable error) { + if (error instanceof RuntimeException runtimeException) { + target.completeExceptionally(MxGatewayErrors.fromGrpc("galaxy async call", runtimeException)); + return; + } + target.completeExceptionally(error); + } + }, + MoreExecutors.directExecutor()); + target.whenComplete((ignoredResult, ignoredError) -> { + if (target.isCancelled()) { + source.cancel(true); + } + }); + return target; + } +} diff --git a/clients/java/mxgateway-client/src/test/java/com/dohertylan/mxgateway/client/GalaxyRepositoryClientTests.java b/clients/java/mxgateway-client/src/test/java/com/dohertylan/mxgateway/client/GalaxyRepositoryClientTests.java new file mode 100644 index 0000000..3ee0a73 --- /dev/null +++ b/clients/java/mxgateway-client/src/test/java/com/dohertylan/mxgateway/client/GalaxyRepositoryClientTests.java @@ -0,0 +1,327 @@ +package com.dohertylan.mxgateway.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.protobuf.Timestamp; +import galaxy_repository.v1.GalaxyRepositoryGrpc; +import galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent; +import galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply; +import galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest; +import galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute; +import galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject; +import galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply; +import galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest; +import galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply; +import galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest; +import galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest; +import io.grpc.ManagedChannel; +import io.grpc.Metadata; +import io.grpc.Server; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; + +final class GalaxyRepositoryClientTests { + @Test + void testConnectionReturnsOkAndSendsAuthMetadata() throws Exception { + AtomicReference authorization = new AtomicReference<>(); + TestService service = new TestService() { + @Override + public void testConnection( + TestConnectionRequest request, StreamObserver responseObserver) { + responseObserver.onNext(TestConnectionReply.newBuilder().setOk(true).build()); + responseObserver.onCompleted(); + } + }; + + try (InProcessGalaxy g = InProcessGalaxy.start(service, authorization); + GalaxyRepositoryClient client = g.client("mxgw_galaxy_secret")) { + assertTrue(client.testConnection()); + assertEquals("Bearer mxgw_galaxy_secret", authorization.get()); + } + } + + @Test + void getLastDeployTimeReturnsEmptyWhenPresentFalse() throws Exception { + TestService service = new TestService() { + @Override + public void getLastDeployTime( + GetLastDeployTimeRequest request, StreamObserver responseObserver) { + responseObserver.onNext( + GetLastDeployTimeReply.newBuilder().setPresent(false).build()); + responseObserver.onCompleted(); + } + }; + + try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>()); + GalaxyRepositoryClient client = g.client("")) { + Optional result = client.getLastDeployTime(); + assertFalse(result.isPresent()); + } + } + + @Test + void getLastDeployTimeReturnsInstantWhenPresent() throws Exception { + Timestamp expected = Timestamp.newBuilder().setSeconds(1_700_000_000L).setNanos(123_000_000).build(); + TestService service = new TestService() { + @Override + public void getLastDeployTime( + GetLastDeployTimeRequest request, StreamObserver responseObserver) { + responseObserver.onNext(GetLastDeployTimeReply.newBuilder() + .setPresent(true) + .setTimeOfLastDeploy(expected) + .build()); + responseObserver.onCompleted(); + } + }; + + try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>()); + GalaxyRepositoryClient client = g.client("")) { + Optional result = client.getLastDeployTime(); + assertTrue(result.isPresent()); + assertEquals(Instant.ofEpochSecond(1_700_000_000L, 123_000_000), result.get()); + } + } + + @Test + void discoverHierarchyReturnsObjectsAndAttributes() throws Exception { + AtomicReference seenRequest = new AtomicReference<>(); + TestService service = new TestService() { + @Override + public void discoverHierarchy( + DiscoverHierarchyRequest request, StreamObserver responseObserver) { + seenRequest.set(request); + responseObserver.onNext(DiscoverHierarchyReply.newBuilder() + .addObjects(GalaxyObject.newBuilder() + .setGobjectId(7) + .setTagName("Pump_001") + .setContainedName("Pump") + .setBrowseName("Pump") + .setParentGobjectId(1) + .setIsArea(false) + .setCategoryId(3) + .setHostedByGobjectId(0) + .addTemplateChain("$Pump") + .addAttributes(GalaxyAttribute.newBuilder() + .setAttributeName("Speed") + .setFullTagReference("Pump_001.Speed") + .setMxDataType(5) + .setDataTypeName("MxFloat") + .setIsArray(false) + .setIsHistorized(true))) + .build()); + responseObserver.onCompleted(); + } + }; + + try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>()); + GalaxyRepositoryClient client = g.client("")) { + List objects = client.discoverHierarchy(); + assertEquals(1, objects.size()); + GalaxyObject only = objects.get(0); + assertEquals(7, only.getGobjectId()); + assertEquals("Pump_001", only.getTagName()); + assertEquals(1, only.getAttributesCount()); + assertEquals("Pump_001.Speed", only.getAttributes(0).getFullTagReference()); + assertTrue(only.getAttributes(0).getIsHistorized()); + } + } + + @Test + void watchDeployEventsReceivesEventsInOrder() throws Exception { + DeployEvent first = DeployEvent.newBuilder() + .setSequence(1) + .setObservedAt(Timestamp.newBuilder().setSeconds(1_700_000_000L).build()) + .setTimeOfLastDeploy(Timestamp.newBuilder().setSeconds(1_699_999_000L).build()) + .setTimeOfLastDeployPresent(true) + .setObjectCount(42) + .setAttributeCount(123) + .build(); + DeployEvent second = DeployEvent.newBuilder() + .setSequence(2) + .setObservedAt(Timestamp.newBuilder().setSeconds(1_700_000_100L).build()) + .setTimeOfLastDeployPresent(false) + .setObjectCount(43) + .setAttributeCount(125) + .build(); + + TestService service = new TestService() { + @Override + public void watchDeployEvents( + WatchDeployEventsRequest request, StreamObserver responseObserver) { + responseObserver.onNext(first); + responseObserver.onNext(second); + responseObserver.onCompleted(); + } + }; + + try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>()); + GalaxyRepositoryClient client = g.client("")) { + try (DeployEventStream stream = client.watchDeployEvents(null)) { + assertTrue(stream.hasNext()); + DeployEvent event1 = stream.next(); + assertEquals(1L, event1.getSequence()); + assertEquals(42, event1.getObjectCount()); + assertTrue(event1.getTimeOfLastDeployPresent()); + + assertTrue(stream.hasNext()); + DeployEvent event2 = stream.next(); + assertEquals(2L, event2.getSequence()); + assertFalse(event2.getTimeOfLastDeployPresent()); + + assertFalse(stream.hasNext()); + } + } + } + + @Test + void watchDeployEventsPropagatesLastSeenDeployTime() throws Exception { + AtomicReference seen = new AtomicReference<>(); + Instant lastSeen = Instant.ofEpochSecond(1_700_000_000L, 250_000_000); + + TestService service = new TestService() { + @Override + public void watchDeployEvents( + WatchDeployEventsRequest request, StreamObserver responseObserver) { + seen.set(request); + responseObserver.onCompleted(); + } + }; + + try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>()); + GalaxyRepositoryClient client = g.client("")) { + try (DeployEventStream stream = client.watchDeployEvents(lastSeen)) { + assertFalse(stream.hasNext()); + } + } + + WatchDeployEventsRequest request = seen.get(); + assertNotNull(request); + Timestamp expected = request.getLastSeenDeployTime(); + assertEquals(lastSeen.getEpochSecond(), expected.getSeconds()); + assertEquals(lastSeen.getNano(), expected.getNanos()); + } + + @Test + void watchDeployEventsClientCancellationTearsDownCleanly() throws Exception { + CountDownLatch cancelObserved = new CountDownLatch(1); + + TestService service = new TestService() { + @Override + public void watchDeployEvents( + WatchDeployEventsRequest request, StreamObserver responseObserver) { + io.grpc.stub.ServerCallStreamObserver serverObserver = + (io.grpc.stub.ServerCallStreamObserver) responseObserver; + serverObserver.setOnCancelHandler(cancelObserved::countDown); + + DeployEvent bootstrap = DeployEvent.newBuilder() + .setSequence(1) + .setObservedAt(Timestamp.newBuilder().setSeconds(1L).build()) + .build(); + responseObserver.onNext(bootstrap); + // Server holds the stream open; cancellation must come from the client. + } + }; + + try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>()); + GalaxyRepositoryClient client = g.client("")) { + DeployEventStream stream = client.watchDeployEvents(null); + assertTrue(stream.hasNext()); + assertEquals(1L, stream.next().getSequence()); + + stream.close(); + assertTrue( + cancelObserved.await(5, TimeUnit.SECONDS), + "server should observe client-side cancellation"); + assertFalse(stream.hasNext()); + } + } + + private abstract static class TestService extends GalaxyRepositoryGrpc.GalaxyRepositoryImplBase { + @Override + public void testConnection( + TestConnectionRequest request, StreamObserver responseObserver) { + responseObserver.onNext(TestConnectionReply.newBuilder().setOk(true).build()); + responseObserver.onCompleted(); + } + + @Override + public void getLastDeployTime( + GetLastDeployTimeRequest request, StreamObserver responseObserver) { + responseObserver.onNext(GetLastDeployTimeReply.newBuilder().setPresent(false).build()); + responseObserver.onCompleted(); + } + + @Override + public void discoverHierarchy( + DiscoverHierarchyRequest request, StreamObserver responseObserver) { + responseObserver.onNext(DiscoverHierarchyReply.getDefaultInstance()); + responseObserver.onCompleted(); + } + + @Override + public void watchDeployEvents( + WatchDeployEventsRequest request, StreamObserver responseObserver) { + responseObserver.onCompleted(); + } + } + + private record InProcessGalaxy(Server server, ManagedChannel channel) implements AutoCloseable { + static InProcessGalaxy start( + GalaxyRepositoryGrpc.GalaxyRepositoryImplBase service, AtomicReference authorization) + throws Exception { + String serverName = "mxgw-galaxy-java-" + UUID.randomUUID(); + ServerInterceptor interceptor = new ServerInterceptor() { + @Override + public ServerCall.Listener interceptCall( + ServerCall call, + Metadata headers, + ServerCallHandler next) { + authorization.set(headers.get(MxGatewayAuthInterceptor.AUTHORIZATION_HEADER)); + return next.startCall(call, headers); + } + }; + Server server = InProcessServerBuilder.forName(serverName) + .directExecutor() + .addService(io.grpc.ServerInterceptors.intercept(service, interceptor)) + .build() + .start(); + ManagedChannel channel = InProcessChannelBuilder.forName(serverName) + .directExecutor() + .build(); + return new InProcessGalaxy(server, channel); + } + + GalaxyRepositoryClient client(String apiKey) { + return new GalaxyRepositoryClient( + channel, + MxGatewayClientOptions.builder() + .endpoint("in-process") + .apiKey(apiKey) + .plaintext(true) + .callTimeout(Duration.ofSeconds(5)) + .build()); + } + + @Override + public void close() { + channel.shutdownNow(); + server.shutdownNow(); + } + } +} diff --git a/clients/java/src/main/generated/main/grpc/galaxy_repository/v1/GalaxyRepositoryGrpc.java b/clients/java/src/main/generated/main/grpc/galaxy_repository/v1/GalaxyRepositoryGrpc.java new file mode 100644 index 0000000..c0bb9db --- /dev/null +++ b/clients/java/src/main/generated/main/grpc/galaxy_repository/v1/GalaxyRepositoryGrpc.java @@ -0,0 +1,641 @@ +package galaxy_repository.v1; + +import static io.grpc.MethodDescriptor.generateFullMethodName; + +/** + *
+ * Read-only browse over the AVEVA System Platform Galaxy Repository (ZB SQL
+ * database). Lets clients enumerate the deployed object hierarchy and each
+ * object's dynamic attributes so they know what tag references to subscribe
+ * to via the MxAccessGateway service.
+ * 
+ */ +@io.grpc.stub.annotations.GrpcGenerated +public final class GalaxyRepositoryGrpc { + + private GalaxyRepositoryGrpc() {} + + public static final java.lang.String SERVICE_NAME = "galaxy_repository.v1.GalaxyRepository"; + + // Static method descriptors that strictly reflect the proto. + private static volatile io.grpc.MethodDescriptor getTestConnectionMethod; + + @io.grpc.stub.annotations.RpcMethod( + fullMethodName = SERVICE_NAME + '/' + "TestConnection", + requestType = galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest.class, + responseType = galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply.class, + methodType = io.grpc.MethodDescriptor.MethodType.UNARY) + public static io.grpc.MethodDescriptor getTestConnectionMethod() { + io.grpc.MethodDescriptor getTestConnectionMethod; + if ((getTestConnectionMethod = GalaxyRepositoryGrpc.getTestConnectionMethod) == null) { + synchronized (GalaxyRepositoryGrpc.class) { + if ((getTestConnectionMethod = GalaxyRepositoryGrpc.getTestConnectionMethod) == null) { + GalaxyRepositoryGrpc.getTestConnectionMethod = getTestConnectionMethod = + io.grpc.MethodDescriptor.newBuilder() + .setType(io.grpc.MethodDescriptor.MethodType.UNARY) + .setFullMethodName(generateFullMethodName(SERVICE_NAME, "TestConnection")) + .setSampledToLocalTracing(true) + .setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( + galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest.getDefaultInstance())) + .setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( + galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply.getDefaultInstance())) + .setSchemaDescriptor(new GalaxyRepositoryMethodDescriptorSupplier("TestConnection")) + .build(); + } + } + } + return getTestConnectionMethod; + } + + private static volatile io.grpc.MethodDescriptor getGetLastDeployTimeMethod; + + @io.grpc.stub.annotations.RpcMethod( + fullMethodName = SERVICE_NAME + '/' + "GetLastDeployTime", + requestType = galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest.class, + responseType = galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply.class, + methodType = io.grpc.MethodDescriptor.MethodType.UNARY) + public static io.grpc.MethodDescriptor getGetLastDeployTimeMethod() { + io.grpc.MethodDescriptor getGetLastDeployTimeMethod; + if ((getGetLastDeployTimeMethod = GalaxyRepositoryGrpc.getGetLastDeployTimeMethod) == null) { + synchronized (GalaxyRepositoryGrpc.class) { + if ((getGetLastDeployTimeMethod = GalaxyRepositoryGrpc.getGetLastDeployTimeMethod) == null) { + GalaxyRepositoryGrpc.getGetLastDeployTimeMethod = getGetLastDeployTimeMethod = + io.grpc.MethodDescriptor.newBuilder() + .setType(io.grpc.MethodDescriptor.MethodType.UNARY) + .setFullMethodName(generateFullMethodName(SERVICE_NAME, "GetLastDeployTime")) + .setSampledToLocalTracing(true) + .setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( + galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest.getDefaultInstance())) + .setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( + galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply.getDefaultInstance())) + .setSchemaDescriptor(new GalaxyRepositoryMethodDescriptorSupplier("GetLastDeployTime")) + .build(); + } + } + } + return getGetLastDeployTimeMethod; + } + + private static volatile io.grpc.MethodDescriptor getDiscoverHierarchyMethod; + + @io.grpc.stub.annotations.RpcMethod( + fullMethodName = SERVICE_NAME + '/' + "DiscoverHierarchy", + requestType = galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest.class, + responseType = galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply.class, + methodType = io.grpc.MethodDescriptor.MethodType.UNARY) + public static io.grpc.MethodDescriptor getDiscoverHierarchyMethod() { + io.grpc.MethodDescriptor getDiscoverHierarchyMethod; + if ((getDiscoverHierarchyMethod = GalaxyRepositoryGrpc.getDiscoverHierarchyMethod) == null) { + synchronized (GalaxyRepositoryGrpc.class) { + if ((getDiscoverHierarchyMethod = GalaxyRepositoryGrpc.getDiscoverHierarchyMethod) == null) { + GalaxyRepositoryGrpc.getDiscoverHierarchyMethod = getDiscoverHierarchyMethod = + io.grpc.MethodDescriptor.newBuilder() + .setType(io.grpc.MethodDescriptor.MethodType.UNARY) + .setFullMethodName(generateFullMethodName(SERVICE_NAME, "DiscoverHierarchy")) + .setSampledToLocalTracing(true) + .setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( + galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest.getDefaultInstance())) + .setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( + galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply.getDefaultInstance())) + .setSchemaDescriptor(new GalaxyRepositoryMethodDescriptorSupplier("DiscoverHierarchy")) + .build(); + } + } + } + return getDiscoverHierarchyMethod; + } + + private static volatile io.grpc.MethodDescriptor getWatchDeployEventsMethod; + + @io.grpc.stub.annotations.RpcMethod( + fullMethodName = SERVICE_NAME + '/' + "WatchDeployEvents", + requestType = galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest.class, + responseType = galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent.class, + methodType = io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING) + public static io.grpc.MethodDescriptor getWatchDeployEventsMethod() { + io.grpc.MethodDescriptor getWatchDeployEventsMethod; + if ((getWatchDeployEventsMethod = GalaxyRepositoryGrpc.getWatchDeployEventsMethod) == null) { + synchronized (GalaxyRepositoryGrpc.class) { + if ((getWatchDeployEventsMethod = GalaxyRepositoryGrpc.getWatchDeployEventsMethod) == null) { + GalaxyRepositoryGrpc.getWatchDeployEventsMethod = getWatchDeployEventsMethod = + io.grpc.MethodDescriptor.newBuilder() + .setType(io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING) + .setFullMethodName(generateFullMethodName(SERVICE_NAME, "WatchDeployEvents")) + .setSampledToLocalTracing(true) + .setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( + galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest.getDefaultInstance())) + .setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( + galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent.getDefaultInstance())) + .setSchemaDescriptor(new GalaxyRepositoryMethodDescriptorSupplier("WatchDeployEvents")) + .build(); + } + } + } + return getWatchDeployEventsMethod; + } + + /** + * Creates a new async stub that supports all call types for the service + */ + public static GalaxyRepositoryStub newStub(io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public GalaxyRepositoryStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new GalaxyRepositoryStub(channel, callOptions); + } + }; + return GalaxyRepositoryStub.newStub(factory, channel); + } + + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static GalaxyRepositoryBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public GalaxyRepositoryBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new GalaxyRepositoryBlockingV2Stub(channel, callOptions); + } + }; + return GalaxyRepositoryBlockingV2Stub.newStub(factory, channel); + } + + /** + * Creates a new blocking-style stub that supports unary and streaming output calls on the service + */ + public static GalaxyRepositoryBlockingStub newBlockingStub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public GalaxyRepositoryBlockingStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new GalaxyRepositoryBlockingStub(channel, callOptions); + } + }; + return GalaxyRepositoryBlockingStub.newStub(factory, channel); + } + + /** + * Creates a new ListenableFuture-style stub that supports unary calls on the service + */ + public static GalaxyRepositoryFutureStub newFutureStub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public GalaxyRepositoryFutureStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new GalaxyRepositoryFutureStub(channel, callOptions); + } + }; + return GalaxyRepositoryFutureStub.newStub(factory, channel); + } + + /** + *
+   * Read-only browse over the AVEVA System Platform Galaxy Repository (ZB SQL
+   * database). Lets clients enumerate the deployed object hierarchy and each
+   * object's dynamic attributes so they know what tag references to subscribe
+   * to via the MxAccessGateway service.
+   * 
+ */ + public interface AsyncService { + + /** + */ + default void testConnection(galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest request, + io.grpc.stub.StreamObserver responseObserver) { + io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getTestConnectionMethod(), responseObserver); + } + + /** + */ + default void getLastDeployTime(galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest request, + io.grpc.stub.StreamObserver responseObserver) { + io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getGetLastDeployTimeMethod(), responseObserver); + } + + /** + */ + default void discoverHierarchy(galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest request, + io.grpc.stub.StreamObserver responseObserver) { + io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getDiscoverHierarchyMethod(), responseObserver); + } + + /** + *
+     * Server-stream of deploy events. The server emits the current state immediately
+     * on subscribe (so clients can bootstrap their cache without waiting for the next
+     * deploy), then emits one event each time the gateway's hierarchy cache observes
+     * a new galaxy.time_of_last_deploy. The sequence field is monotonically
+     * increasing per server start; gaps indicate the per-subscriber buffer dropped
+     * older events because the client was too slow.
+     * 
+ */ + default void watchDeployEvents(galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest request, + io.grpc.stub.StreamObserver responseObserver) { + io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getWatchDeployEventsMethod(), responseObserver); + } + } + + /** + * Base class for the server implementation of the service GalaxyRepository. + *
+   * Read-only browse over the AVEVA System Platform Galaxy Repository (ZB SQL
+   * database). Lets clients enumerate the deployed object hierarchy and each
+   * object's dynamic attributes so they know what tag references to subscribe
+   * to via the MxAccessGateway service.
+   * 
+ */ + public static abstract class GalaxyRepositoryImplBase + implements io.grpc.BindableService, AsyncService { + + @java.lang.Override public final io.grpc.ServerServiceDefinition bindService() { + return GalaxyRepositoryGrpc.bindService(this); + } + } + + /** + * A stub to allow clients to do asynchronous rpc calls to service GalaxyRepository. + *
+   * Read-only browse over the AVEVA System Platform Galaxy Repository (ZB SQL
+   * database). Lets clients enumerate the deployed object hierarchy and each
+   * object's dynamic attributes so they know what tag references to subscribe
+   * to via the MxAccessGateway service.
+   * 
+ */ + public static final class GalaxyRepositoryStub + extends io.grpc.stub.AbstractAsyncStub { + private GalaxyRepositoryStub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected GalaxyRepositoryStub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new GalaxyRepositoryStub(channel, callOptions); + } + + /** + */ + public void testConnection(galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest request, + io.grpc.stub.StreamObserver responseObserver) { + io.grpc.stub.ClientCalls.asyncUnaryCall( + getChannel().newCall(getTestConnectionMethod(), getCallOptions()), request, responseObserver); + } + + /** + */ + public void getLastDeployTime(galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest request, + io.grpc.stub.StreamObserver responseObserver) { + io.grpc.stub.ClientCalls.asyncUnaryCall( + getChannel().newCall(getGetLastDeployTimeMethod(), getCallOptions()), request, responseObserver); + } + + /** + */ + public void discoverHierarchy(galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest request, + io.grpc.stub.StreamObserver responseObserver) { + io.grpc.stub.ClientCalls.asyncUnaryCall( + getChannel().newCall(getDiscoverHierarchyMethod(), getCallOptions()), request, responseObserver); + } + + /** + *
+     * Server-stream of deploy events. The server emits the current state immediately
+     * on subscribe (so clients can bootstrap their cache without waiting for the next
+     * deploy), then emits one event each time the gateway's hierarchy cache observes
+     * a new galaxy.time_of_last_deploy. The sequence field is monotonically
+     * increasing per server start; gaps indicate the per-subscriber buffer dropped
+     * older events because the client was too slow.
+     * 
+ */ + public void watchDeployEvents(galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest request, + io.grpc.stub.StreamObserver responseObserver) { + io.grpc.stub.ClientCalls.asyncServerStreamingCall( + getChannel().newCall(getWatchDeployEventsMethod(), getCallOptions()), request, responseObserver); + } + } + + /** + * A stub to allow clients to do synchronous rpc calls to service GalaxyRepository. + *
+   * Read-only browse over the AVEVA System Platform Galaxy Repository (ZB SQL
+   * database). Lets clients enumerate the deployed object hierarchy and each
+   * object's dynamic attributes so they know what tag references to subscribe
+   * to via the MxAccessGateway service.
+   * 
+ */ + public static final class GalaxyRepositoryBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private GalaxyRepositoryBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected GalaxyRepositoryBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new GalaxyRepositoryBlockingV2Stub(channel, callOptions); + } + + /** + */ + public galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply testConnection(galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( + getChannel(), getTestConnectionMethod(), getCallOptions(), request); + } + + /** + */ + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply getLastDeployTime(galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( + getChannel(), getGetLastDeployTimeMethod(), getCallOptions(), request); + } + + /** + */ + public galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply discoverHierarchy(galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( + getChannel(), getDiscoverHierarchyMethod(), getCallOptions(), request); + } + + /** + *
+     * Server-stream of deploy events. The server emits the current state immediately
+     * on subscribe (so clients can bootstrap their cache without waiting for the next
+     * deploy), then emits one event each time the gateway's hierarchy cache observes
+     * a new galaxy.time_of_last_deploy. The sequence field is monotonically
+     * increasing per server start; gaps indicate the per-subscriber buffer dropped
+     * older events because the client was too slow.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + watchDeployEvents(galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest request) { + return io.grpc.stub.ClientCalls.blockingV2ServerStreamingCall( + getChannel(), getWatchDeployEventsMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do limited synchronous rpc calls to service GalaxyRepository. + *
+   * Read-only browse over the AVEVA System Platform Galaxy Repository (ZB SQL
+   * database). Lets clients enumerate the deployed object hierarchy and each
+   * object's dynamic attributes so they know what tag references to subscribe
+   * to via the MxAccessGateway service.
+   * 
+ */ + public static final class GalaxyRepositoryBlockingStub + extends io.grpc.stub.AbstractBlockingStub { + private GalaxyRepositoryBlockingStub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected GalaxyRepositoryBlockingStub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new GalaxyRepositoryBlockingStub(channel, callOptions); + } + + /** + */ + public galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply testConnection(galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getTestConnectionMethod(), getCallOptions(), request); + } + + /** + */ + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply getLastDeployTime(galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getGetLastDeployTimeMethod(), getCallOptions(), request); + } + + /** + */ + public galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply discoverHierarchy(galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getDiscoverHierarchyMethod(), getCallOptions(), request); + } + + /** + *
+     * Server-stream of deploy events. The server emits the current state immediately
+     * on subscribe (so clients can bootstrap their cache without waiting for the next
+     * deploy), then emits one event each time the gateway's hierarchy cache observes
+     * a new galaxy.time_of_last_deploy. The sequence field is monotonically
+     * increasing per server start; gaps indicate the per-subscriber buffer dropped
+     * older events because the client was too slow.
+     * 
+ */ + public java.util.Iterator watchDeployEvents( + galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest request) { + return io.grpc.stub.ClientCalls.blockingServerStreamingCall( + getChannel(), getWatchDeployEventsMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do ListenableFuture-style rpc calls to service GalaxyRepository. + *
+   * Read-only browse over the AVEVA System Platform Galaxy Repository (ZB SQL
+   * database). Lets clients enumerate the deployed object hierarchy and each
+   * object's dynamic attributes so they know what tag references to subscribe
+   * to via the MxAccessGateway service.
+   * 
+ */ + public static final class GalaxyRepositoryFutureStub + extends io.grpc.stub.AbstractFutureStub { + private GalaxyRepositoryFutureStub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected GalaxyRepositoryFutureStub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new GalaxyRepositoryFutureStub(channel, callOptions); + } + + /** + */ + public com.google.common.util.concurrent.ListenableFuture testConnection( + galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest request) { + return io.grpc.stub.ClientCalls.futureUnaryCall( + getChannel().newCall(getTestConnectionMethod(), getCallOptions()), request); + } + + /** + */ + public com.google.common.util.concurrent.ListenableFuture getLastDeployTime( + galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest request) { + return io.grpc.stub.ClientCalls.futureUnaryCall( + getChannel().newCall(getGetLastDeployTimeMethod(), getCallOptions()), request); + } + + /** + */ + public com.google.common.util.concurrent.ListenableFuture discoverHierarchy( + galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest request) { + return io.grpc.stub.ClientCalls.futureUnaryCall( + getChannel().newCall(getDiscoverHierarchyMethod(), getCallOptions()), request); + } + } + + private static final int METHODID_TEST_CONNECTION = 0; + private static final int METHODID_GET_LAST_DEPLOY_TIME = 1; + private static final int METHODID_DISCOVER_HIERARCHY = 2; + private static final int METHODID_WATCH_DEPLOY_EVENTS = 3; + + private static final class MethodHandlers implements + io.grpc.stub.ServerCalls.UnaryMethod, + io.grpc.stub.ServerCalls.ServerStreamingMethod, + io.grpc.stub.ServerCalls.ClientStreamingMethod, + io.grpc.stub.ServerCalls.BidiStreamingMethod { + private final AsyncService serviceImpl; + private final int methodId; + + MethodHandlers(AsyncService serviceImpl, int methodId) { + this.serviceImpl = serviceImpl; + this.methodId = methodId; + } + + @java.lang.Override + @java.lang.SuppressWarnings("unchecked") + public void invoke(Req request, io.grpc.stub.StreamObserver responseObserver) { + switch (methodId) { + case METHODID_TEST_CONNECTION: + serviceImpl.testConnection((galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest) request, + (io.grpc.stub.StreamObserver) responseObserver); + break; + case METHODID_GET_LAST_DEPLOY_TIME: + serviceImpl.getLastDeployTime((galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest) request, + (io.grpc.stub.StreamObserver) responseObserver); + break; + case METHODID_DISCOVER_HIERARCHY: + serviceImpl.discoverHierarchy((galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest) request, + (io.grpc.stub.StreamObserver) responseObserver); + break; + case METHODID_WATCH_DEPLOY_EVENTS: + serviceImpl.watchDeployEvents((galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest) request, + (io.grpc.stub.StreamObserver) responseObserver); + break; + default: + throw new AssertionError(); + } + } + + @java.lang.Override + @java.lang.SuppressWarnings("unchecked") + public io.grpc.stub.StreamObserver invoke( + io.grpc.stub.StreamObserver responseObserver) { + switch (methodId) { + default: + throw new AssertionError(); + } + } + } + + public static final io.grpc.ServerServiceDefinition bindService(AsyncService service) { + return io.grpc.ServerServiceDefinition.builder(getServiceDescriptor()) + .addMethod( + getTestConnectionMethod(), + io.grpc.stub.ServerCalls.asyncUnaryCall( + new MethodHandlers< + galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest, + galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply>( + service, METHODID_TEST_CONNECTION))) + .addMethod( + getGetLastDeployTimeMethod(), + io.grpc.stub.ServerCalls.asyncUnaryCall( + new MethodHandlers< + galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest, + galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply>( + service, METHODID_GET_LAST_DEPLOY_TIME))) + .addMethod( + getDiscoverHierarchyMethod(), + io.grpc.stub.ServerCalls.asyncUnaryCall( + new MethodHandlers< + galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest, + galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply>( + service, METHODID_DISCOVER_HIERARCHY))) + .addMethod( + getWatchDeployEventsMethod(), + io.grpc.stub.ServerCalls.asyncServerStreamingCall( + new MethodHandlers< + galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest, + galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent>( + service, METHODID_WATCH_DEPLOY_EVENTS))) + .build(); + } + + private static abstract class GalaxyRepositoryBaseDescriptorSupplier + implements io.grpc.protobuf.ProtoFileDescriptorSupplier, io.grpc.protobuf.ProtoServiceDescriptorSupplier { + GalaxyRepositoryBaseDescriptorSupplier() {} + + @java.lang.Override + public com.google.protobuf.Descriptors.FileDescriptor getFileDescriptor() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.getDescriptor(); + } + + @java.lang.Override + public com.google.protobuf.Descriptors.ServiceDescriptor getServiceDescriptor() { + return getFileDescriptor().findServiceByName("GalaxyRepository"); + } + } + + private static final class GalaxyRepositoryFileDescriptorSupplier + extends GalaxyRepositoryBaseDescriptorSupplier { + GalaxyRepositoryFileDescriptorSupplier() {} + } + + private static final class GalaxyRepositoryMethodDescriptorSupplier + extends GalaxyRepositoryBaseDescriptorSupplier + implements io.grpc.protobuf.ProtoMethodDescriptorSupplier { + private final java.lang.String methodName; + + GalaxyRepositoryMethodDescriptorSupplier(java.lang.String methodName) { + this.methodName = methodName; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.MethodDescriptor getMethodDescriptor() { + return getServiceDescriptor().findMethodByName(methodName); + } + } + + private static volatile io.grpc.ServiceDescriptor serviceDescriptor; + + public static io.grpc.ServiceDescriptor getServiceDescriptor() { + io.grpc.ServiceDescriptor result = serviceDescriptor; + if (result == null) { + synchronized (GalaxyRepositoryGrpc.class) { + result = serviceDescriptor; + if (result == null) { + serviceDescriptor = result = io.grpc.ServiceDescriptor.newBuilder(SERVICE_NAME) + .setSchemaDescriptor(new GalaxyRepositoryFileDescriptorSupplier()) + .addMethod(getTestConnectionMethod()) + .addMethod(getGetLastDeployTimeMethod()) + .addMethod(getDiscoverHierarchyMethod()) + .addMethod(getWatchDeployEventsMethod()) + .build(); + } + } + } + return result; + } +} diff --git a/clients/java/src/main/generated/main/java/galaxy_repository/v1/GalaxyRepositoryOuterClass.java b/clients/java/src/main/generated/main/java/galaxy_repository/v1/GalaxyRepositoryOuterClass.java new file mode 100644 index 0000000..5fc4aa6 --- /dev/null +++ b/clients/java/src/main/generated/main/java/galaxy_repository/v1/GalaxyRepositoryOuterClass.java @@ -0,0 +1,8077 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// NO CHECKED-IN PROTOBUF GENCODE +// source: galaxy_repository.proto +// Protobuf Java Version: 4.33.1 + +package galaxy_repository.v1; + +@com.google.protobuf.Generated +public final class GalaxyRepositoryOuterClass extends com.google.protobuf.GeneratedFile { + private GalaxyRepositoryOuterClass() {} + static { + com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( + com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, + /* major= */ 4, + /* minor= */ 33, + /* patch= */ 1, + /* suffix= */ "", + "GalaxyRepositoryOuterClass"); + } + public static void registerAllExtensions( + com.google.protobuf.ExtensionRegistryLite registry) { + } + + public static void registerAllExtensions( + com.google.protobuf.ExtensionRegistry registry) { + registerAllExtensions( + (com.google.protobuf.ExtensionRegistryLite) registry); + } + public interface TestConnectionRequestOrBuilder extends + // @@protoc_insertion_point(interface_extends:galaxy_repository.v1.TestConnectionRequest) + com.google.protobuf.MessageOrBuilder { + } + /** + * Protobuf type {@code galaxy_repository.v1.TestConnectionRequest} + */ + public static final class TestConnectionRequest extends + com.google.protobuf.GeneratedMessage implements + // @@protoc_insertion_point(message_implements:galaxy_repository.v1.TestConnectionRequest) + TestConnectionRequestOrBuilder { + private static final long serialVersionUID = 0L; + static { + com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( + com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, + /* major= */ 4, + /* minor= */ 33, + /* patch= */ 1, + /* suffix= */ "", + "TestConnectionRequest"); + } + // Use TestConnectionRequest.newBuilder() to construct. + private TestConnectionRequest(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + } + private TestConnectionRequest() { + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_TestConnectionRequest_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_TestConnectionRequest_fieldAccessorTable + .ensureFieldAccessorsInitialized( + galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest.class, galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest.Builder.class); + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getUnknownFields().writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + size += getUnknownFields().getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest)) { + return super.equals(obj); + } + galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest other = (galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest) obj; + + if (!getUnknownFields().equals(other.getUnknownFields())) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + hash = (29 * hash) + getUnknownFields().hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code galaxy_repository.v1.TestConnectionRequest} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder implements + // @@protoc_insertion_point(builder_implements:galaxy_repository.v1.TestConnectionRequest) + galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequestOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_TestConnectionRequest_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_TestConnectionRequest_fieldAccessorTable + .ensureFieldAccessorsInitialized( + galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest.class, galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest.Builder.class); + } + + // Construct using galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest.newBuilder() + private Builder() { + + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + + } + @java.lang.Override + public Builder clear() { + super.clear(); + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_TestConnectionRequest_descriptor; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest getDefaultInstanceForType() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest.getDefaultInstance(); + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest build() { + galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest buildPartial() { + galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest result = new galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest(this); + onBuilt(); + return result; + } + + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest) { + return mergeFrom((galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest other) { + if (other == galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest.getDefaultInstance()) return this; + this.mergeUnknownFields(other.getUnknownFields()); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + default: { + if (!super.parseUnknownField(input, extensionRegistry, tag)) { + done = true; // was an endgroup tag + } + break; + } // default: + } // switch (tag) + } // while (!done) + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.unwrapIOException(); + } finally { + onChanged(); + } // finally + return this; + } + + // @@protoc_insertion_point(builder_scope:galaxy_repository.v1.TestConnectionRequest) + } + + // @@protoc_insertion_point(class_scope:galaxy_repository.v1.TestConnectionRequest) + private static final galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest(); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public TestConnectionRequest parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + Builder builder = newBuilder(); + try { + builder.mergeFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(builder.buildPartial()); + } catch (com.google.protobuf.UninitializedMessageException e) { + throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException(e) + .setUnfinishedMessage(builder.buildPartial()); + } + return builder.buildPartial(); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + + } + + public interface TestConnectionReplyOrBuilder extends + // @@protoc_insertion_point(interface_extends:galaxy_repository.v1.TestConnectionReply) + com.google.protobuf.MessageOrBuilder { + + /** + * bool ok = 1; + * @return The ok. + */ + boolean getOk(); + } + /** + * Protobuf type {@code galaxy_repository.v1.TestConnectionReply} + */ + public static final class TestConnectionReply extends + com.google.protobuf.GeneratedMessage implements + // @@protoc_insertion_point(message_implements:galaxy_repository.v1.TestConnectionReply) + TestConnectionReplyOrBuilder { + private static final long serialVersionUID = 0L; + static { + com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( + com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, + /* major= */ 4, + /* minor= */ 33, + /* patch= */ 1, + /* suffix= */ "", + "TestConnectionReply"); + } + // Use TestConnectionReply.newBuilder() to construct. + private TestConnectionReply(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + } + private TestConnectionReply() { + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_TestConnectionReply_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_TestConnectionReply_fieldAccessorTable + .ensureFieldAccessorsInitialized( + galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply.class, galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply.Builder.class); + } + + public static final int OK_FIELD_NUMBER = 1; + private boolean ok_ = false; + /** + * bool ok = 1; + * @return The ok. + */ + @java.lang.Override + public boolean getOk() { + return ok_; + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + if (ok_ != false) { + output.writeBool(1, ok_); + } + getUnknownFields().writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + if (ok_ != false) { + size += com.google.protobuf.CodedOutputStream + .computeBoolSize(1, ok_); + } + size += getUnknownFields().getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply)) { + return super.equals(obj); + } + galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply other = (galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply) obj; + + if (getOk() + != other.getOk()) return false; + if (!getUnknownFields().equals(other.getUnknownFields())) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + hash = (37 * hash) + OK_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashBoolean( + getOk()); + hash = (29 * hash) + getUnknownFields().hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code galaxy_repository.v1.TestConnectionReply} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder implements + // @@protoc_insertion_point(builder_implements:galaxy_repository.v1.TestConnectionReply) + galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReplyOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_TestConnectionReply_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_TestConnectionReply_fieldAccessorTable + .ensureFieldAccessorsInitialized( + galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply.class, galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply.Builder.class); + } + + // Construct using galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply.newBuilder() + private Builder() { + + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + + } + @java.lang.Override + public Builder clear() { + super.clear(); + bitField0_ = 0; + ok_ = false; + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_TestConnectionReply_descriptor; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply getDefaultInstanceForType() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply.getDefaultInstance(); + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply build() { + galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply buildPartial() { + galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply result = new galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply(this); + if (bitField0_ != 0) { buildPartial0(result); } + onBuilt(); + return result; + } + + private void buildPartial0(galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply result) { + int from_bitField0_ = bitField0_; + if (((from_bitField0_ & 0x00000001) != 0)) { + result.ok_ = ok_; + } + } + + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply) { + return mergeFrom((galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply other) { + if (other == galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply.getDefaultInstance()) return this; + if (other.getOk() != false) { + setOk(other.getOk()); + } + this.mergeUnknownFields(other.getUnknownFields()); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 8: { + ok_ = input.readBool(); + bitField0_ |= 0x00000001; + break; + } // case 8 + default: { + if (!super.parseUnknownField(input, extensionRegistry, tag)) { + done = true; // was an endgroup tag + } + break; + } // default: + } // switch (tag) + } // while (!done) + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.unwrapIOException(); + } finally { + onChanged(); + } // finally + return this; + } + private int bitField0_; + + private boolean ok_ ; + /** + * bool ok = 1; + * @return The ok. + */ + @java.lang.Override + public boolean getOk() { + return ok_; + } + /** + * bool ok = 1; + * @param value The ok to set. + * @return This builder for chaining. + */ + public Builder setOk(boolean value) { + + ok_ = value; + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + /** + * bool ok = 1; + * @return This builder for chaining. + */ + public Builder clearOk() { + bitField0_ = (bitField0_ & ~0x00000001); + ok_ = false; + onChanged(); + return this; + } + + // @@protoc_insertion_point(builder_scope:galaxy_repository.v1.TestConnectionReply) + } + + // @@protoc_insertion_point(class_scope:galaxy_repository.v1.TestConnectionReply) + private static final galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply(); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public TestConnectionReply parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + Builder builder = newBuilder(); + try { + builder.mergeFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(builder.buildPartial()); + } catch (com.google.protobuf.UninitializedMessageException e) { + throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException(e) + .setUnfinishedMessage(builder.buildPartial()); + } + return builder.buildPartial(); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + + } + + public interface GetLastDeployTimeRequestOrBuilder extends + // @@protoc_insertion_point(interface_extends:galaxy_repository.v1.GetLastDeployTimeRequest) + com.google.protobuf.MessageOrBuilder { + } + /** + * Protobuf type {@code galaxy_repository.v1.GetLastDeployTimeRequest} + */ + public static final class GetLastDeployTimeRequest extends + com.google.protobuf.GeneratedMessage implements + // @@protoc_insertion_point(message_implements:galaxy_repository.v1.GetLastDeployTimeRequest) + GetLastDeployTimeRequestOrBuilder { + private static final long serialVersionUID = 0L; + static { + com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( + com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, + /* major= */ 4, + /* minor= */ 33, + /* patch= */ 1, + /* suffix= */ "", + "GetLastDeployTimeRequest"); + } + // Use GetLastDeployTimeRequest.newBuilder() to construct. + private GetLastDeployTimeRequest(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + } + private GetLastDeployTimeRequest() { + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_GetLastDeployTimeRequest_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_GetLastDeployTimeRequest_fieldAccessorTable + .ensureFieldAccessorsInitialized( + galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest.class, galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest.Builder.class); + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getUnknownFields().writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + size += getUnknownFields().getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest)) { + return super.equals(obj); + } + galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest other = (galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest) obj; + + if (!getUnknownFields().equals(other.getUnknownFields())) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + hash = (29 * hash) + getUnknownFields().hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code galaxy_repository.v1.GetLastDeployTimeRequest} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder implements + // @@protoc_insertion_point(builder_implements:galaxy_repository.v1.GetLastDeployTimeRequest) + galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequestOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_GetLastDeployTimeRequest_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_GetLastDeployTimeRequest_fieldAccessorTable + .ensureFieldAccessorsInitialized( + galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest.class, galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest.Builder.class); + } + + // Construct using galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest.newBuilder() + private Builder() { + + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + + } + @java.lang.Override + public Builder clear() { + super.clear(); + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_GetLastDeployTimeRequest_descriptor; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest getDefaultInstanceForType() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest.getDefaultInstance(); + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest build() { + galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest buildPartial() { + galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest result = new galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest(this); + onBuilt(); + return result; + } + + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest) { + return mergeFrom((galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest other) { + if (other == galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest.getDefaultInstance()) return this; + this.mergeUnknownFields(other.getUnknownFields()); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + default: { + if (!super.parseUnknownField(input, extensionRegistry, tag)) { + done = true; // was an endgroup tag + } + break; + } // default: + } // switch (tag) + } // while (!done) + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.unwrapIOException(); + } finally { + onChanged(); + } // finally + return this; + } + + // @@protoc_insertion_point(builder_scope:galaxy_repository.v1.GetLastDeployTimeRequest) + } + + // @@protoc_insertion_point(class_scope:galaxy_repository.v1.GetLastDeployTimeRequest) + private static final galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest(); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public GetLastDeployTimeRequest parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + Builder builder = newBuilder(); + try { + builder.mergeFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(builder.buildPartial()); + } catch (com.google.protobuf.UninitializedMessageException e) { + throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException(e) + .setUnfinishedMessage(builder.buildPartial()); + } + return builder.buildPartial(); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + + } + + public interface GetLastDeployTimeReplyOrBuilder extends + // @@protoc_insertion_point(interface_extends:galaxy_repository.v1.GetLastDeployTimeReply) + com.google.protobuf.MessageOrBuilder { + + /** + * bool present = 1; + * @return The present. + */ + boolean getPresent(); + + /** + * .google.protobuf.Timestamp time_of_last_deploy = 2; + * @return Whether the timeOfLastDeploy field is set. + */ + boolean hasTimeOfLastDeploy(); + /** + * .google.protobuf.Timestamp time_of_last_deploy = 2; + * @return The timeOfLastDeploy. + */ + com.google.protobuf.Timestamp getTimeOfLastDeploy(); + /** + * .google.protobuf.Timestamp time_of_last_deploy = 2; + */ + com.google.protobuf.TimestampOrBuilder getTimeOfLastDeployOrBuilder(); + } + /** + * Protobuf type {@code galaxy_repository.v1.GetLastDeployTimeReply} + */ + public static final class GetLastDeployTimeReply extends + com.google.protobuf.GeneratedMessage implements + // @@protoc_insertion_point(message_implements:galaxy_repository.v1.GetLastDeployTimeReply) + GetLastDeployTimeReplyOrBuilder { + private static final long serialVersionUID = 0L; + static { + com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( + com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, + /* major= */ 4, + /* minor= */ 33, + /* patch= */ 1, + /* suffix= */ "", + "GetLastDeployTimeReply"); + } + // Use GetLastDeployTimeReply.newBuilder() to construct. + private GetLastDeployTimeReply(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + } + private GetLastDeployTimeReply() { + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_GetLastDeployTimeReply_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_GetLastDeployTimeReply_fieldAccessorTable + .ensureFieldAccessorsInitialized( + galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply.class, galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply.Builder.class); + } + + private int bitField0_; + public static final int PRESENT_FIELD_NUMBER = 1; + private boolean present_ = false; + /** + * bool present = 1; + * @return The present. + */ + @java.lang.Override + public boolean getPresent() { + return present_; + } + + public static final int TIME_OF_LAST_DEPLOY_FIELD_NUMBER = 2; + private com.google.protobuf.Timestamp timeOfLastDeploy_; + /** + * .google.protobuf.Timestamp time_of_last_deploy = 2; + * @return Whether the timeOfLastDeploy field is set. + */ + @java.lang.Override + public boolean hasTimeOfLastDeploy() { + return ((bitField0_ & 0x00000001) != 0); + } + /** + * .google.protobuf.Timestamp time_of_last_deploy = 2; + * @return The timeOfLastDeploy. + */ + @java.lang.Override + public com.google.protobuf.Timestamp getTimeOfLastDeploy() { + return timeOfLastDeploy_ == null ? com.google.protobuf.Timestamp.getDefaultInstance() : timeOfLastDeploy_; + } + /** + * .google.protobuf.Timestamp time_of_last_deploy = 2; + */ + @java.lang.Override + public com.google.protobuf.TimestampOrBuilder getTimeOfLastDeployOrBuilder() { + return timeOfLastDeploy_ == null ? com.google.protobuf.Timestamp.getDefaultInstance() : timeOfLastDeploy_; + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + if (present_ != false) { + output.writeBool(1, present_); + } + if (((bitField0_ & 0x00000001) != 0)) { + output.writeMessage(2, getTimeOfLastDeploy()); + } + getUnknownFields().writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + if (present_ != false) { + size += com.google.protobuf.CodedOutputStream + .computeBoolSize(1, present_); + } + if (((bitField0_ & 0x00000001) != 0)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(2, getTimeOfLastDeploy()); + } + size += getUnknownFields().getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply)) { + return super.equals(obj); + } + galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply other = (galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply) obj; + + if (getPresent() + != other.getPresent()) return false; + if (hasTimeOfLastDeploy() != other.hasTimeOfLastDeploy()) return false; + if (hasTimeOfLastDeploy()) { + if (!getTimeOfLastDeploy() + .equals(other.getTimeOfLastDeploy())) return false; + } + if (!getUnknownFields().equals(other.getUnknownFields())) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + hash = (37 * hash) + PRESENT_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashBoolean( + getPresent()); + if (hasTimeOfLastDeploy()) { + hash = (37 * hash) + TIME_OF_LAST_DEPLOY_FIELD_NUMBER; + hash = (53 * hash) + getTimeOfLastDeploy().hashCode(); + } + hash = (29 * hash) + getUnknownFields().hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code galaxy_repository.v1.GetLastDeployTimeReply} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder implements + // @@protoc_insertion_point(builder_implements:galaxy_repository.v1.GetLastDeployTimeReply) + galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReplyOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_GetLastDeployTimeReply_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_GetLastDeployTimeReply_fieldAccessorTable + .ensureFieldAccessorsInitialized( + galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply.class, galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply.Builder.class); + } + + // Construct using galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessage + .alwaysUseFieldBuilders) { + internalGetTimeOfLastDeployFieldBuilder(); + } + } + @java.lang.Override + public Builder clear() { + super.clear(); + bitField0_ = 0; + present_ = false; + timeOfLastDeploy_ = null; + if (timeOfLastDeployBuilder_ != null) { + timeOfLastDeployBuilder_.dispose(); + timeOfLastDeployBuilder_ = null; + } + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_GetLastDeployTimeReply_descriptor; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply getDefaultInstanceForType() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply.getDefaultInstance(); + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply build() { + galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply buildPartial() { + galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply result = new galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply(this); + if (bitField0_ != 0) { buildPartial0(result); } + onBuilt(); + return result; + } + + private void buildPartial0(galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply result) { + int from_bitField0_ = bitField0_; + if (((from_bitField0_ & 0x00000001) != 0)) { + result.present_ = present_; + } + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000002) != 0)) { + result.timeOfLastDeploy_ = timeOfLastDeployBuilder_ == null + ? timeOfLastDeploy_ + : timeOfLastDeployBuilder_.build(); + to_bitField0_ |= 0x00000001; + } + result.bitField0_ |= to_bitField0_; + } + + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply) { + return mergeFrom((galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply other) { + if (other == galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply.getDefaultInstance()) return this; + if (other.getPresent() != false) { + setPresent(other.getPresent()); + } + if (other.hasTimeOfLastDeploy()) { + mergeTimeOfLastDeploy(other.getTimeOfLastDeploy()); + } + this.mergeUnknownFields(other.getUnknownFields()); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 8: { + present_ = input.readBool(); + bitField0_ |= 0x00000001; + break; + } // case 8 + case 18: { + input.readMessage( + internalGetTimeOfLastDeployFieldBuilder().getBuilder(), + extensionRegistry); + bitField0_ |= 0x00000002; + break; + } // case 18 + default: { + if (!super.parseUnknownField(input, extensionRegistry, tag)) { + done = true; // was an endgroup tag + } + break; + } // default: + } // switch (tag) + } // while (!done) + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.unwrapIOException(); + } finally { + onChanged(); + } // finally + return this; + } + private int bitField0_; + + private boolean present_ ; + /** + * bool present = 1; + * @return The present. + */ + @java.lang.Override + public boolean getPresent() { + return present_; + } + /** + * bool present = 1; + * @param value The present to set. + * @return This builder for chaining. + */ + public Builder setPresent(boolean value) { + + present_ = value; + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + /** + * bool present = 1; + * @return This builder for chaining. + */ + public Builder clearPresent() { + bitField0_ = (bitField0_ & ~0x00000001); + present_ = false; + onChanged(); + return this; + } + + private com.google.protobuf.Timestamp timeOfLastDeploy_; + private com.google.protobuf.SingleFieldBuilder< + com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder> timeOfLastDeployBuilder_; + /** + * .google.protobuf.Timestamp time_of_last_deploy = 2; + * @return Whether the timeOfLastDeploy field is set. + */ + public boolean hasTimeOfLastDeploy() { + return ((bitField0_ & 0x00000002) != 0); + } + /** + * .google.protobuf.Timestamp time_of_last_deploy = 2; + * @return The timeOfLastDeploy. + */ + public com.google.protobuf.Timestamp getTimeOfLastDeploy() { + if (timeOfLastDeployBuilder_ == null) { + return timeOfLastDeploy_ == null ? com.google.protobuf.Timestamp.getDefaultInstance() : timeOfLastDeploy_; + } else { + return timeOfLastDeployBuilder_.getMessage(); + } + } + /** + * .google.protobuf.Timestamp time_of_last_deploy = 2; + */ + public Builder setTimeOfLastDeploy(com.google.protobuf.Timestamp value) { + if (timeOfLastDeployBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + timeOfLastDeploy_ = value; + } else { + timeOfLastDeployBuilder_.setMessage(value); + } + bitField0_ |= 0x00000002; + onChanged(); + return this; + } + /** + * .google.protobuf.Timestamp time_of_last_deploy = 2; + */ + public Builder setTimeOfLastDeploy( + com.google.protobuf.Timestamp.Builder builderForValue) { + if (timeOfLastDeployBuilder_ == null) { + timeOfLastDeploy_ = builderForValue.build(); + } else { + timeOfLastDeployBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000002; + onChanged(); + return this; + } + /** + * .google.protobuf.Timestamp time_of_last_deploy = 2; + */ + public Builder mergeTimeOfLastDeploy(com.google.protobuf.Timestamp value) { + if (timeOfLastDeployBuilder_ == null) { + if (((bitField0_ & 0x00000002) != 0) && + timeOfLastDeploy_ != null && + timeOfLastDeploy_ != com.google.protobuf.Timestamp.getDefaultInstance()) { + getTimeOfLastDeployBuilder().mergeFrom(value); + } else { + timeOfLastDeploy_ = value; + } + } else { + timeOfLastDeployBuilder_.mergeFrom(value); + } + if (timeOfLastDeploy_ != null) { + bitField0_ |= 0x00000002; + onChanged(); + } + return this; + } + /** + * .google.protobuf.Timestamp time_of_last_deploy = 2; + */ + public Builder clearTimeOfLastDeploy() { + bitField0_ = (bitField0_ & ~0x00000002); + timeOfLastDeploy_ = null; + if (timeOfLastDeployBuilder_ != null) { + timeOfLastDeployBuilder_.dispose(); + timeOfLastDeployBuilder_ = null; + } + onChanged(); + return this; + } + /** + * .google.protobuf.Timestamp time_of_last_deploy = 2; + */ + public com.google.protobuf.Timestamp.Builder getTimeOfLastDeployBuilder() { + bitField0_ |= 0x00000002; + onChanged(); + return internalGetTimeOfLastDeployFieldBuilder().getBuilder(); + } + /** + * .google.protobuf.Timestamp time_of_last_deploy = 2; + */ + public com.google.protobuf.TimestampOrBuilder getTimeOfLastDeployOrBuilder() { + if (timeOfLastDeployBuilder_ != null) { + return timeOfLastDeployBuilder_.getMessageOrBuilder(); + } else { + return timeOfLastDeploy_ == null ? + com.google.protobuf.Timestamp.getDefaultInstance() : timeOfLastDeploy_; + } + } + /** + * .google.protobuf.Timestamp time_of_last_deploy = 2; + */ + private com.google.protobuf.SingleFieldBuilder< + com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder> + internalGetTimeOfLastDeployFieldBuilder() { + if (timeOfLastDeployBuilder_ == null) { + timeOfLastDeployBuilder_ = new com.google.protobuf.SingleFieldBuilder< + com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder>( + getTimeOfLastDeploy(), + getParentForChildren(), + isClean()); + timeOfLastDeploy_ = null; + } + return timeOfLastDeployBuilder_; + } + + // @@protoc_insertion_point(builder_scope:galaxy_repository.v1.GetLastDeployTimeReply) + } + + // @@protoc_insertion_point(class_scope:galaxy_repository.v1.GetLastDeployTimeReply) + private static final galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply(); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public GetLastDeployTimeReply parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + Builder builder = newBuilder(); + try { + builder.mergeFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(builder.buildPartial()); + } catch (com.google.protobuf.UninitializedMessageException e) { + throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException(e) + .setUnfinishedMessage(builder.buildPartial()); + } + return builder.buildPartial(); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + + } + + public interface DiscoverHierarchyRequestOrBuilder extends + // @@protoc_insertion_point(interface_extends:galaxy_repository.v1.DiscoverHierarchyRequest) + com.google.protobuf.MessageOrBuilder { + } + /** + * Protobuf type {@code galaxy_repository.v1.DiscoverHierarchyRequest} + */ + public static final class DiscoverHierarchyRequest extends + com.google.protobuf.GeneratedMessage implements + // @@protoc_insertion_point(message_implements:galaxy_repository.v1.DiscoverHierarchyRequest) + DiscoverHierarchyRequestOrBuilder { + private static final long serialVersionUID = 0L; + static { + com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( + com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, + /* major= */ 4, + /* minor= */ 33, + /* patch= */ 1, + /* suffix= */ "", + "DiscoverHierarchyRequest"); + } + // Use DiscoverHierarchyRequest.newBuilder() to construct. + private DiscoverHierarchyRequest(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + } + private DiscoverHierarchyRequest() { + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_DiscoverHierarchyRequest_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_DiscoverHierarchyRequest_fieldAccessorTable + .ensureFieldAccessorsInitialized( + galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest.class, galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest.Builder.class); + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getUnknownFields().writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + size += getUnknownFields().getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest)) { + return super.equals(obj); + } + galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest other = (galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest) obj; + + if (!getUnknownFields().equals(other.getUnknownFields())) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + hash = (29 * hash) + getUnknownFields().hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code galaxy_repository.v1.DiscoverHierarchyRequest} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder implements + // @@protoc_insertion_point(builder_implements:galaxy_repository.v1.DiscoverHierarchyRequest) + galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequestOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_DiscoverHierarchyRequest_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_DiscoverHierarchyRequest_fieldAccessorTable + .ensureFieldAccessorsInitialized( + galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest.class, galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest.Builder.class); + } + + // Construct using galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest.newBuilder() + private Builder() { + + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + + } + @java.lang.Override + public Builder clear() { + super.clear(); + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_DiscoverHierarchyRequest_descriptor; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest getDefaultInstanceForType() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest.getDefaultInstance(); + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest build() { + galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest buildPartial() { + galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest result = new galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest(this); + onBuilt(); + return result; + } + + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest) { + return mergeFrom((galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest other) { + if (other == galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest.getDefaultInstance()) return this; + this.mergeUnknownFields(other.getUnknownFields()); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + default: { + if (!super.parseUnknownField(input, extensionRegistry, tag)) { + done = true; // was an endgroup tag + } + break; + } // default: + } // switch (tag) + } // while (!done) + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.unwrapIOException(); + } finally { + onChanged(); + } // finally + return this; + } + + // @@protoc_insertion_point(builder_scope:galaxy_repository.v1.DiscoverHierarchyRequest) + } + + // @@protoc_insertion_point(class_scope:galaxy_repository.v1.DiscoverHierarchyRequest) + private static final galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest(); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public DiscoverHierarchyRequest parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + Builder builder = newBuilder(); + try { + builder.mergeFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(builder.buildPartial()); + } catch (com.google.protobuf.UninitializedMessageException e) { + throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException(e) + .setUnfinishedMessage(builder.buildPartial()); + } + return builder.buildPartial(); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + + } + + public interface DiscoverHierarchyReplyOrBuilder extends + // @@protoc_insertion_point(interface_extends:galaxy_repository.v1.DiscoverHierarchyReply) + com.google.protobuf.MessageOrBuilder { + + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + java.util.List + getObjectsList(); + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject getObjects(int index); + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + int getObjectsCount(); + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + java.util.List + getObjectsOrBuilderList(); + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObjectOrBuilder getObjectsOrBuilder( + int index); + } + /** + * Protobuf type {@code galaxy_repository.v1.DiscoverHierarchyReply} + */ + public static final class DiscoverHierarchyReply extends + com.google.protobuf.GeneratedMessage implements + // @@protoc_insertion_point(message_implements:galaxy_repository.v1.DiscoverHierarchyReply) + DiscoverHierarchyReplyOrBuilder { + private static final long serialVersionUID = 0L; + static { + com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( + com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, + /* major= */ 4, + /* minor= */ 33, + /* patch= */ 1, + /* suffix= */ "", + "DiscoverHierarchyReply"); + } + // Use DiscoverHierarchyReply.newBuilder() to construct. + private DiscoverHierarchyReply(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + } + private DiscoverHierarchyReply() { + objects_ = java.util.Collections.emptyList(); + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_DiscoverHierarchyReply_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_DiscoverHierarchyReply_fieldAccessorTable + .ensureFieldAccessorsInitialized( + galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply.class, galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply.Builder.class); + } + + public static final int OBJECTS_FIELD_NUMBER = 1; + @SuppressWarnings("serial") + private java.util.List objects_; + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + @java.lang.Override + public java.util.List getObjectsList() { + return objects_; + } + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + @java.lang.Override + public java.util.List + getObjectsOrBuilderList() { + return objects_; + } + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + @java.lang.Override + public int getObjectsCount() { + return objects_.size(); + } + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject getObjects(int index) { + return objects_.get(index); + } + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObjectOrBuilder getObjectsOrBuilder( + int index) { + return objects_.get(index); + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + for (int i = 0; i < objects_.size(); i++) { + output.writeMessage(1, objects_.get(i)); + } + getUnknownFields().writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + for (int i = 0; i < objects_.size(); i++) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(1, objects_.get(i)); + } + size += getUnknownFields().getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply)) { + return super.equals(obj); + } + galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply other = (galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply) obj; + + if (!getObjectsList() + .equals(other.getObjectsList())) return false; + if (!getUnknownFields().equals(other.getUnknownFields())) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + if (getObjectsCount() > 0) { + hash = (37 * hash) + OBJECTS_FIELD_NUMBER; + hash = (53 * hash) + getObjectsList().hashCode(); + } + hash = (29 * hash) + getUnknownFields().hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code galaxy_repository.v1.DiscoverHierarchyReply} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder implements + // @@protoc_insertion_point(builder_implements:galaxy_repository.v1.DiscoverHierarchyReply) + galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReplyOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_DiscoverHierarchyReply_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_DiscoverHierarchyReply_fieldAccessorTable + .ensureFieldAccessorsInitialized( + galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply.class, galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply.Builder.class); + } + + // Construct using galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply.newBuilder() + private Builder() { + + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + + } + @java.lang.Override + public Builder clear() { + super.clear(); + bitField0_ = 0; + if (objectsBuilder_ == null) { + objects_ = java.util.Collections.emptyList(); + } else { + objects_ = null; + objectsBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000001); + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_DiscoverHierarchyReply_descriptor; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply getDefaultInstanceForType() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply.getDefaultInstance(); + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply build() { + galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply buildPartial() { + galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply result = new galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply(this); + buildPartialRepeatedFields(result); + if (bitField0_ != 0) { buildPartial0(result); } + onBuilt(); + return result; + } + + private void buildPartialRepeatedFields(galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply result) { + if (objectsBuilder_ == null) { + if (((bitField0_ & 0x00000001) != 0)) { + objects_ = java.util.Collections.unmodifiableList(objects_); + bitField0_ = (bitField0_ & ~0x00000001); + } + result.objects_ = objects_; + } else { + result.objects_ = objectsBuilder_.build(); + } + } + + private void buildPartial0(galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply result) { + int from_bitField0_ = bitField0_; + } + + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply) { + return mergeFrom((galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply other) { + if (other == galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply.getDefaultInstance()) return this; + if (objectsBuilder_ == null) { + if (!other.objects_.isEmpty()) { + if (objects_.isEmpty()) { + objects_ = other.objects_; + bitField0_ = (bitField0_ & ~0x00000001); + } else { + ensureObjectsIsMutable(); + objects_.addAll(other.objects_); + } + onChanged(); + } + } else { + if (!other.objects_.isEmpty()) { + if (objectsBuilder_.isEmpty()) { + objectsBuilder_.dispose(); + objectsBuilder_ = null; + objects_ = other.objects_; + bitField0_ = (bitField0_ & ~0x00000001); + objectsBuilder_ = + com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ? + internalGetObjectsFieldBuilder() : null; + } else { + objectsBuilder_.addAllMessages(other.objects_); + } + } + } + this.mergeUnknownFields(other.getUnknownFields()); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 10: { + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject m = + input.readMessage( + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.parser(), + extensionRegistry); + if (objectsBuilder_ == null) { + ensureObjectsIsMutable(); + objects_.add(m); + } else { + objectsBuilder_.addMessage(m); + } + break; + } // case 10 + default: { + if (!super.parseUnknownField(input, extensionRegistry, tag)) { + done = true; // was an endgroup tag + } + break; + } // default: + } // switch (tag) + } // while (!done) + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.unwrapIOException(); + } finally { + onChanged(); + } // finally + return this; + } + private int bitField0_; + + private java.util.List objects_ = + java.util.Collections.emptyList(); + private void ensureObjectsIsMutable() { + if (!((bitField0_ & 0x00000001) != 0)) { + objects_ = new java.util.ArrayList(objects_); + bitField0_ |= 0x00000001; + } + } + + private com.google.protobuf.RepeatedFieldBuilder< + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.Builder, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObjectOrBuilder> objectsBuilder_; + + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + public java.util.List getObjectsList() { + if (objectsBuilder_ == null) { + return java.util.Collections.unmodifiableList(objects_); + } else { + return objectsBuilder_.getMessageList(); + } + } + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + public int getObjectsCount() { + if (objectsBuilder_ == null) { + return objects_.size(); + } else { + return objectsBuilder_.getCount(); + } + } + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject getObjects(int index) { + if (objectsBuilder_ == null) { + return objects_.get(index); + } else { + return objectsBuilder_.getMessage(index); + } + } + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + public Builder setObjects( + int index, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject value) { + if (objectsBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureObjectsIsMutable(); + objects_.set(index, value); + onChanged(); + } else { + objectsBuilder_.setMessage(index, value); + } + return this; + } + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + public Builder setObjects( + int index, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.Builder builderForValue) { + if (objectsBuilder_ == null) { + ensureObjectsIsMutable(); + objects_.set(index, builderForValue.build()); + onChanged(); + } else { + objectsBuilder_.setMessage(index, builderForValue.build()); + } + return this; + } + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + public Builder addObjects(galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject value) { + if (objectsBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureObjectsIsMutable(); + objects_.add(value); + onChanged(); + } else { + objectsBuilder_.addMessage(value); + } + return this; + } + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + public Builder addObjects( + int index, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject value) { + if (objectsBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureObjectsIsMutable(); + objects_.add(index, value); + onChanged(); + } else { + objectsBuilder_.addMessage(index, value); + } + return this; + } + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + public Builder addObjects( + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.Builder builderForValue) { + if (objectsBuilder_ == null) { + ensureObjectsIsMutable(); + objects_.add(builderForValue.build()); + onChanged(); + } else { + objectsBuilder_.addMessage(builderForValue.build()); + } + return this; + } + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + public Builder addObjects( + int index, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.Builder builderForValue) { + if (objectsBuilder_ == null) { + ensureObjectsIsMutable(); + objects_.add(index, builderForValue.build()); + onChanged(); + } else { + objectsBuilder_.addMessage(index, builderForValue.build()); + } + return this; + } + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + public Builder addAllObjects( + java.lang.Iterable values) { + if (objectsBuilder_ == null) { + ensureObjectsIsMutable(); + com.google.protobuf.AbstractMessageLite.Builder.addAll( + values, objects_); + onChanged(); + } else { + objectsBuilder_.addAllMessages(values); + } + return this; + } + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + public Builder clearObjects() { + if (objectsBuilder_ == null) { + objects_ = java.util.Collections.emptyList(); + bitField0_ = (bitField0_ & ~0x00000001); + onChanged(); + } else { + objectsBuilder_.clear(); + } + return this; + } + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + public Builder removeObjects(int index) { + if (objectsBuilder_ == null) { + ensureObjectsIsMutable(); + objects_.remove(index); + onChanged(); + } else { + objectsBuilder_.remove(index); + } + return this; + } + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.Builder getObjectsBuilder( + int index) { + return internalGetObjectsFieldBuilder().getBuilder(index); + } + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObjectOrBuilder getObjectsOrBuilder( + int index) { + if (objectsBuilder_ == null) { + return objects_.get(index); } else { + return objectsBuilder_.getMessageOrBuilder(index); + } + } + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + public java.util.List + getObjectsOrBuilderList() { + if (objectsBuilder_ != null) { + return objectsBuilder_.getMessageOrBuilderList(); + } else { + return java.util.Collections.unmodifiableList(objects_); + } + } + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.Builder addObjectsBuilder() { + return internalGetObjectsFieldBuilder().addBuilder( + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.getDefaultInstance()); + } + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.Builder addObjectsBuilder( + int index) { + return internalGetObjectsFieldBuilder().addBuilder( + index, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.getDefaultInstance()); + } + /** + * repeated .galaxy_repository.v1.GalaxyObject objects = 1; + */ + public java.util.List + getObjectsBuilderList() { + return internalGetObjectsFieldBuilder().getBuilderList(); + } + private com.google.protobuf.RepeatedFieldBuilder< + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.Builder, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObjectOrBuilder> + internalGetObjectsFieldBuilder() { + if (objectsBuilder_ == null) { + objectsBuilder_ = new com.google.protobuf.RepeatedFieldBuilder< + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.Builder, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObjectOrBuilder>( + objects_, + ((bitField0_ & 0x00000001) != 0), + getParentForChildren(), + isClean()); + objects_ = null; + } + return objectsBuilder_; + } + + // @@protoc_insertion_point(builder_scope:galaxy_repository.v1.DiscoverHierarchyReply) + } + + // @@protoc_insertion_point(class_scope:galaxy_repository.v1.DiscoverHierarchyReply) + private static final galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply(); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public DiscoverHierarchyReply parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + Builder builder = newBuilder(); + try { + builder.mergeFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(builder.buildPartial()); + } catch (com.google.protobuf.UninitializedMessageException e) { + throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException(e) + .setUnfinishedMessage(builder.buildPartial()); + } + return builder.buildPartial(); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + + } + + public interface WatchDeployEventsRequestOrBuilder extends + // @@protoc_insertion_point(interface_extends:galaxy_repository.v1.WatchDeployEventsRequest) + com.google.protobuf.MessageOrBuilder { + + /** + *
+     * Optional. When set, the bootstrap event is suppressed if the cached deploy
+     * time matches this value. Future events are still emitted normally.
+     * 
+ * + * .google.protobuf.Timestamp last_seen_deploy_time = 1; + * @return Whether the lastSeenDeployTime field is set. + */ + boolean hasLastSeenDeployTime(); + /** + *
+     * Optional. When set, the bootstrap event is suppressed if the cached deploy
+     * time matches this value. Future events are still emitted normally.
+     * 
+ * + * .google.protobuf.Timestamp last_seen_deploy_time = 1; + * @return The lastSeenDeployTime. + */ + com.google.protobuf.Timestamp getLastSeenDeployTime(); + /** + *
+     * Optional. When set, the bootstrap event is suppressed if the cached deploy
+     * time matches this value. Future events are still emitted normally.
+     * 
+ * + * .google.protobuf.Timestamp last_seen_deploy_time = 1; + */ + com.google.protobuf.TimestampOrBuilder getLastSeenDeployTimeOrBuilder(); + } + /** + * Protobuf type {@code galaxy_repository.v1.WatchDeployEventsRequest} + */ + public static final class WatchDeployEventsRequest extends + com.google.protobuf.GeneratedMessage implements + // @@protoc_insertion_point(message_implements:galaxy_repository.v1.WatchDeployEventsRequest) + WatchDeployEventsRequestOrBuilder { + private static final long serialVersionUID = 0L; + static { + com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( + com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, + /* major= */ 4, + /* minor= */ 33, + /* patch= */ 1, + /* suffix= */ "", + "WatchDeployEventsRequest"); + } + // Use WatchDeployEventsRequest.newBuilder() to construct. + private WatchDeployEventsRequest(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + } + private WatchDeployEventsRequest() { + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_WatchDeployEventsRequest_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_WatchDeployEventsRequest_fieldAccessorTable + .ensureFieldAccessorsInitialized( + galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest.class, galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest.Builder.class); + } + + private int bitField0_; + public static final int LAST_SEEN_DEPLOY_TIME_FIELD_NUMBER = 1; + private com.google.protobuf.Timestamp lastSeenDeployTime_; + /** + *
+     * Optional. When set, the bootstrap event is suppressed if the cached deploy
+     * time matches this value. Future events are still emitted normally.
+     * 
+ * + * .google.protobuf.Timestamp last_seen_deploy_time = 1; + * @return Whether the lastSeenDeployTime field is set. + */ + @java.lang.Override + public boolean hasLastSeenDeployTime() { + return ((bitField0_ & 0x00000001) != 0); + } + /** + *
+     * Optional. When set, the bootstrap event is suppressed if the cached deploy
+     * time matches this value. Future events are still emitted normally.
+     * 
+ * + * .google.protobuf.Timestamp last_seen_deploy_time = 1; + * @return The lastSeenDeployTime. + */ + @java.lang.Override + public com.google.protobuf.Timestamp getLastSeenDeployTime() { + return lastSeenDeployTime_ == null ? com.google.protobuf.Timestamp.getDefaultInstance() : lastSeenDeployTime_; + } + /** + *
+     * Optional. When set, the bootstrap event is suppressed if the cached deploy
+     * time matches this value. Future events are still emitted normally.
+     * 
+ * + * .google.protobuf.Timestamp last_seen_deploy_time = 1; + */ + @java.lang.Override + public com.google.protobuf.TimestampOrBuilder getLastSeenDeployTimeOrBuilder() { + return lastSeenDeployTime_ == null ? com.google.protobuf.Timestamp.getDefaultInstance() : lastSeenDeployTime_; + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + if (((bitField0_ & 0x00000001) != 0)) { + output.writeMessage(1, getLastSeenDeployTime()); + } + getUnknownFields().writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + if (((bitField0_ & 0x00000001) != 0)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(1, getLastSeenDeployTime()); + } + size += getUnknownFields().getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest)) { + return super.equals(obj); + } + galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest other = (galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest) obj; + + if (hasLastSeenDeployTime() != other.hasLastSeenDeployTime()) return false; + if (hasLastSeenDeployTime()) { + if (!getLastSeenDeployTime() + .equals(other.getLastSeenDeployTime())) return false; + } + if (!getUnknownFields().equals(other.getUnknownFields())) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + if (hasLastSeenDeployTime()) { + hash = (37 * hash) + LAST_SEEN_DEPLOY_TIME_FIELD_NUMBER; + hash = (53 * hash) + getLastSeenDeployTime().hashCode(); + } + hash = (29 * hash) + getUnknownFields().hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code galaxy_repository.v1.WatchDeployEventsRequest} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder implements + // @@protoc_insertion_point(builder_implements:galaxy_repository.v1.WatchDeployEventsRequest) + galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequestOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_WatchDeployEventsRequest_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_WatchDeployEventsRequest_fieldAccessorTable + .ensureFieldAccessorsInitialized( + galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest.class, galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest.Builder.class); + } + + // Construct using galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessage + .alwaysUseFieldBuilders) { + internalGetLastSeenDeployTimeFieldBuilder(); + } + } + @java.lang.Override + public Builder clear() { + super.clear(); + bitField0_ = 0; + lastSeenDeployTime_ = null; + if (lastSeenDeployTimeBuilder_ != null) { + lastSeenDeployTimeBuilder_.dispose(); + lastSeenDeployTimeBuilder_ = null; + } + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_WatchDeployEventsRequest_descriptor; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest getDefaultInstanceForType() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest.getDefaultInstance(); + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest build() { + galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest buildPartial() { + galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest result = new galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest(this); + if (bitField0_ != 0) { buildPartial0(result); } + onBuilt(); + return result; + } + + private void buildPartial0(galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest result) { + int from_bitField0_ = bitField0_; + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000001) != 0)) { + result.lastSeenDeployTime_ = lastSeenDeployTimeBuilder_ == null + ? lastSeenDeployTime_ + : lastSeenDeployTimeBuilder_.build(); + to_bitField0_ |= 0x00000001; + } + result.bitField0_ |= to_bitField0_; + } + + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest) { + return mergeFrom((galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest other) { + if (other == galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest.getDefaultInstance()) return this; + if (other.hasLastSeenDeployTime()) { + mergeLastSeenDeployTime(other.getLastSeenDeployTime()); + } + this.mergeUnknownFields(other.getUnknownFields()); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 10: { + input.readMessage( + internalGetLastSeenDeployTimeFieldBuilder().getBuilder(), + extensionRegistry); + bitField0_ |= 0x00000001; + break; + } // case 10 + default: { + if (!super.parseUnknownField(input, extensionRegistry, tag)) { + done = true; // was an endgroup tag + } + break; + } // default: + } // switch (tag) + } // while (!done) + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.unwrapIOException(); + } finally { + onChanged(); + } // finally + return this; + } + private int bitField0_; + + private com.google.protobuf.Timestamp lastSeenDeployTime_; + private com.google.protobuf.SingleFieldBuilder< + com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder> lastSeenDeployTimeBuilder_; + /** + *
+       * Optional. When set, the bootstrap event is suppressed if the cached deploy
+       * time matches this value. Future events are still emitted normally.
+       * 
+ * + * .google.protobuf.Timestamp last_seen_deploy_time = 1; + * @return Whether the lastSeenDeployTime field is set. + */ + public boolean hasLastSeenDeployTime() { + return ((bitField0_ & 0x00000001) != 0); + } + /** + *
+       * Optional. When set, the bootstrap event is suppressed if the cached deploy
+       * time matches this value. Future events are still emitted normally.
+       * 
+ * + * .google.protobuf.Timestamp last_seen_deploy_time = 1; + * @return The lastSeenDeployTime. + */ + public com.google.protobuf.Timestamp getLastSeenDeployTime() { + if (lastSeenDeployTimeBuilder_ == null) { + return lastSeenDeployTime_ == null ? com.google.protobuf.Timestamp.getDefaultInstance() : lastSeenDeployTime_; + } else { + return lastSeenDeployTimeBuilder_.getMessage(); + } + } + /** + *
+       * Optional. When set, the bootstrap event is suppressed if the cached deploy
+       * time matches this value. Future events are still emitted normally.
+       * 
+ * + * .google.protobuf.Timestamp last_seen_deploy_time = 1; + */ + public Builder setLastSeenDeployTime(com.google.protobuf.Timestamp value) { + if (lastSeenDeployTimeBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + lastSeenDeployTime_ = value; + } else { + lastSeenDeployTimeBuilder_.setMessage(value); + } + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + /** + *
+       * Optional. When set, the bootstrap event is suppressed if the cached deploy
+       * time matches this value. Future events are still emitted normally.
+       * 
+ * + * .google.protobuf.Timestamp last_seen_deploy_time = 1; + */ + public Builder setLastSeenDeployTime( + com.google.protobuf.Timestamp.Builder builderForValue) { + if (lastSeenDeployTimeBuilder_ == null) { + lastSeenDeployTime_ = builderForValue.build(); + } else { + lastSeenDeployTimeBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + /** + *
+       * Optional. When set, the bootstrap event is suppressed if the cached deploy
+       * time matches this value. Future events are still emitted normally.
+       * 
+ * + * .google.protobuf.Timestamp last_seen_deploy_time = 1; + */ + public Builder mergeLastSeenDeployTime(com.google.protobuf.Timestamp value) { + if (lastSeenDeployTimeBuilder_ == null) { + if (((bitField0_ & 0x00000001) != 0) && + lastSeenDeployTime_ != null && + lastSeenDeployTime_ != com.google.protobuf.Timestamp.getDefaultInstance()) { + getLastSeenDeployTimeBuilder().mergeFrom(value); + } else { + lastSeenDeployTime_ = value; + } + } else { + lastSeenDeployTimeBuilder_.mergeFrom(value); + } + if (lastSeenDeployTime_ != null) { + bitField0_ |= 0x00000001; + onChanged(); + } + return this; + } + /** + *
+       * Optional. When set, the bootstrap event is suppressed if the cached deploy
+       * time matches this value. Future events are still emitted normally.
+       * 
+ * + * .google.protobuf.Timestamp last_seen_deploy_time = 1; + */ + public Builder clearLastSeenDeployTime() { + bitField0_ = (bitField0_ & ~0x00000001); + lastSeenDeployTime_ = null; + if (lastSeenDeployTimeBuilder_ != null) { + lastSeenDeployTimeBuilder_.dispose(); + lastSeenDeployTimeBuilder_ = null; + } + onChanged(); + return this; + } + /** + *
+       * Optional. When set, the bootstrap event is suppressed if the cached deploy
+       * time matches this value. Future events are still emitted normally.
+       * 
+ * + * .google.protobuf.Timestamp last_seen_deploy_time = 1; + */ + public com.google.protobuf.Timestamp.Builder getLastSeenDeployTimeBuilder() { + bitField0_ |= 0x00000001; + onChanged(); + return internalGetLastSeenDeployTimeFieldBuilder().getBuilder(); + } + /** + *
+       * Optional. When set, the bootstrap event is suppressed if the cached deploy
+       * time matches this value. Future events are still emitted normally.
+       * 
+ * + * .google.protobuf.Timestamp last_seen_deploy_time = 1; + */ + public com.google.protobuf.TimestampOrBuilder getLastSeenDeployTimeOrBuilder() { + if (lastSeenDeployTimeBuilder_ != null) { + return lastSeenDeployTimeBuilder_.getMessageOrBuilder(); + } else { + return lastSeenDeployTime_ == null ? + com.google.protobuf.Timestamp.getDefaultInstance() : lastSeenDeployTime_; + } + } + /** + *
+       * Optional. When set, the bootstrap event is suppressed if the cached deploy
+       * time matches this value. Future events are still emitted normally.
+       * 
+ * + * .google.protobuf.Timestamp last_seen_deploy_time = 1; + */ + private com.google.protobuf.SingleFieldBuilder< + com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder> + internalGetLastSeenDeployTimeFieldBuilder() { + if (lastSeenDeployTimeBuilder_ == null) { + lastSeenDeployTimeBuilder_ = new com.google.protobuf.SingleFieldBuilder< + com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder>( + getLastSeenDeployTime(), + getParentForChildren(), + isClean()); + lastSeenDeployTime_ = null; + } + return lastSeenDeployTimeBuilder_; + } + + // @@protoc_insertion_point(builder_scope:galaxy_repository.v1.WatchDeployEventsRequest) + } + + // @@protoc_insertion_point(class_scope:galaxy_repository.v1.WatchDeployEventsRequest) + private static final galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest(); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public WatchDeployEventsRequest parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + Builder builder = newBuilder(); + try { + builder.mergeFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(builder.buildPartial()); + } catch (com.google.protobuf.UninitializedMessageException e) { + throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException(e) + .setUnfinishedMessage(builder.buildPartial()); + } + return builder.buildPartial(); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + + } + + public interface DeployEventOrBuilder extends + // @@protoc_insertion_point(interface_extends:galaxy_repository.v1.DeployEvent) + com.google.protobuf.MessageOrBuilder { + + /** + *
+     * Monotonically increasing per server start. Gaps indicate dropped events.
+     * 
+ * + * uint64 sequence = 1; + * @return The sequence. + */ + long getSequence(); + + /** + *
+     * Server wall-clock when the cache observed the deploy.
+     * 
+ * + * .google.protobuf.Timestamp observed_at = 2; + * @return Whether the observedAt field is set. + */ + boolean hasObservedAt(); + /** + *
+     * Server wall-clock when the cache observed the deploy.
+     * 
+ * + * .google.protobuf.Timestamp observed_at = 2; + * @return The observedAt. + */ + com.google.protobuf.Timestamp getObservedAt(); + /** + *
+     * Server wall-clock when the cache observed the deploy.
+     * 
+ * + * .google.protobuf.Timestamp observed_at = 2; + */ + com.google.protobuf.TimestampOrBuilder getObservedAtOrBuilder(); + + /** + *
+     * Galaxy.time_of_last_deploy. Absent only when the Galaxy table reports null.
+     * 
+ * + * .google.protobuf.Timestamp time_of_last_deploy = 3; + * @return Whether the timeOfLastDeploy field is set. + */ + boolean hasTimeOfLastDeploy(); + /** + *
+     * Galaxy.time_of_last_deploy. Absent only when the Galaxy table reports null.
+     * 
+ * + * .google.protobuf.Timestamp time_of_last_deploy = 3; + * @return The timeOfLastDeploy. + */ + com.google.protobuf.Timestamp getTimeOfLastDeploy(); + /** + *
+     * Galaxy.time_of_last_deploy. Absent only when the Galaxy table reports null.
+     * 
+ * + * .google.protobuf.Timestamp time_of_last_deploy = 3; + */ + com.google.protobuf.TimestampOrBuilder getTimeOfLastDeployOrBuilder(); + + /** + * bool time_of_last_deploy_present = 4; + * @return The timeOfLastDeployPresent. + */ + boolean getTimeOfLastDeployPresent(); + + /** + * int32 object_count = 5; + * @return The objectCount. + */ + int getObjectCount(); + + /** + * int32 attribute_count = 6; + * @return The attributeCount. + */ + int getAttributeCount(); + } + /** + * Protobuf type {@code galaxy_repository.v1.DeployEvent} + */ + public static final class DeployEvent extends + com.google.protobuf.GeneratedMessage implements + // @@protoc_insertion_point(message_implements:galaxy_repository.v1.DeployEvent) + DeployEventOrBuilder { + private static final long serialVersionUID = 0L; + static { + com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( + com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, + /* major= */ 4, + /* minor= */ 33, + /* patch= */ 1, + /* suffix= */ "", + "DeployEvent"); + } + // Use DeployEvent.newBuilder() to construct. + private DeployEvent(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + } + private DeployEvent() { + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_DeployEvent_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_DeployEvent_fieldAccessorTable + .ensureFieldAccessorsInitialized( + galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent.class, galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent.Builder.class); + } + + private int bitField0_; + public static final int SEQUENCE_FIELD_NUMBER = 1; + private long sequence_ = 0L; + /** + *
+     * Monotonically increasing per server start. Gaps indicate dropped events.
+     * 
+ * + * uint64 sequence = 1; + * @return The sequence. + */ + @java.lang.Override + public long getSequence() { + return sequence_; + } + + public static final int OBSERVED_AT_FIELD_NUMBER = 2; + private com.google.protobuf.Timestamp observedAt_; + /** + *
+     * Server wall-clock when the cache observed the deploy.
+     * 
+ * + * .google.protobuf.Timestamp observed_at = 2; + * @return Whether the observedAt field is set. + */ + @java.lang.Override + public boolean hasObservedAt() { + return ((bitField0_ & 0x00000001) != 0); + } + /** + *
+     * Server wall-clock when the cache observed the deploy.
+     * 
+ * + * .google.protobuf.Timestamp observed_at = 2; + * @return The observedAt. + */ + @java.lang.Override + public com.google.protobuf.Timestamp getObservedAt() { + return observedAt_ == null ? com.google.protobuf.Timestamp.getDefaultInstance() : observedAt_; + } + /** + *
+     * Server wall-clock when the cache observed the deploy.
+     * 
+ * + * .google.protobuf.Timestamp observed_at = 2; + */ + @java.lang.Override + public com.google.protobuf.TimestampOrBuilder getObservedAtOrBuilder() { + return observedAt_ == null ? com.google.protobuf.Timestamp.getDefaultInstance() : observedAt_; + } + + public static final int TIME_OF_LAST_DEPLOY_FIELD_NUMBER = 3; + private com.google.protobuf.Timestamp timeOfLastDeploy_; + /** + *
+     * Galaxy.time_of_last_deploy. Absent only when the Galaxy table reports null.
+     * 
+ * + * .google.protobuf.Timestamp time_of_last_deploy = 3; + * @return Whether the timeOfLastDeploy field is set. + */ + @java.lang.Override + public boolean hasTimeOfLastDeploy() { + return ((bitField0_ & 0x00000002) != 0); + } + /** + *
+     * Galaxy.time_of_last_deploy. Absent only when the Galaxy table reports null.
+     * 
+ * + * .google.protobuf.Timestamp time_of_last_deploy = 3; + * @return The timeOfLastDeploy. + */ + @java.lang.Override + public com.google.protobuf.Timestamp getTimeOfLastDeploy() { + return timeOfLastDeploy_ == null ? com.google.protobuf.Timestamp.getDefaultInstance() : timeOfLastDeploy_; + } + /** + *
+     * Galaxy.time_of_last_deploy. Absent only when the Galaxy table reports null.
+     * 
+ * + * .google.protobuf.Timestamp time_of_last_deploy = 3; + */ + @java.lang.Override + public com.google.protobuf.TimestampOrBuilder getTimeOfLastDeployOrBuilder() { + return timeOfLastDeploy_ == null ? com.google.protobuf.Timestamp.getDefaultInstance() : timeOfLastDeploy_; + } + + public static final int TIME_OF_LAST_DEPLOY_PRESENT_FIELD_NUMBER = 4; + private boolean timeOfLastDeployPresent_ = false; + /** + * bool time_of_last_deploy_present = 4; + * @return The timeOfLastDeployPresent. + */ + @java.lang.Override + public boolean getTimeOfLastDeployPresent() { + return timeOfLastDeployPresent_; + } + + public static final int OBJECT_COUNT_FIELD_NUMBER = 5; + private int objectCount_ = 0; + /** + * int32 object_count = 5; + * @return The objectCount. + */ + @java.lang.Override + public int getObjectCount() { + return objectCount_; + } + + public static final int ATTRIBUTE_COUNT_FIELD_NUMBER = 6; + private int attributeCount_ = 0; + /** + * int32 attribute_count = 6; + * @return The attributeCount. + */ + @java.lang.Override + public int getAttributeCount() { + return attributeCount_; + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + if (sequence_ != 0L) { + output.writeUInt64(1, sequence_); + } + if (((bitField0_ & 0x00000001) != 0)) { + output.writeMessage(2, getObservedAt()); + } + if (((bitField0_ & 0x00000002) != 0)) { + output.writeMessage(3, getTimeOfLastDeploy()); + } + if (timeOfLastDeployPresent_ != false) { + output.writeBool(4, timeOfLastDeployPresent_); + } + if (objectCount_ != 0) { + output.writeInt32(5, objectCount_); + } + if (attributeCount_ != 0) { + output.writeInt32(6, attributeCount_); + } + getUnknownFields().writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + if (sequence_ != 0L) { + size += com.google.protobuf.CodedOutputStream + .computeUInt64Size(1, sequence_); + } + if (((bitField0_ & 0x00000001) != 0)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(2, getObservedAt()); + } + if (((bitField0_ & 0x00000002) != 0)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(3, getTimeOfLastDeploy()); + } + if (timeOfLastDeployPresent_ != false) { + size += com.google.protobuf.CodedOutputStream + .computeBoolSize(4, timeOfLastDeployPresent_); + } + if (objectCount_ != 0) { + size += com.google.protobuf.CodedOutputStream + .computeInt32Size(5, objectCount_); + } + if (attributeCount_ != 0) { + size += com.google.protobuf.CodedOutputStream + .computeInt32Size(6, attributeCount_); + } + size += getUnknownFields().getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent)) { + return super.equals(obj); + } + galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent other = (galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent) obj; + + if (getSequence() + != other.getSequence()) return false; + if (hasObservedAt() != other.hasObservedAt()) return false; + if (hasObservedAt()) { + if (!getObservedAt() + .equals(other.getObservedAt())) return false; + } + if (hasTimeOfLastDeploy() != other.hasTimeOfLastDeploy()) return false; + if (hasTimeOfLastDeploy()) { + if (!getTimeOfLastDeploy() + .equals(other.getTimeOfLastDeploy())) return false; + } + if (getTimeOfLastDeployPresent() + != other.getTimeOfLastDeployPresent()) return false; + if (getObjectCount() + != other.getObjectCount()) return false; + if (getAttributeCount() + != other.getAttributeCount()) return false; + if (!getUnknownFields().equals(other.getUnknownFields())) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + hash = (37 * hash) + SEQUENCE_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashLong( + getSequence()); + if (hasObservedAt()) { + hash = (37 * hash) + OBSERVED_AT_FIELD_NUMBER; + hash = (53 * hash) + getObservedAt().hashCode(); + } + if (hasTimeOfLastDeploy()) { + hash = (37 * hash) + TIME_OF_LAST_DEPLOY_FIELD_NUMBER; + hash = (53 * hash) + getTimeOfLastDeploy().hashCode(); + } + hash = (37 * hash) + TIME_OF_LAST_DEPLOY_PRESENT_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashBoolean( + getTimeOfLastDeployPresent()); + hash = (37 * hash) + OBJECT_COUNT_FIELD_NUMBER; + hash = (53 * hash) + getObjectCount(); + hash = (37 * hash) + ATTRIBUTE_COUNT_FIELD_NUMBER; + hash = (53 * hash) + getAttributeCount(); + hash = (29 * hash) + getUnknownFields().hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code galaxy_repository.v1.DeployEvent} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder implements + // @@protoc_insertion_point(builder_implements:galaxy_repository.v1.DeployEvent) + galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEventOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_DeployEvent_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_DeployEvent_fieldAccessorTable + .ensureFieldAccessorsInitialized( + galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent.class, galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent.Builder.class); + } + + // Construct using galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessage + .alwaysUseFieldBuilders) { + internalGetObservedAtFieldBuilder(); + internalGetTimeOfLastDeployFieldBuilder(); + } + } + @java.lang.Override + public Builder clear() { + super.clear(); + bitField0_ = 0; + sequence_ = 0L; + observedAt_ = null; + if (observedAtBuilder_ != null) { + observedAtBuilder_.dispose(); + observedAtBuilder_ = null; + } + timeOfLastDeploy_ = null; + if (timeOfLastDeployBuilder_ != null) { + timeOfLastDeployBuilder_.dispose(); + timeOfLastDeployBuilder_ = null; + } + timeOfLastDeployPresent_ = false; + objectCount_ = 0; + attributeCount_ = 0; + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_DeployEvent_descriptor; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent getDefaultInstanceForType() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent.getDefaultInstance(); + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent build() { + galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent buildPartial() { + galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent result = new galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent(this); + if (bitField0_ != 0) { buildPartial0(result); } + onBuilt(); + return result; + } + + private void buildPartial0(galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent result) { + int from_bitField0_ = bitField0_; + if (((from_bitField0_ & 0x00000001) != 0)) { + result.sequence_ = sequence_; + } + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000002) != 0)) { + result.observedAt_ = observedAtBuilder_ == null + ? observedAt_ + : observedAtBuilder_.build(); + to_bitField0_ |= 0x00000001; + } + if (((from_bitField0_ & 0x00000004) != 0)) { + result.timeOfLastDeploy_ = timeOfLastDeployBuilder_ == null + ? timeOfLastDeploy_ + : timeOfLastDeployBuilder_.build(); + to_bitField0_ |= 0x00000002; + } + if (((from_bitField0_ & 0x00000008) != 0)) { + result.timeOfLastDeployPresent_ = timeOfLastDeployPresent_; + } + if (((from_bitField0_ & 0x00000010) != 0)) { + result.objectCount_ = objectCount_; + } + if (((from_bitField0_ & 0x00000020) != 0)) { + result.attributeCount_ = attributeCount_; + } + result.bitField0_ |= to_bitField0_; + } + + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent) { + return mergeFrom((galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent other) { + if (other == galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent.getDefaultInstance()) return this; + if (other.getSequence() != 0L) { + setSequence(other.getSequence()); + } + if (other.hasObservedAt()) { + mergeObservedAt(other.getObservedAt()); + } + if (other.hasTimeOfLastDeploy()) { + mergeTimeOfLastDeploy(other.getTimeOfLastDeploy()); + } + if (other.getTimeOfLastDeployPresent() != false) { + setTimeOfLastDeployPresent(other.getTimeOfLastDeployPresent()); + } + if (other.getObjectCount() != 0) { + setObjectCount(other.getObjectCount()); + } + if (other.getAttributeCount() != 0) { + setAttributeCount(other.getAttributeCount()); + } + this.mergeUnknownFields(other.getUnknownFields()); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 8: { + sequence_ = input.readUInt64(); + bitField0_ |= 0x00000001; + break; + } // case 8 + case 18: { + input.readMessage( + internalGetObservedAtFieldBuilder().getBuilder(), + extensionRegistry); + bitField0_ |= 0x00000002; + break; + } // case 18 + case 26: { + input.readMessage( + internalGetTimeOfLastDeployFieldBuilder().getBuilder(), + extensionRegistry); + bitField0_ |= 0x00000004; + break; + } // case 26 + case 32: { + timeOfLastDeployPresent_ = input.readBool(); + bitField0_ |= 0x00000008; + break; + } // case 32 + case 40: { + objectCount_ = input.readInt32(); + bitField0_ |= 0x00000010; + break; + } // case 40 + case 48: { + attributeCount_ = input.readInt32(); + bitField0_ |= 0x00000020; + break; + } // case 48 + default: { + if (!super.parseUnknownField(input, extensionRegistry, tag)) { + done = true; // was an endgroup tag + } + break; + } // default: + } // switch (tag) + } // while (!done) + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.unwrapIOException(); + } finally { + onChanged(); + } // finally + return this; + } + private int bitField0_; + + private long sequence_ ; + /** + *
+       * Monotonically increasing per server start. Gaps indicate dropped events.
+       * 
+ * + * uint64 sequence = 1; + * @return The sequence. + */ + @java.lang.Override + public long getSequence() { + return sequence_; + } + /** + *
+       * Monotonically increasing per server start. Gaps indicate dropped events.
+       * 
+ * + * uint64 sequence = 1; + * @param value The sequence to set. + * @return This builder for chaining. + */ + public Builder setSequence(long value) { + + sequence_ = value; + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + /** + *
+       * Monotonically increasing per server start. Gaps indicate dropped events.
+       * 
+ * + * uint64 sequence = 1; + * @return This builder for chaining. + */ + public Builder clearSequence() { + bitField0_ = (bitField0_ & ~0x00000001); + sequence_ = 0L; + onChanged(); + return this; + } + + private com.google.protobuf.Timestamp observedAt_; + private com.google.protobuf.SingleFieldBuilder< + com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder> observedAtBuilder_; + /** + *
+       * Server wall-clock when the cache observed the deploy.
+       * 
+ * + * .google.protobuf.Timestamp observed_at = 2; + * @return Whether the observedAt field is set. + */ + public boolean hasObservedAt() { + return ((bitField0_ & 0x00000002) != 0); + } + /** + *
+       * Server wall-clock when the cache observed the deploy.
+       * 
+ * + * .google.protobuf.Timestamp observed_at = 2; + * @return The observedAt. + */ + public com.google.protobuf.Timestamp getObservedAt() { + if (observedAtBuilder_ == null) { + return observedAt_ == null ? com.google.protobuf.Timestamp.getDefaultInstance() : observedAt_; + } else { + return observedAtBuilder_.getMessage(); + } + } + /** + *
+       * Server wall-clock when the cache observed the deploy.
+       * 
+ * + * .google.protobuf.Timestamp observed_at = 2; + */ + public Builder setObservedAt(com.google.protobuf.Timestamp value) { + if (observedAtBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + observedAt_ = value; + } else { + observedAtBuilder_.setMessage(value); + } + bitField0_ |= 0x00000002; + onChanged(); + return this; + } + /** + *
+       * Server wall-clock when the cache observed the deploy.
+       * 
+ * + * .google.protobuf.Timestamp observed_at = 2; + */ + public Builder setObservedAt( + com.google.protobuf.Timestamp.Builder builderForValue) { + if (observedAtBuilder_ == null) { + observedAt_ = builderForValue.build(); + } else { + observedAtBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000002; + onChanged(); + return this; + } + /** + *
+       * Server wall-clock when the cache observed the deploy.
+       * 
+ * + * .google.protobuf.Timestamp observed_at = 2; + */ + public Builder mergeObservedAt(com.google.protobuf.Timestamp value) { + if (observedAtBuilder_ == null) { + if (((bitField0_ & 0x00000002) != 0) && + observedAt_ != null && + observedAt_ != com.google.protobuf.Timestamp.getDefaultInstance()) { + getObservedAtBuilder().mergeFrom(value); + } else { + observedAt_ = value; + } + } else { + observedAtBuilder_.mergeFrom(value); + } + if (observedAt_ != null) { + bitField0_ |= 0x00000002; + onChanged(); + } + return this; + } + /** + *
+       * Server wall-clock when the cache observed the deploy.
+       * 
+ * + * .google.protobuf.Timestamp observed_at = 2; + */ + public Builder clearObservedAt() { + bitField0_ = (bitField0_ & ~0x00000002); + observedAt_ = null; + if (observedAtBuilder_ != null) { + observedAtBuilder_.dispose(); + observedAtBuilder_ = null; + } + onChanged(); + return this; + } + /** + *
+       * Server wall-clock when the cache observed the deploy.
+       * 
+ * + * .google.protobuf.Timestamp observed_at = 2; + */ + public com.google.protobuf.Timestamp.Builder getObservedAtBuilder() { + bitField0_ |= 0x00000002; + onChanged(); + return internalGetObservedAtFieldBuilder().getBuilder(); + } + /** + *
+       * Server wall-clock when the cache observed the deploy.
+       * 
+ * + * .google.protobuf.Timestamp observed_at = 2; + */ + public com.google.protobuf.TimestampOrBuilder getObservedAtOrBuilder() { + if (observedAtBuilder_ != null) { + return observedAtBuilder_.getMessageOrBuilder(); + } else { + return observedAt_ == null ? + com.google.protobuf.Timestamp.getDefaultInstance() : observedAt_; + } + } + /** + *
+       * Server wall-clock when the cache observed the deploy.
+       * 
+ * + * .google.protobuf.Timestamp observed_at = 2; + */ + private com.google.protobuf.SingleFieldBuilder< + com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder> + internalGetObservedAtFieldBuilder() { + if (observedAtBuilder_ == null) { + observedAtBuilder_ = new com.google.protobuf.SingleFieldBuilder< + com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder>( + getObservedAt(), + getParentForChildren(), + isClean()); + observedAt_ = null; + } + return observedAtBuilder_; + } + + private com.google.protobuf.Timestamp timeOfLastDeploy_; + private com.google.protobuf.SingleFieldBuilder< + com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder> timeOfLastDeployBuilder_; + /** + *
+       * Galaxy.time_of_last_deploy. Absent only when the Galaxy table reports null.
+       * 
+ * + * .google.protobuf.Timestamp time_of_last_deploy = 3; + * @return Whether the timeOfLastDeploy field is set. + */ + public boolean hasTimeOfLastDeploy() { + return ((bitField0_ & 0x00000004) != 0); + } + /** + *
+       * Galaxy.time_of_last_deploy. Absent only when the Galaxy table reports null.
+       * 
+ * + * .google.protobuf.Timestamp time_of_last_deploy = 3; + * @return The timeOfLastDeploy. + */ + public com.google.protobuf.Timestamp getTimeOfLastDeploy() { + if (timeOfLastDeployBuilder_ == null) { + return timeOfLastDeploy_ == null ? com.google.protobuf.Timestamp.getDefaultInstance() : timeOfLastDeploy_; + } else { + return timeOfLastDeployBuilder_.getMessage(); + } + } + /** + *
+       * Galaxy.time_of_last_deploy. Absent only when the Galaxy table reports null.
+       * 
+ * + * .google.protobuf.Timestamp time_of_last_deploy = 3; + */ + public Builder setTimeOfLastDeploy(com.google.protobuf.Timestamp value) { + if (timeOfLastDeployBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + timeOfLastDeploy_ = value; + } else { + timeOfLastDeployBuilder_.setMessage(value); + } + bitField0_ |= 0x00000004; + onChanged(); + return this; + } + /** + *
+       * Galaxy.time_of_last_deploy. Absent only when the Galaxy table reports null.
+       * 
+ * + * .google.protobuf.Timestamp time_of_last_deploy = 3; + */ + public Builder setTimeOfLastDeploy( + com.google.protobuf.Timestamp.Builder builderForValue) { + if (timeOfLastDeployBuilder_ == null) { + timeOfLastDeploy_ = builderForValue.build(); + } else { + timeOfLastDeployBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000004; + onChanged(); + return this; + } + /** + *
+       * Galaxy.time_of_last_deploy. Absent only when the Galaxy table reports null.
+       * 
+ * + * .google.protobuf.Timestamp time_of_last_deploy = 3; + */ + public Builder mergeTimeOfLastDeploy(com.google.protobuf.Timestamp value) { + if (timeOfLastDeployBuilder_ == null) { + if (((bitField0_ & 0x00000004) != 0) && + timeOfLastDeploy_ != null && + timeOfLastDeploy_ != com.google.protobuf.Timestamp.getDefaultInstance()) { + getTimeOfLastDeployBuilder().mergeFrom(value); + } else { + timeOfLastDeploy_ = value; + } + } else { + timeOfLastDeployBuilder_.mergeFrom(value); + } + if (timeOfLastDeploy_ != null) { + bitField0_ |= 0x00000004; + onChanged(); + } + return this; + } + /** + *
+       * Galaxy.time_of_last_deploy. Absent only when the Galaxy table reports null.
+       * 
+ * + * .google.protobuf.Timestamp time_of_last_deploy = 3; + */ + public Builder clearTimeOfLastDeploy() { + bitField0_ = (bitField0_ & ~0x00000004); + timeOfLastDeploy_ = null; + if (timeOfLastDeployBuilder_ != null) { + timeOfLastDeployBuilder_.dispose(); + timeOfLastDeployBuilder_ = null; + } + onChanged(); + return this; + } + /** + *
+       * Galaxy.time_of_last_deploy. Absent only when the Galaxy table reports null.
+       * 
+ * + * .google.protobuf.Timestamp time_of_last_deploy = 3; + */ + public com.google.protobuf.Timestamp.Builder getTimeOfLastDeployBuilder() { + bitField0_ |= 0x00000004; + onChanged(); + return internalGetTimeOfLastDeployFieldBuilder().getBuilder(); + } + /** + *
+       * Galaxy.time_of_last_deploy. Absent only when the Galaxy table reports null.
+       * 
+ * + * .google.protobuf.Timestamp time_of_last_deploy = 3; + */ + public com.google.protobuf.TimestampOrBuilder getTimeOfLastDeployOrBuilder() { + if (timeOfLastDeployBuilder_ != null) { + return timeOfLastDeployBuilder_.getMessageOrBuilder(); + } else { + return timeOfLastDeploy_ == null ? + com.google.protobuf.Timestamp.getDefaultInstance() : timeOfLastDeploy_; + } + } + /** + *
+       * Galaxy.time_of_last_deploy. Absent only when the Galaxy table reports null.
+       * 
+ * + * .google.protobuf.Timestamp time_of_last_deploy = 3; + */ + private com.google.protobuf.SingleFieldBuilder< + com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder> + internalGetTimeOfLastDeployFieldBuilder() { + if (timeOfLastDeployBuilder_ == null) { + timeOfLastDeployBuilder_ = new com.google.protobuf.SingleFieldBuilder< + com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder>( + getTimeOfLastDeploy(), + getParentForChildren(), + isClean()); + timeOfLastDeploy_ = null; + } + return timeOfLastDeployBuilder_; + } + + private boolean timeOfLastDeployPresent_ ; + /** + * bool time_of_last_deploy_present = 4; + * @return The timeOfLastDeployPresent. + */ + @java.lang.Override + public boolean getTimeOfLastDeployPresent() { + return timeOfLastDeployPresent_; + } + /** + * bool time_of_last_deploy_present = 4; + * @param value The timeOfLastDeployPresent to set. + * @return This builder for chaining. + */ + public Builder setTimeOfLastDeployPresent(boolean value) { + + timeOfLastDeployPresent_ = value; + bitField0_ |= 0x00000008; + onChanged(); + return this; + } + /** + * bool time_of_last_deploy_present = 4; + * @return This builder for chaining. + */ + public Builder clearTimeOfLastDeployPresent() { + bitField0_ = (bitField0_ & ~0x00000008); + timeOfLastDeployPresent_ = false; + onChanged(); + return this; + } + + private int objectCount_ ; + /** + * int32 object_count = 5; + * @return The objectCount. + */ + @java.lang.Override + public int getObjectCount() { + return objectCount_; + } + /** + * int32 object_count = 5; + * @param value The objectCount to set. + * @return This builder for chaining. + */ + public Builder setObjectCount(int value) { + + objectCount_ = value; + bitField0_ |= 0x00000010; + onChanged(); + return this; + } + /** + * int32 object_count = 5; + * @return This builder for chaining. + */ + public Builder clearObjectCount() { + bitField0_ = (bitField0_ & ~0x00000010); + objectCount_ = 0; + onChanged(); + return this; + } + + private int attributeCount_ ; + /** + * int32 attribute_count = 6; + * @return The attributeCount. + */ + @java.lang.Override + public int getAttributeCount() { + return attributeCount_; + } + /** + * int32 attribute_count = 6; + * @param value The attributeCount to set. + * @return This builder for chaining. + */ + public Builder setAttributeCount(int value) { + + attributeCount_ = value; + bitField0_ |= 0x00000020; + onChanged(); + return this; + } + /** + * int32 attribute_count = 6; + * @return This builder for chaining. + */ + public Builder clearAttributeCount() { + bitField0_ = (bitField0_ & ~0x00000020); + attributeCount_ = 0; + onChanged(); + return this; + } + + // @@protoc_insertion_point(builder_scope:galaxy_repository.v1.DeployEvent) + } + + // @@protoc_insertion_point(class_scope:galaxy_repository.v1.DeployEvent) + private static final galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent(); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public DeployEvent parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + Builder builder = newBuilder(); + try { + builder.mergeFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(builder.buildPartial()); + } catch (com.google.protobuf.UninitializedMessageException e) { + throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException(e) + .setUnfinishedMessage(builder.buildPartial()); + } + return builder.buildPartial(); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + + } + + public interface GalaxyObjectOrBuilder extends + // @@protoc_insertion_point(interface_extends:galaxy_repository.v1.GalaxyObject) + com.google.protobuf.MessageOrBuilder { + + /** + * int32 gobject_id = 1; + * @return The gobjectId. + */ + int getGobjectId(); + + /** + * string tag_name = 2; + * @return The tagName. + */ + java.lang.String getTagName(); + /** + * string tag_name = 2; + * @return The bytes for tagName. + */ + com.google.protobuf.ByteString + getTagNameBytes(); + + /** + * string contained_name = 3; + * @return The containedName. + */ + java.lang.String getContainedName(); + /** + * string contained_name = 3; + * @return The bytes for containedName. + */ + com.google.protobuf.ByteString + getContainedNameBytes(); + + /** + * string browse_name = 4; + * @return The browseName. + */ + java.lang.String getBrowseName(); + /** + * string browse_name = 4; + * @return The bytes for browseName. + */ + com.google.protobuf.ByteString + getBrowseNameBytes(); + + /** + * int32 parent_gobject_id = 5; + * @return The parentGobjectId. + */ + int getParentGobjectId(); + + /** + * bool is_area = 6; + * @return The isArea. + */ + boolean getIsArea(); + + /** + * int32 category_id = 7; + * @return The categoryId. + */ + int getCategoryId(); + + /** + * int32 hosted_by_gobject_id = 8; + * @return The hostedByGobjectId. + */ + int getHostedByGobjectId(); + + /** + * repeated string template_chain = 9; + * @return A list containing the templateChain. + */ + java.util.List + getTemplateChainList(); + /** + * repeated string template_chain = 9; + * @return The count of templateChain. + */ + int getTemplateChainCount(); + /** + * repeated string template_chain = 9; + * @param index The index of the element to return. + * @return The templateChain at the given index. + */ + java.lang.String getTemplateChain(int index); + /** + * repeated string template_chain = 9; + * @param index The index of the value to return. + * @return The bytes of the templateChain at the given index. + */ + com.google.protobuf.ByteString + getTemplateChainBytes(int index); + + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + java.util.List + getAttributesList(); + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute getAttributes(int index); + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + int getAttributesCount(); + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + java.util.List + getAttributesOrBuilderList(); + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttributeOrBuilder getAttributesOrBuilder( + int index); + } + /** + * Protobuf type {@code galaxy_repository.v1.GalaxyObject} + */ + public static final class GalaxyObject extends + com.google.protobuf.GeneratedMessage implements + // @@protoc_insertion_point(message_implements:galaxy_repository.v1.GalaxyObject) + GalaxyObjectOrBuilder { + private static final long serialVersionUID = 0L; + static { + com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( + com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, + /* major= */ 4, + /* minor= */ 33, + /* patch= */ 1, + /* suffix= */ "", + "GalaxyObject"); + } + // Use GalaxyObject.newBuilder() to construct. + private GalaxyObject(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + } + private GalaxyObject() { + tagName_ = ""; + containedName_ = ""; + browseName_ = ""; + templateChain_ = + com.google.protobuf.LazyStringArrayList.emptyList(); + attributes_ = java.util.Collections.emptyList(); + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_GalaxyObject_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_GalaxyObject_fieldAccessorTable + .ensureFieldAccessorsInitialized( + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.class, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.Builder.class); + } + + public static final int GOBJECT_ID_FIELD_NUMBER = 1; + private int gobjectId_ = 0; + /** + * int32 gobject_id = 1; + * @return The gobjectId. + */ + @java.lang.Override + public int getGobjectId() { + return gobjectId_; + } + + public static final int TAG_NAME_FIELD_NUMBER = 2; + @SuppressWarnings("serial") + private volatile java.lang.Object tagName_ = ""; + /** + * string tag_name = 2; + * @return The tagName. + */ + @java.lang.Override + public java.lang.String getTagName() { + java.lang.Object ref = tagName_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + tagName_ = s; + return s; + } + } + /** + * string tag_name = 2; + * @return The bytes for tagName. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getTagNameBytes() { + java.lang.Object ref = tagName_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + tagName_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + public static final int CONTAINED_NAME_FIELD_NUMBER = 3; + @SuppressWarnings("serial") + private volatile java.lang.Object containedName_ = ""; + /** + * string contained_name = 3; + * @return The containedName. + */ + @java.lang.Override + public java.lang.String getContainedName() { + java.lang.Object ref = containedName_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + containedName_ = s; + return s; + } + } + /** + * string contained_name = 3; + * @return The bytes for containedName. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getContainedNameBytes() { + java.lang.Object ref = containedName_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + containedName_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + public static final int BROWSE_NAME_FIELD_NUMBER = 4; + @SuppressWarnings("serial") + private volatile java.lang.Object browseName_ = ""; + /** + * string browse_name = 4; + * @return The browseName. + */ + @java.lang.Override + public java.lang.String getBrowseName() { + java.lang.Object ref = browseName_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + browseName_ = s; + return s; + } + } + /** + * string browse_name = 4; + * @return The bytes for browseName. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getBrowseNameBytes() { + java.lang.Object ref = browseName_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + browseName_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + public static final int PARENT_GOBJECT_ID_FIELD_NUMBER = 5; + private int parentGobjectId_ = 0; + /** + * int32 parent_gobject_id = 5; + * @return The parentGobjectId. + */ + @java.lang.Override + public int getParentGobjectId() { + return parentGobjectId_; + } + + public static final int IS_AREA_FIELD_NUMBER = 6; + private boolean isArea_ = false; + /** + * bool is_area = 6; + * @return The isArea. + */ + @java.lang.Override + public boolean getIsArea() { + return isArea_; + } + + public static final int CATEGORY_ID_FIELD_NUMBER = 7; + private int categoryId_ = 0; + /** + * int32 category_id = 7; + * @return The categoryId. + */ + @java.lang.Override + public int getCategoryId() { + return categoryId_; + } + + public static final int HOSTED_BY_GOBJECT_ID_FIELD_NUMBER = 8; + private int hostedByGobjectId_ = 0; + /** + * int32 hosted_by_gobject_id = 8; + * @return The hostedByGobjectId. + */ + @java.lang.Override + public int getHostedByGobjectId() { + return hostedByGobjectId_; + } + + public static final int TEMPLATE_CHAIN_FIELD_NUMBER = 9; + @SuppressWarnings("serial") + private com.google.protobuf.LazyStringArrayList templateChain_ = + com.google.protobuf.LazyStringArrayList.emptyList(); + /** + * repeated string template_chain = 9; + * @return A list containing the templateChain. + */ + public com.google.protobuf.ProtocolStringList + getTemplateChainList() { + return templateChain_; + } + /** + * repeated string template_chain = 9; + * @return The count of templateChain. + */ + public int getTemplateChainCount() { + return templateChain_.size(); + } + /** + * repeated string template_chain = 9; + * @param index The index of the element to return. + * @return The templateChain at the given index. + */ + public java.lang.String getTemplateChain(int index) { + return templateChain_.get(index); + } + /** + * repeated string template_chain = 9; + * @param index The index of the value to return. + * @return The bytes of the templateChain at the given index. + */ + public com.google.protobuf.ByteString + getTemplateChainBytes(int index) { + return templateChain_.getByteString(index); + } + + public static final int ATTRIBUTES_FIELD_NUMBER = 10; + @SuppressWarnings("serial") + private java.util.List attributes_; + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + @java.lang.Override + public java.util.List getAttributesList() { + return attributes_; + } + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + @java.lang.Override + public java.util.List + getAttributesOrBuilderList() { + return attributes_; + } + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + @java.lang.Override + public int getAttributesCount() { + return attributes_.size(); + } + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute getAttributes(int index) { + return attributes_.get(index); + } + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttributeOrBuilder getAttributesOrBuilder( + int index) { + return attributes_.get(index); + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + if (gobjectId_ != 0) { + output.writeInt32(1, gobjectId_); + } + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(tagName_)) { + com.google.protobuf.GeneratedMessage.writeString(output, 2, tagName_); + } + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(containedName_)) { + com.google.protobuf.GeneratedMessage.writeString(output, 3, containedName_); + } + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(browseName_)) { + com.google.protobuf.GeneratedMessage.writeString(output, 4, browseName_); + } + if (parentGobjectId_ != 0) { + output.writeInt32(5, parentGobjectId_); + } + if (isArea_ != false) { + output.writeBool(6, isArea_); + } + if (categoryId_ != 0) { + output.writeInt32(7, categoryId_); + } + if (hostedByGobjectId_ != 0) { + output.writeInt32(8, hostedByGobjectId_); + } + for (int i = 0; i < templateChain_.size(); i++) { + com.google.protobuf.GeneratedMessage.writeString(output, 9, templateChain_.getRaw(i)); + } + for (int i = 0; i < attributes_.size(); i++) { + output.writeMessage(10, attributes_.get(i)); + } + getUnknownFields().writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + if (gobjectId_ != 0) { + size += com.google.protobuf.CodedOutputStream + .computeInt32Size(1, gobjectId_); + } + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(tagName_)) { + size += com.google.protobuf.GeneratedMessage.computeStringSize(2, tagName_); + } + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(containedName_)) { + size += com.google.protobuf.GeneratedMessage.computeStringSize(3, containedName_); + } + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(browseName_)) { + size += com.google.protobuf.GeneratedMessage.computeStringSize(4, browseName_); + } + if (parentGobjectId_ != 0) { + size += com.google.protobuf.CodedOutputStream + .computeInt32Size(5, parentGobjectId_); + } + if (isArea_ != false) { + size += com.google.protobuf.CodedOutputStream + .computeBoolSize(6, isArea_); + } + if (categoryId_ != 0) { + size += com.google.protobuf.CodedOutputStream + .computeInt32Size(7, categoryId_); + } + if (hostedByGobjectId_ != 0) { + size += com.google.protobuf.CodedOutputStream + .computeInt32Size(8, hostedByGobjectId_); + } + { + int dataSize = 0; + for (int i = 0; i < templateChain_.size(); i++) { + dataSize += computeStringSizeNoTag(templateChain_.getRaw(i)); + } + size += dataSize; + size += 1 * getTemplateChainList().size(); + } + for (int i = 0; i < attributes_.size(); i++) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(10, attributes_.get(i)); + } + size += getUnknownFields().getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject)) { + return super.equals(obj); + } + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject other = (galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject) obj; + + if (getGobjectId() + != other.getGobjectId()) return false; + if (!getTagName() + .equals(other.getTagName())) return false; + if (!getContainedName() + .equals(other.getContainedName())) return false; + if (!getBrowseName() + .equals(other.getBrowseName())) return false; + if (getParentGobjectId() + != other.getParentGobjectId()) return false; + if (getIsArea() + != other.getIsArea()) return false; + if (getCategoryId() + != other.getCategoryId()) return false; + if (getHostedByGobjectId() + != other.getHostedByGobjectId()) return false; + if (!getTemplateChainList() + .equals(other.getTemplateChainList())) return false; + if (!getAttributesList() + .equals(other.getAttributesList())) return false; + if (!getUnknownFields().equals(other.getUnknownFields())) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + hash = (37 * hash) + GOBJECT_ID_FIELD_NUMBER; + hash = (53 * hash) + getGobjectId(); + hash = (37 * hash) + TAG_NAME_FIELD_NUMBER; + hash = (53 * hash) + getTagName().hashCode(); + hash = (37 * hash) + CONTAINED_NAME_FIELD_NUMBER; + hash = (53 * hash) + getContainedName().hashCode(); + hash = (37 * hash) + BROWSE_NAME_FIELD_NUMBER; + hash = (53 * hash) + getBrowseName().hashCode(); + hash = (37 * hash) + PARENT_GOBJECT_ID_FIELD_NUMBER; + hash = (53 * hash) + getParentGobjectId(); + hash = (37 * hash) + IS_AREA_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashBoolean( + getIsArea()); + hash = (37 * hash) + CATEGORY_ID_FIELD_NUMBER; + hash = (53 * hash) + getCategoryId(); + hash = (37 * hash) + HOSTED_BY_GOBJECT_ID_FIELD_NUMBER; + hash = (53 * hash) + getHostedByGobjectId(); + if (getTemplateChainCount() > 0) { + hash = (37 * hash) + TEMPLATE_CHAIN_FIELD_NUMBER; + hash = (53 * hash) + getTemplateChainList().hashCode(); + } + if (getAttributesCount() > 0) { + hash = (37 * hash) + ATTRIBUTES_FIELD_NUMBER; + hash = (53 * hash) + getAttributesList().hashCode(); + } + hash = (29 * hash) + getUnknownFields().hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code galaxy_repository.v1.GalaxyObject} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder implements + // @@protoc_insertion_point(builder_implements:galaxy_repository.v1.GalaxyObject) + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObjectOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_GalaxyObject_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_GalaxyObject_fieldAccessorTable + .ensureFieldAccessorsInitialized( + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.class, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.Builder.class); + } + + // Construct using galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.newBuilder() + private Builder() { + + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + + } + @java.lang.Override + public Builder clear() { + super.clear(); + bitField0_ = 0; + gobjectId_ = 0; + tagName_ = ""; + containedName_ = ""; + browseName_ = ""; + parentGobjectId_ = 0; + isArea_ = false; + categoryId_ = 0; + hostedByGobjectId_ = 0; + templateChain_ = + com.google.protobuf.LazyStringArrayList.emptyList(); + if (attributesBuilder_ == null) { + attributes_ = java.util.Collections.emptyList(); + } else { + attributes_ = null; + attributesBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000200); + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_GalaxyObject_descriptor; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject getDefaultInstanceForType() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.getDefaultInstance(); + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject build() { + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject buildPartial() { + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject result = new galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject(this); + buildPartialRepeatedFields(result); + if (bitField0_ != 0) { buildPartial0(result); } + onBuilt(); + return result; + } + + private void buildPartialRepeatedFields(galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject result) { + if (attributesBuilder_ == null) { + if (((bitField0_ & 0x00000200) != 0)) { + attributes_ = java.util.Collections.unmodifiableList(attributes_); + bitField0_ = (bitField0_ & ~0x00000200); + } + result.attributes_ = attributes_; + } else { + result.attributes_ = attributesBuilder_.build(); + } + } + + private void buildPartial0(galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject result) { + int from_bitField0_ = bitField0_; + if (((from_bitField0_ & 0x00000001) != 0)) { + result.gobjectId_ = gobjectId_; + } + if (((from_bitField0_ & 0x00000002) != 0)) { + result.tagName_ = tagName_; + } + if (((from_bitField0_ & 0x00000004) != 0)) { + result.containedName_ = containedName_; + } + if (((from_bitField0_ & 0x00000008) != 0)) { + result.browseName_ = browseName_; + } + if (((from_bitField0_ & 0x00000010) != 0)) { + result.parentGobjectId_ = parentGobjectId_; + } + if (((from_bitField0_ & 0x00000020) != 0)) { + result.isArea_ = isArea_; + } + if (((from_bitField0_ & 0x00000040) != 0)) { + result.categoryId_ = categoryId_; + } + if (((from_bitField0_ & 0x00000080) != 0)) { + result.hostedByGobjectId_ = hostedByGobjectId_; + } + if (((from_bitField0_ & 0x00000100) != 0)) { + templateChain_.makeImmutable(); + result.templateChain_ = templateChain_; + } + } + + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject) { + return mergeFrom((galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject other) { + if (other == galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.getDefaultInstance()) return this; + if (other.getGobjectId() != 0) { + setGobjectId(other.getGobjectId()); + } + if (!other.getTagName().isEmpty()) { + tagName_ = other.tagName_; + bitField0_ |= 0x00000002; + onChanged(); + } + if (!other.getContainedName().isEmpty()) { + containedName_ = other.containedName_; + bitField0_ |= 0x00000004; + onChanged(); + } + if (!other.getBrowseName().isEmpty()) { + browseName_ = other.browseName_; + bitField0_ |= 0x00000008; + onChanged(); + } + if (other.getParentGobjectId() != 0) { + setParentGobjectId(other.getParentGobjectId()); + } + if (other.getIsArea() != false) { + setIsArea(other.getIsArea()); + } + if (other.getCategoryId() != 0) { + setCategoryId(other.getCategoryId()); + } + if (other.getHostedByGobjectId() != 0) { + setHostedByGobjectId(other.getHostedByGobjectId()); + } + if (!other.templateChain_.isEmpty()) { + if (templateChain_.isEmpty()) { + templateChain_ = other.templateChain_; + bitField0_ |= 0x00000100; + } else { + ensureTemplateChainIsMutable(); + templateChain_.addAll(other.templateChain_); + } + onChanged(); + } + if (attributesBuilder_ == null) { + if (!other.attributes_.isEmpty()) { + if (attributes_.isEmpty()) { + attributes_ = other.attributes_; + bitField0_ = (bitField0_ & ~0x00000200); + } else { + ensureAttributesIsMutable(); + attributes_.addAll(other.attributes_); + } + onChanged(); + } + } else { + if (!other.attributes_.isEmpty()) { + if (attributesBuilder_.isEmpty()) { + attributesBuilder_.dispose(); + attributesBuilder_ = null; + attributes_ = other.attributes_; + bitField0_ = (bitField0_ & ~0x00000200); + attributesBuilder_ = + com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ? + internalGetAttributesFieldBuilder() : null; + } else { + attributesBuilder_.addAllMessages(other.attributes_); + } + } + } + this.mergeUnknownFields(other.getUnknownFields()); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 8: { + gobjectId_ = input.readInt32(); + bitField0_ |= 0x00000001; + break; + } // case 8 + case 18: { + tagName_ = input.readStringRequireUtf8(); + bitField0_ |= 0x00000002; + break; + } // case 18 + case 26: { + containedName_ = input.readStringRequireUtf8(); + bitField0_ |= 0x00000004; + break; + } // case 26 + case 34: { + browseName_ = input.readStringRequireUtf8(); + bitField0_ |= 0x00000008; + break; + } // case 34 + case 40: { + parentGobjectId_ = input.readInt32(); + bitField0_ |= 0x00000010; + break; + } // case 40 + case 48: { + isArea_ = input.readBool(); + bitField0_ |= 0x00000020; + break; + } // case 48 + case 56: { + categoryId_ = input.readInt32(); + bitField0_ |= 0x00000040; + break; + } // case 56 + case 64: { + hostedByGobjectId_ = input.readInt32(); + bitField0_ |= 0x00000080; + break; + } // case 64 + case 74: { + java.lang.String s = input.readStringRequireUtf8(); + ensureTemplateChainIsMutable(); + templateChain_.add(s); + break; + } // case 74 + case 82: { + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute m = + input.readMessage( + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.parser(), + extensionRegistry); + if (attributesBuilder_ == null) { + ensureAttributesIsMutable(); + attributes_.add(m); + } else { + attributesBuilder_.addMessage(m); + } + break; + } // case 82 + default: { + if (!super.parseUnknownField(input, extensionRegistry, tag)) { + done = true; // was an endgroup tag + } + break; + } // default: + } // switch (tag) + } // while (!done) + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.unwrapIOException(); + } finally { + onChanged(); + } // finally + return this; + } + private int bitField0_; + + private int gobjectId_ ; + /** + * int32 gobject_id = 1; + * @return The gobjectId. + */ + @java.lang.Override + public int getGobjectId() { + return gobjectId_; + } + /** + * int32 gobject_id = 1; + * @param value The gobjectId to set. + * @return This builder for chaining. + */ + public Builder setGobjectId(int value) { + + gobjectId_ = value; + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + /** + * int32 gobject_id = 1; + * @return This builder for chaining. + */ + public Builder clearGobjectId() { + bitField0_ = (bitField0_ & ~0x00000001); + gobjectId_ = 0; + onChanged(); + return this; + } + + private java.lang.Object tagName_ = ""; + /** + * string tag_name = 2; + * @return The tagName. + */ + public java.lang.String getTagName() { + java.lang.Object ref = tagName_; + if (!(ref instanceof java.lang.String)) { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + tagName_ = s; + return s; + } else { + return (java.lang.String) ref; + } + } + /** + * string tag_name = 2; + * @return The bytes for tagName. + */ + public com.google.protobuf.ByteString + getTagNameBytes() { + java.lang.Object ref = tagName_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + tagName_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * string tag_name = 2; + * @param value The tagName to set. + * @return This builder for chaining. + */ + public Builder setTagName( + java.lang.String value) { + if (value == null) { throw new NullPointerException(); } + tagName_ = value; + bitField0_ |= 0x00000002; + onChanged(); + return this; + } + /** + * string tag_name = 2; + * @return This builder for chaining. + */ + public Builder clearTagName() { + tagName_ = getDefaultInstance().getTagName(); + bitField0_ = (bitField0_ & ~0x00000002); + onChanged(); + return this; + } + /** + * string tag_name = 2; + * @param value The bytes for tagName to set. + * @return This builder for chaining. + */ + public Builder setTagNameBytes( + com.google.protobuf.ByteString value) { + if (value == null) { throw new NullPointerException(); } + checkByteStringIsUtf8(value); + tagName_ = value; + bitField0_ |= 0x00000002; + onChanged(); + return this; + } + + private java.lang.Object containedName_ = ""; + /** + * string contained_name = 3; + * @return The containedName. + */ + public java.lang.String getContainedName() { + java.lang.Object ref = containedName_; + if (!(ref instanceof java.lang.String)) { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + containedName_ = s; + return s; + } else { + return (java.lang.String) ref; + } + } + /** + * string contained_name = 3; + * @return The bytes for containedName. + */ + public com.google.protobuf.ByteString + getContainedNameBytes() { + java.lang.Object ref = containedName_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + containedName_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * string contained_name = 3; + * @param value The containedName to set. + * @return This builder for chaining. + */ + public Builder setContainedName( + java.lang.String value) { + if (value == null) { throw new NullPointerException(); } + containedName_ = value; + bitField0_ |= 0x00000004; + onChanged(); + return this; + } + /** + * string contained_name = 3; + * @return This builder for chaining. + */ + public Builder clearContainedName() { + containedName_ = getDefaultInstance().getContainedName(); + bitField0_ = (bitField0_ & ~0x00000004); + onChanged(); + return this; + } + /** + * string contained_name = 3; + * @param value The bytes for containedName to set. + * @return This builder for chaining. + */ + public Builder setContainedNameBytes( + com.google.protobuf.ByteString value) { + if (value == null) { throw new NullPointerException(); } + checkByteStringIsUtf8(value); + containedName_ = value; + bitField0_ |= 0x00000004; + onChanged(); + return this; + } + + private java.lang.Object browseName_ = ""; + /** + * string browse_name = 4; + * @return The browseName. + */ + public java.lang.String getBrowseName() { + java.lang.Object ref = browseName_; + if (!(ref instanceof java.lang.String)) { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + browseName_ = s; + return s; + } else { + return (java.lang.String) ref; + } + } + /** + * string browse_name = 4; + * @return The bytes for browseName. + */ + public com.google.protobuf.ByteString + getBrowseNameBytes() { + java.lang.Object ref = browseName_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + browseName_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * string browse_name = 4; + * @param value The browseName to set. + * @return This builder for chaining. + */ + public Builder setBrowseName( + java.lang.String value) { + if (value == null) { throw new NullPointerException(); } + browseName_ = value; + bitField0_ |= 0x00000008; + onChanged(); + return this; + } + /** + * string browse_name = 4; + * @return This builder for chaining. + */ + public Builder clearBrowseName() { + browseName_ = getDefaultInstance().getBrowseName(); + bitField0_ = (bitField0_ & ~0x00000008); + onChanged(); + return this; + } + /** + * string browse_name = 4; + * @param value The bytes for browseName to set. + * @return This builder for chaining. + */ + public Builder setBrowseNameBytes( + com.google.protobuf.ByteString value) { + if (value == null) { throw new NullPointerException(); } + checkByteStringIsUtf8(value); + browseName_ = value; + bitField0_ |= 0x00000008; + onChanged(); + return this; + } + + private int parentGobjectId_ ; + /** + * int32 parent_gobject_id = 5; + * @return The parentGobjectId. + */ + @java.lang.Override + public int getParentGobjectId() { + return parentGobjectId_; + } + /** + * int32 parent_gobject_id = 5; + * @param value The parentGobjectId to set. + * @return This builder for chaining. + */ + public Builder setParentGobjectId(int value) { + + parentGobjectId_ = value; + bitField0_ |= 0x00000010; + onChanged(); + return this; + } + /** + * int32 parent_gobject_id = 5; + * @return This builder for chaining. + */ + public Builder clearParentGobjectId() { + bitField0_ = (bitField0_ & ~0x00000010); + parentGobjectId_ = 0; + onChanged(); + return this; + } + + private boolean isArea_ ; + /** + * bool is_area = 6; + * @return The isArea. + */ + @java.lang.Override + public boolean getIsArea() { + return isArea_; + } + /** + * bool is_area = 6; + * @param value The isArea to set. + * @return This builder for chaining. + */ + public Builder setIsArea(boolean value) { + + isArea_ = value; + bitField0_ |= 0x00000020; + onChanged(); + return this; + } + /** + * bool is_area = 6; + * @return This builder for chaining. + */ + public Builder clearIsArea() { + bitField0_ = (bitField0_ & ~0x00000020); + isArea_ = false; + onChanged(); + return this; + } + + private int categoryId_ ; + /** + * int32 category_id = 7; + * @return The categoryId. + */ + @java.lang.Override + public int getCategoryId() { + return categoryId_; + } + /** + * int32 category_id = 7; + * @param value The categoryId to set. + * @return This builder for chaining. + */ + public Builder setCategoryId(int value) { + + categoryId_ = value; + bitField0_ |= 0x00000040; + onChanged(); + return this; + } + /** + * int32 category_id = 7; + * @return This builder for chaining. + */ + public Builder clearCategoryId() { + bitField0_ = (bitField0_ & ~0x00000040); + categoryId_ = 0; + onChanged(); + return this; + } + + private int hostedByGobjectId_ ; + /** + * int32 hosted_by_gobject_id = 8; + * @return The hostedByGobjectId. + */ + @java.lang.Override + public int getHostedByGobjectId() { + return hostedByGobjectId_; + } + /** + * int32 hosted_by_gobject_id = 8; + * @param value The hostedByGobjectId to set. + * @return This builder for chaining. + */ + public Builder setHostedByGobjectId(int value) { + + hostedByGobjectId_ = value; + bitField0_ |= 0x00000080; + onChanged(); + return this; + } + /** + * int32 hosted_by_gobject_id = 8; + * @return This builder for chaining. + */ + public Builder clearHostedByGobjectId() { + bitField0_ = (bitField0_ & ~0x00000080); + hostedByGobjectId_ = 0; + onChanged(); + return this; + } + + private com.google.protobuf.LazyStringArrayList templateChain_ = + com.google.protobuf.LazyStringArrayList.emptyList(); + private void ensureTemplateChainIsMutable() { + if (!templateChain_.isModifiable()) { + templateChain_ = new com.google.protobuf.LazyStringArrayList(templateChain_); + } + bitField0_ |= 0x00000100; + } + /** + * repeated string template_chain = 9; + * @return A list containing the templateChain. + */ + public com.google.protobuf.ProtocolStringList + getTemplateChainList() { + templateChain_.makeImmutable(); + return templateChain_; + } + /** + * repeated string template_chain = 9; + * @return The count of templateChain. + */ + public int getTemplateChainCount() { + return templateChain_.size(); + } + /** + * repeated string template_chain = 9; + * @param index The index of the element to return. + * @return The templateChain at the given index. + */ + public java.lang.String getTemplateChain(int index) { + return templateChain_.get(index); + } + /** + * repeated string template_chain = 9; + * @param index The index of the value to return. + * @return The bytes of the templateChain at the given index. + */ + public com.google.protobuf.ByteString + getTemplateChainBytes(int index) { + return templateChain_.getByteString(index); + } + /** + * repeated string template_chain = 9; + * @param index The index to set the value at. + * @param value The templateChain to set. + * @return This builder for chaining. + */ + public Builder setTemplateChain( + int index, java.lang.String value) { + if (value == null) { throw new NullPointerException(); } + ensureTemplateChainIsMutable(); + templateChain_.set(index, value); + bitField0_ |= 0x00000100; + onChanged(); + return this; + } + /** + * repeated string template_chain = 9; + * @param value The templateChain to add. + * @return This builder for chaining. + */ + public Builder addTemplateChain( + java.lang.String value) { + if (value == null) { throw new NullPointerException(); } + ensureTemplateChainIsMutable(); + templateChain_.add(value); + bitField0_ |= 0x00000100; + onChanged(); + return this; + } + /** + * repeated string template_chain = 9; + * @param values The templateChain to add. + * @return This builder for chaining. + */ + public Builder addAllTemplateChain( + java.lang.Iterable values) { + ensureTemplateChainIsMutable(); + com.google.protobuf.AbstractMessageLite.Builder.addAll( + values, templateChain_); + bitField0_ |= 0x00000100; + onChanged(); + return this; + } + /** + * repeated string template_chain = 9; + * @return This builder for chaining. + */ + public Builder clearTemplateChain() { + templateChain_ = + com.google.protobuf.LazyStringArrayList.emptyList(); + bitField0_ = (bitField0_ & ~0x00000100);; + onChanged(); + return this; + } + /** + * repeated string template_chain = 9; + * @param value The bytes of the templateChain to add. + * @return This builder for chaining. + */ + public Builder addTemplateChainBytes( + com.google.protobuf.ByteString value) { + if (value == null) { throw new NullPointerException(); } + checkByteStringIsUtf8(value); + ensureTemplateChainIsMutable(); + templateChain_.add(value); + bitField0_ |= 0x00000100; + onChanged(); + return this; + } + + private java.util.List attributes_ = + java.util.Collections.emptyList(); + private void ensureAttributesIsMutable() { + if (!((bitField0_ & 0x00000200) != 0)) { + attributes_ = new java.util.ArrayList(attributes_); + bitField0_ |= 0x00000200; + } + } + + private com.google.protobuf.RepeatedFieldBuilder< + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.Builder, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttributeOrBuilder> attributesBuilder_; + + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + public java.util.List getAttributesList() { + if (attributesBuilder_ == null) { + return java.util.Collections.unmodifiableList(attributes_); + } else { + return attributesBuilder_.getMessageList(); + } + } + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + public int getAttributesCount() { + if (attributesBuilder_ == null) { + return attributes_.size(); + } else { + return attributesBuilder_.getCount(); + } + } + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute getAttributes(int index) { + if (attributesBuilder_ == null) { + return attributes_.get(index); + } else { + return attributesBuilder_.getMessage(index); + } + } + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + public Builder setAttributes( + int index, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute value) { + if (attributesBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureAttributesIsMutable(); + attributes_.set(index, value); + onChanged(); + } else { + attributesBuilder_.setMessage(index, value); + } + return this; + } + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + public Builder setAttributes( + int index, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.Builder builderForValue) { + if (attributesBuilder_ == null) { + ensureAttributesIsMutable(); + attributes_.set(index, builderForValue.build()); + onChanged(); + } else { + attributesBuilder_.setMessage(index, builderForValue.build()); + } + return this; + } + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + public Builder addAttributes(galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute value) { + if (attributesBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureAttributesIsMutable(); + attributes_.add(value); + onChanged(); + } else { + attributesBuilder_.addMessage(value); + } + return this; + } + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + public Builder addAttributes( + int index, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute value) { + if (attributesBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureAttributesIsMutable(); + attributes_.add(index, value); + onChanged(); + } else { + attributesBuilder_.addMessage(index, value); + } + return this; + } + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + public Builder addAttributes( + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.Builder builderForValue) { + if (attributesBuilder_ == null) { + ensureAttributesIsMutable(); + attributes_.add(builderForValue.build()); + onChanged(); + } else { + attributesBuilder_.addMessage(builderForValue.build()); + } + return this; + } + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + public Builder addAttributes( + int index, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.Builder builderForValue) { + if (attributesBuilder_ == null) { + ensureAttributesIsMutable(); + attributes_.add(index, builderForValue.build()); + onChanged(); + } else { + attributesBuilder_.addMessage(index, builderForValue.build()); + } + return this; + } + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + public Builder addAllAttributes( + java.lang.Iterable values) { + if (attributesBuilder_ == null) { + ensureAttributesIsMutable(); + com.google.protobuf.AbstractMessageLite.Builder.addAll( + values, attributes_); + onChanged(); + } else { + attributesBuilder_.addAllMessages(values); + } + return this; + } + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + public Builder clearAttributes() { + if (attributesBuilder_ == null) { + attributes_ = java.util.Collections.emptyList(); + bitField0_ = (bitField0_ & ~0x00000200); + onChanged(); + } else { + attributesBuilder_.clear(); + } + return this; + } + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + public Builder removeAttributes(int index) { + if (attributesBuilder_ == null) { + ensureAttributesIsMutable(); + attributes_.remove(index); + onChanged(); + } else { + attributesBuilder_.remove(index); + } + return this; + } + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.Builder getAttributesBuilder( + int index) { + return internalGetAttributesFieldBuilder().getBuilder(index); + } + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttributeOrBuilder getAttributesOrBuilder( + int index) { + if (attributesBuilder_ == null) { + return attributes_.get(index); } else { + return attributesBuilder_.getMessageOrBuilder(index); + } + } + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + public java.util.List + getAttributesOrBuilderList() { + if (attributesBuilder_ != null) { + return attributesBuilder_.getMessageOrBuilderList(); + } else { + return java.util.Collections.unmodifiableList(attributes_); + } + } + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.Builder addAttributesBuilder() { + return internalGetAttributesFieldBuilder().addBuilder( + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.getDefaultInstance()); + } + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.Builder addAttributesBuilder( + int index) { + return internalGetAttributesFieldBuilder().addBuilder( + index, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.getDefaultInstance()); + } + /** + * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; + */ + public java.util.List + getAttributesBuilderList() { + return internalGetAttributesFieldBuilder().getBuilderList(); + } + private com.google.protobuf.RepeatedFieldBuilder< + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.Builder, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttributeOrBuilder> + internalGetAttributesFieldBuilder() { + if (attributesBuilder_ == null) { + attributesBuilder_ = new com.google.protobuf.RepeatedFieldBuilder< + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.Builder, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttributeOrBuilder>( + attributes_, + ((bitField0_ & 0x00000200) != 0), + getParentForChildren(), + isClean()); + attributes_ = null; + } + return attributesBuilder_; + } + + // @@protoc_insertion_point(builder_scope:galaxy_repository.v1.GalaxyObject) + } + + // @@protoc_insertion_point(class_scope:galaxy_repository.v1.GalaxyObject) + private static final galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject(); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public GalaxyObject parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + Builder builder = newBuilder(); + try { + builder.mergeFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(builder.buildPartial()); + } catch (com.google.protobuf.UninitializedMessageException e) { + throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException(e) + .setUnfinishedMessage(builder.buildPartial()); + } + return builder.buildPartial(); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + + } + + public interface GalaxyAttributeOrBuilder extends + // @@protoc_insertion_point(interface_extends:galaxy_repository.v1.GalaxyAttribute) + com.google.protobuf.MessageOrBuilder { + + /** + * string attribute_name = 1; + * @return The attributeName. + */ + java.lang.String getAttributeName(); + /** + * string attribute_name = 1; + * @return The bytes for attributeName. + */ + com.google.protobuf.ByteString + getAttributeNameBytes(); + + /** + * string full_tag_reference = 2; + * @return The fullTagReference. + */ + java.lang.String getFullTagReference(); + /** + * string full_tag_reference = 2; + * @return The bytes for fullTagReference. + */ + com.google.protobuf.ByteString + getFullTagReferenceBytes(); + + /** + * int32 mx_data_type = 3; + * @return The mxDataType. + */ + int getMxDataType(); + + /** + * string data_type_name = 4; + * @return The dataTypeName. + */ + java.lang.String getDataTypeName(); + /** + * string data_type_name = 4; + * @return The bytes for dataTypeName. + */ + com.google.protobuf.ByteString + getDataTypeNameBytes(); + + /** + * bool is_array = 5; + * @return The isArray. + */ + boolean getIsArray(); + + /** + * int32 array_dimension = 6; + * @return The arrayDimension. + */ + int getArrayDimension(); + + /** + * bool array_dimension_present = 7; + * @return The arrayDimensionPresent. + */ + boolean getArrayDimensionPresent(); + + /** + * int32 mx_attribute_category = 8; + * @return The mxAttributeCategory. + */ + int getMxAttributeCategory(); + + /** + * int32 security_classification = 9; + * @return The securityClassification. + */ + int getSecurityClassification(); + + /** + * bool is_historized = 10; + * @return The isHistorized. + */ + boolean getIsHistorized(); + + /** + * bool is_alarm = 11; + * @return The isAlarm. + */ + boolean getIsAlarm(); + } + /** + * Protobuf type {@code galaxy_repository.v1.GalaxyAttribute} + */ + public static final class GalaxyAttribute extends + com.google.protobuf.GeneratedMessage implements + // @@protoc_insertion_point(message_implements:galaxy_repository.v1.GalaxyAttribute) + GalaxyAttributeOrBuilder { + private static final long serialVersionUID = 0L; + static { + com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( + com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, + /* major= */ 4, + /* minor= */ 33, + /* patch= */ 1, + /* suffix= */ "", + "GalaxyAttribute"); + } + // Use GalaxyAttribute.newBuilder() to construct. + private GalaxyAttribute(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + } + private GalaxyAttribute() { + attributeName_ = ""; + fullTagReference_ = ""; + dataTypeName_ = ""; + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_GalaxyAttribute_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_GalaxyAttribute_fieldAccessorTable + .ensureFieldAccessorsInitialized( + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.class, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.Builder.class); + } + + public static final int ATTRIBUTE_NAME_FIELD_NUMBER = 1; + @SuppressWarnings("serial") + private volatile java.lang.Object attributeName_ = ""; + /** + * string attribute_name = 1; + * @return The attributeName. + */ + @java.lang.Override + public java.lang.String getAttributeName() { + java.lang.Object ref = attributeName_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + attributeName_ = s; + return s; + } + } + /** + * string attribute_name = 1; + * @return The bytes for attributeName. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getAttributeNameBytes() { + java.lang.Object ref = attributeName_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + attributeName_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + public static final int FULL_TAG_REFERENCE_FIELD_NUMBER = 2; + @SuppressWarnings("serial") + private volatile java.lang.Object fullTagReference_ = ""; + /** + * string full_tag_reference = 2; + * @return The fullTagReference. + */ + @java.lang.Override + public java.lang.String getFullTagReference() { + java.lang.Object ref = fullTagReference_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + fullTagReference_ = s; + return s; + } + } + /** + * string full_tag_reference = 2; + * @return The bytes for fullTagReference. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getFullTagReferenceBytes() { + java.lang.Object ref = fullTagReference_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + fullTagReference_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + public static final int MX_DATA_TYPE_FIELD_NUMBER = 3; + private int mxDataType_ = 0; + /** + * int32 mx_data_type = 3; + * @return The mxDataType. + */ + @java.lang.Override + public int getMxDataType() { + return mxDataType_; + } + + public static final int DATA_TYPE_NAME_FIELD_NUMBER = 4; + @SuppressWarnings("serial") + private volatile java.lang.Object dataTypeName_ = ""; + /** + * string data_type_name = 4; + * @return The dataTypeName. + */ + @java.lang.Override + public java.lang.String getDataTypeName() { + java.lang.Object ref = dataTypeName_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + dataTypeName_ = s; + return s; + } + } + /** + * string data_type_name = 4; + * @return The bytes for dataTypeName. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getDataTypeNameBytes() { + java.lang.Object ref = dataTypeName_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + dataTypeName_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + public static final int IS_ARRAY_FIELD_NUMBER = 5; + private boolean isArray_ = false; + /** + * bool is_array = 5; + * @return The isArray. + */ + @java.lang.Override + public boolean getIsArray() { + return isArray_; + } + + public static final int ARRAY_DIMENSION_FIELD_NUMBER = 6; + private int arrayDimension_ = 0; + /** + * int32 array_dimension = 6; + * @return The arrayDimension. + */ + @java.lang.Override + public int getArrayDimension() { + return arrayDimension_; + } + + public static final int ARRAY_DIMENSION_PRESENT_FIELD_NUMBER = 7; + private boolean arrayDimensionPresent_ = false; + /** + * bool array_dimension_present = 7; + * @return The arrayDimensionPresent. + */ + @java.lang.Override + public boolean getArrayDimensionPresent() { + return arrayDimensionPresent_; + } + + public static final int MX_ATTRIBUTE_CATEGORY_FIELD_NUMBER = 8; + private int mxAttributeCategory_ = 0; + /** + * int32 mx_attribute_category = 8; + * @return The mxAttributeCategory. + */ + @java.lang.Override + public int getMxAttributeCategory() { + return mxAttributeCategory_; + } + + public static final int SECURITY_CLASSIFICATION_FIELD_NUMBER = 9; + private int securityClassification_ = 0; + /** + * int32 security_classification = 9; + * @return The securityClassification. + */ + @java.lang.Override + public int getSecurityClassification() { + return securityClassification_; + } + + public static final int IS_HISTORIZED_FIELD_NUMBER = 10; + private boolean isHistorized_ = false; + /** + * bool is_historized = 10; + * @return The isHistorized. + */ + @java.lang.Override + public boolean getIsHistorized() { + return isHistorized_; + } + + public static final int IS_ALARM_FIELD_NUMBER = 11; + private boolean isAlarm_ = false; + /** + * bool is_alarm = 11; + * @return The isAlarm. + */ + @java.lang.Override + public boolean getIsAlarm() { + return isAlarm_; + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(attributeName_)) { + com.google.protobuf.GeneratedMessage.writeString(output, 1, attributeName_); + } + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(fullTagReference_)) { + com.google.protobuf.GeneratedMessage.writeString(output, 2, fullTagReference_); + } + if (mxDataType_ != 0) { + output.writeInt32(3, mxDataType_); + } + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(dataTypeName_)) { + com.google.protobuf.GeneratedMessage.writeString(output, 4, dataTypeName_); + } + if (isArray_ != false) { + output.writeBool(5, isArray_); + } + if (arrayDimension_ != 0) { + output.writeInt32(6, arrayDimension_); + } + if (arrayDimensionPresent_ != false) { + output.writeBool(7, arrayDimensionPresent_); + } + if (mxAttributeCategory_ != 0) { + output.writeInt32(8, mxAttributeCategory_); + } + if (securityClassification_ != 0) { + output.writeInt32(9, securityClassification_); + } + if (isHistorized_ != false) { + output.writeBool(10, isHistorized_); + } + if (isAlarm_ != false) { + output.writeBool(11, isAlarm_); + } + getUnknownFields().writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(attributeName_)) { + size += com.google.protobuf.GeneratedMessage.computeStringSize(1, attributeName_); + } + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(fullTagReference_)) { + size += com.google.protobuf.GeneratedMessage.computeStringSize(2, fullTagReference_); + } + if (mxDataType_ != 0) { + size += com.google.protobuf.CodedOutputStream + .computeInt32Size(3, mxDataType_); + } + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(dataTypeName_)) { + size += com.google.protobuf.GeneratedMessage.computeStringSize(4, dataTypeName_); + } + if (isArray_ != false) { + size += com.google.protobuf.CodedOutputStream + .computeBoolSize(5, isArray_); + } + if (arrayDimension_ != 0) { + size += com.google.protobuf.CodedOutputStream + .computeInt32Size(6, arrayDimension_); + } + if (arrayDimensionPresent_ != false) { + size += com.google.protobuf.CodedOutputStream + .computeBoolSize(7, arrayDimensionPresent_); + } + if (mxAttributeCategory_ != 0) { + size += com.google.protobuf.CodedOutputStream + .computeInt32Size(8, mxAttributeCategory_); + } + if (securityClassification_ != 0) { + size += com.google.protobuf.CodedOutputStream + .computeInt32Size(9, securityClassification_); + } + if (isHistorized_ != false) { + size += com.google.protobuf.CodedOutputStream + .computeBoolSize(10, isHistorized_); + } + if (isAlarm_ != false) { + size += com.google.protobuf.CodedOutputStream + .computeBoolSize(11, isAlarm_); + } + size += getUnknownFields().getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute)) { + return super.equals(obj); + } + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute other = (galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute) obj; + + if (!getAttributeName() + .equals(other.getAttributeName())) return false; + if (!getFullTagReference() + .equals(other.getFullTagReference())) return false; + if (getMxDataType() + != other.getMxDataType()) return false; + if (!getDataTypeName() + .equals(other.getDataTypeName())) return false; + if (getIsArray() + != other.getIsArray()) return false; + if (getArrayDimension() + != other.getArrayDimension()) return false; + if (getArrayDimensionPresent() + != other.getArrayDimensionPresent()) return false; + if (getMxAttributeCategory() + != other.getMxAttributeCategory()) return false; + if (getSecurityClassification() + != other.getSecurityClassification()) return false; + if (getIsHistorized() + != other.getIsHistorized()) return false; + if (getIsAlarm() + != other.getIsAlarm()) return false; + if (!getUnknownFields().equals(other.getUnknownFields())) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + hash = (37 * hash) + ATTRIBUTE_NAME_FIELD_NUMBER; + hash = (53 * hash) + getAttributeName().hashCode(); + hash = (37 * hash) + FULL_TAG_REFERENCE_FIELD_NUMBER; + hash = (53 * hash) + getFullTagReference().hashCode(); + hash = (37 * hash) + MX_DATA_TYPE_FIELD_NUMBER; + hash = (53 * hash) + getMxDataType(); + hash = (37 * hash) + DATA_TYPE_NAME_FIELD_NUMBER; + hash = (53 * hash) + getDataTypeName().hashCode(); + hash = (37 * hash) + IS_ARRAY_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashBoolean( + getIsArray()); + hash = (37 * hash) + ARRAY_DIMENSION_FIELD_NUMBER; + hash = (53 * hash) + getArrayDimension(); + hash = (37 * hash) + ARRAY_DIMENSION_PRESENT_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashBoolean( + getArrayDimensionPresent()); + hash = (37 * hash) + MX_ATTRIBUTE_CATEGORY_FIELD_NUMBER; + hash = (53 * hash) + getMxAttributeCategory(); + hash = (37 * hash) + SECURITY_CLASSIFICATION_FIELD_NUMBER; + hash = (53 * hash) + getSecurityClassification(); + hash = (37 * hash) + IS_HISTORIZED_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashBoolean( + getIsHistorized()); + hash = (37 * hash) + IS_ALARM_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashBoolean( + getIsAlarm()); + hash = (29 * hash) + getUnknownFields().hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code galaxy_repository.v1.GalaxyAttribute} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder implements + // @@protoc_insertion_point(builder_implements:galaxy_repository.v1.GalaxyAttribute) + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttributeOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_GalaxyAttribute_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_GalaxyAttribute_fieldAccessorTable + .ensureFieldAccessorsInitialized( + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.class, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.Builder.class); + } + + // Construct using galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.newBuilder() + private Builder() { + + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + + } + @java.lang.Override + public Builder clear() { + super.clear(); + bitField0_ = 0; + attributeName_ = ""; + fullTagReference_ = ""; + mxDataType_ = 0; + dataTypeName_ = ""; + isArray_ = false; + arrayDimension_ = 0; + arrayDimensionPresent_ = false; + mxAttributeCategory_ = 0; + securityClassification_ = 0; + isHistorized_ = false; + isAlarm_ = false; + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.internal_static_galaxy_repository_v1_GalaxyAttribute_descriptor; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute getDefaultInstanceForType() { + return galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.getDefaultInstance(); + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute build() { + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute buildPartial() { + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute result = new galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute(this); + if (bitField0_ != 0) { buildPartial0(result); } + onBuilt(); + return result; + } + + private void buildPartial0(galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute result) { + int from_bitField0_ = bitField0_; + if (((from_bitField0_ & 0x00000001) != 0)) { + result.attributeName_ = attributeName_; + } + if (((from_bitField0_ & 0x00000002) != 0)) { + result.fullTagReference_ = fullTagReference_; + } + if (((from_bitField0_ & 0x00000004) != 0)) { + result.mxDataType_ = mxDataType_; + } + if (((from_bitField0_ & 0x00000008) != 0)) { + result.dataTypeName_ = dataTypeName_; + } + if (((from_bitField0_ & 0x00000010) != 0)) { + result.isArray_ = isArray_; + } + if (((from_bitField0_ & 0x00000020) != 0)) { + result.arrayDimension_ = arrayDimension_; + } + if (((from_bitField0_ & 0x00000040) != 0)) { + result.arrayDimensionPresent_ = arrayDimensionPresent_; + } + if (((from_bitField0_ & 0x00000080) != 0)) { + result.mxAttributeCategory_ = mxAttributeCategory_; + } + if (((from_bitField0_ & 0x00000100) != 0)) { + result.securityClassification_ = securityClassification_; + } + if (((from_bitField0_ & 0x00000200) != 0)) { + result.isHistorized_ = isHistorized_; + } + if (((from_bitField0_ & 0x00000400) != 0)) { + result.isAlarm_ = isAlarm_; + } + } + + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute) { + return mergeFrom((galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute other) { + if (other == galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.getDefaultInstance()) return this; + if (!other.getAttributeName().isEmpty()) { + attributeName_ = other.attributeName_; + bitField0_ |= 0x00000001; + onChanged(); + } + if (!other.getFullTagReference().isEmpty()) { + fullTagReference_ = other.fullTagReference_; + bitField0_ |= 0x00000002; + onChanged(); + } + if (other.getMxDataType() != 0) { + setMxDataType(other.getMxDataType()); + } + if (!other.getDataTypeName().isEmpty()) { + dataTypeName_ = other.dataTypeName_; + bitField0_ |= 0x00000008; + onChanged(); + } + if (other.getIsArray() != false) { + setIsArray(other.getIsArray()); + } + if (other.getArrayDimension() != 0) { + setArrayDimension(other.getArrayDimension()); + } + if (other.getArrayDimensionPresent() != false) { + setArrayDimensionPresent(other.getArrayDimensionPresent()); + } + if (other.getMxAttributeCategory() != 0) { + setMxAttributeCategory(other.getMxAttributeCategory()); + } + if (other.getSecurityClassification() != 0) { + setSecurityClassification(other.getSecurityClassification()); + } + if (other.getIsHistorized() != false) { + setIsHistorized(other.getIsHistorized()); + } + if (other.getIsAlarm() != false) { + setIsAlarm(other.getIsAlarm()); + } + this.mergeUnknownFields(other.getUnknownFields()); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 10: { + attributeName_ = input.readStringRequireUtf8(); + bitField0_ |= 0x00000001; + break; + } // case 10 + case 18: { + fullTagReference_ = input.readStringRequireUtf8(); + bitField0_ |= 0x00000002; + break; + } // case 18 + case 24: { + mxDataType_ = input.readInt32(); + bitField0_ |= 0x00000004; + break; + } // case 24 + case 34: { + dataTypeName_ = input.readStringRequireUtf8(); + bitField0_ |= 0x00000008; + break; + } // case 34 + case 40: { + isArray_ = input.readBool(); + bitField0_ |= 0x00000010; + break; + } // case 40 + case 48: { + arrayDimension_ = input.readInt32(); + bitField0_ |= 0x00000020; + break; + } // case 48 + case 56: { + arrayDimensionPresent_ = input.readBool(); + bitField0_ |= 0x00000040; + break; + } // case 56 + case 64: { + mxAttributeCategory_ = input.readInt32(); + bitField0_ |= 0x00000080; + break; + } // case 64 + case 72: { + securityClassification_ = input.readInt32(); + bitField0_ |= 0x00000100; + break; + } // case 72 + case 80: { + isHistorized_ = input.readBool(); + bitField0_ |= 0x00000200; + break; + } // case 80 + case 88: { + isAlarm_ = input.readBool(); + bitField0_ |= 0x00000400; + break; + } // case 88 + default: { + if (!super.parseUnknownField(input, extensionRegistry, tag)) { + done = true; // was an endgroup tag + } + break; + } // default: + } // switch (tag) + } // while (!done) + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.unwrapIOException(); + } finally { + onChanged(); + } // finally + return this; + } + private int bitField0_; + + private java.lang.Object attributeName_ = ""; + /** + * string attribute_name = 1; + * @return The attributeName. + */ + public java.lang.String getAttributeName() { + java.lang.Object ref = attributeName_; + if (!(ref instanceof java.lang.String)) { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + attributeName_ = s; + return s; + } else { + return (java.lang.String) ref; + } + } + /** + * string attribute_name = 1; + * @return The bytes for attributeName. + */ + public com.google.protobuf.ByteString + getAttributeNameBytes() { + java.lang.Object ref = attributeName_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + attributeName_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * string attribute_name = 1; + * @param value The attributeName to set. + * @return This builder for chaining. + */ + public Builder setAttributeName( + java.lang.String value) { + if (value == null) { throw new NullPointerException(); } + attributeName_ = value; + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + /** + * string attribute_name = 1; + * @return This builder for chaining. + */ + public Builder clearAttributeName() { + attributeName_ = getDefaultInstance().getAttributeName(); + bitField0_ = (bitField0_ & ~0x00000001); + onChanged(); + return this; + } + /** + * string attribute_name = 1; + * @param value The bytes for attributeName to set. + * @return This builder for chaining. + */ + public Builder setAttributeNameBytes( + com.google.protobuf.ByteString value) { + if (value == null) { throw new NullPointerException(); } + checkByteStringIsUtf8(value); + attributeName_ = value; + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + + private java.lang.Object fullTagReference_ = ""; + /** + * string full_tag_reference = 2; + * @return The fullTagReference. + */ + public java.lang.String getFullTagReference() { + java.lang.Object ref = fullTagReference_; + if (!(ref instanceof java.lang.String)) { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + fullTagReference_ = s; + return s; + } else { + return (java.lang.String) ref; + } + } + /** + * string full_tag_reference = 2; + * @return The bytes for fullTagReference. + */ + public com.google.protobuf.ByteString + getFullTagReferenceBytes() { + java.lang.Object ref = fullTagReference_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + fullTagReference_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * string full_tag_reference = 2; + * @param value The fullTagReference to set. + * @return This builder for chaining. + */ + public Builder setFullTagReference( + java.lang.String value) { + if (value == null) { throw new NullPointerException(); } + fullTagReference_ = value; + bitField0_ |= 0x00000002; + onChanged(); + return this; + } + /** + * string full_tag_reference = 2; + * @return This builder for chaining. + */ + public Builder clearFullTagReference() { + fullTagReference_ = getDefaultInstance().getFullTagReference(); + bitField0_ = (bitField0_ & ~0x00000002); + onChanged(); + return this; + } + /** + * string full_tag_reference = 2; + * @param value The bytes for fullTagReference to set. + * @return This builder for chaining. + */ + public Builder setFullTagReferenceBytes( + com.google.protobuf.ByteString value) { + if (value == null) { throw new NullPointerException(); } + checkByteStringIsUtf8(value); + fullTagReference_ = value; + bitField0_ |= 0x00000002; + onChanged(); + return this; + } + + private int mxDataType_ ; + /** + * int32 mx_data_type = 3; + * @return The mxDataType. + */ + @java.lang.Override + public int getMxDataType() { + return mxDataType_; + } + /** + * int32 mx_data_type = 3; + * @param value The mxDataType to set. + * @return This builder for chaining. + */ + public Builder setMxDataType(int value) { + + mxDataType_ = value; + bitField0_ |= 0x00000004; + onChanged(); + return this; + } + /** + * int32 mx_data_type = 3; + * @return This builder for chaining. + */ + public Builder clearMxDataType() { + bitField0_ = (bitField0_ & ~0x00000004); + mxDataType_ = 0; + onChanged(); + return this; + } + + private java.lang.Object dataTypeName_ = ""; + /** + * string data_type_name = 4; + * @return The dataTypeName. + */ + public java.lang.String getDataTypeName() { + java.lang.Object ref = dataTypeName_; + if (!(ref instanceof java.lang.String)) { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + dataTypeName_ = s; + return s; + } else { + return (java.lang.String) ref; + } + } + /** + * string data_type_name = 4; + * @return The bytes for dataTypeName. + */ + public com.google.protobuf.ByteString + getDataTypeNameBytes() { + java.lang.Object ref = dataTypeName_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + dataTypeName_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * string data_type_name = 4; + * @param value The dataTypeName to set. + * @return This builder for chaining. + */ + public Builder setDataTypeName( + java.lang.String value) { + if (value == null) { throw new NullPointerException(); } + dataTypeName_ = value; + bitField0_ |= 0x00000008; + onChanged(); + return this; + } + /** + * string data_type_name = 4; + * @return This builder for chaining. + */ + public Builder clearDataTypeName() { + dataTypeName_ = getDefaultInstance().getDataTypeName(); + bitField0_ = (bitField0_ & ~0x00000008); + onChanged(); + return this; + } + /** + * string data_type_name = 4; + * @param value The bytes for dataTypeName to set. + * @return This builder for chaining. + */ + public Builder setDataTypeNameBytes( + com.google.protobuf.ByteString value) { + if (value == null) { throw new NullPointerException(); } + checkByteStringIsUtf8(value); + dataTypeName_ = value; + bitField0_ |= 0x00000008; + onChanged(); + return this; + } + + private boolean isArray_ ; + /** + * bool is_array = 5; + * @return The isArray. + */ + @java.lang.Override + public boolean getIsArray() { + return isArray_; + } + /** + * bool is_array = 5; + * @param value The isArray to set. + * @return This builder for chaining. + */ + public Builder setIsArray(boolean value) { + + isArray_ = value; + bitField0_ |= 0x00000010; + onChanged(); + return this; + } + /** + * bool is_array = 5; + * @return This builder for chaining. + */ + public Builder clearIsArray() { + bitField0_ = (bitField0_ & ~0x00000010); + isArray_ = false; + onChanged(); + return this; + } + + private int arrayDimension_ ; + /** + * int32 array_dimension = 6; + * @return The arrayDimension. + */ + @java.lang.Override + public int getArrayDimension() { + return arrayDimension_; + } + /** + * int32 array_dimension = 6; + * @param value The arrayDimension to set. + * @return This builder for chaining. + */ + public Builder setArrayDimension(int value) { + + arrayDimension_ = value; + bitField0_ |= 0x00000020; + onChanged(); + return this; + } + /** + * int32 array_dimension = 6; + * @return This builder for chaining. + */ + public Builder clearArrayDimension() { + bitField0_ = (bitField0_ & ~0x00000020); + arrayDimension_ = 0; + onChanged(); + return this; + } + + private boolean arrayDimensionPresent_ ; + /** + * bool array_dimension_present = 7; + * @return The arrayDimensionPresent. + */ + @java.lang.Override + public boolean getArrayDimensionPresent() { + return arrayDimensionPresent_; + } + /** + * bool array_dimension_present = 7; + * @param value The arrayDimensionPresent to set. + * @return This builder for chaining. + */ + public Builder setArrayDimensionPresent(boolean value) { + + arrayDimensionPresent_ = value; + bitField0_ |= 0x00000040; + onChanged(); + return this; + } + /** + * bool array_dimension_present = 7; + * @return This builder for chaining. + */ + public Builder clearArrayDimensionPresent() { + bitField0_ = (bitField0_ & ~0x00000040); + arrayDimensionPresent_ = false; + onChanged(); + return this; + } + + private int mxAttributeCategory_ ; + /** + * int32 mx_attribute_category = 8; + * @return The mxAttributeCategory. + */ + @java.lang.Override + public int getMxAttributeCategory() { + return mxAttributeCategory_; + } + /** + * int32 mx_attribute_category = 8; + * @param value The mxAttributeCategory to set. + * @return This builder for chaining. + */ + public Builder setMxAttributeCategory(int value) { + + mxAttributeCategory_ = value; + bitField0_ |= 0x00000080; + onChanged(); + return this; + } + /** + * int32 mx_attribute_category = 8; + * @return This builder for chaining. + */ + public Builder clearMxAttributeCategory() { + bitField0_ = (bitField0_ & ~0x00000080); + mxAttributeCategory_ = 0; + onChanged(); + return this; + } + + private int securityClassification_ ; + /** + * int32 security_classification = 9; + * @return The securityClassification. + */ + @java.lang.Override + public int getSecurityClassification() { + return securityClassification_; + } + /** + * int32 security_classification = 9; + * @param value The securityClassification to set. + * @return This builder for chaining. + */ + public Builder setSecurityClassification(int value) { + + securityClassification_ = value; + bitField0_ |= 0x00000100; + onChanged(); + return this; + } + /** + * int32 security_classification = 9; + * @return This builder for chaining. + */ + public Builder clearSecurityClassification() { + bitField0_ = (bitField0_ & ~0x00000100); + securityClassification_ = 0; + onChanged(); + return this; + } + + private boolean isHistorized_ ; + /** + * bool is_historized = 10; + * @return The isHistorized. + */ + @java.lang.Override + public boolean getIsHistorized() { + return isHistorized_; + } + /** + * bool is_historized = 10; + * @param value The isHistorized to set. + * @return This builder for chaining. + */ + public Builder setIsHistorized(boolean value) { + + isHistorized_ = value; + bitField0_ |= 0x00000200; + onChanged(); + return this; + } + /** + * bool is_historized = 10; + * @return This builder for chaining. + */ + public Builder clearIsHistorized() { + bitField0_ = (bitField0_ & ~0x00000200); + isHistorized_ = false; + onChanged(); + return this; + } + + private boolean isAlarm_ ; + /** + * bool is_alarm = 11; + * @return The isAlarm. + */ + @java.lang.Override + public boolean getIsAlarm() { + return isAlarm_; + } + /** + * bool is_alarm = 11; + * @param value The isAlarm to set. + * @return This builder for chaining. + */ + public Builder setIsAlarm(boolean value) { + + isAlarm_ = value; + bitField0_ |= 0x00000400; + onChanged(); + return this; + } + /** + * bool is_alarm = 11; + * @return This builder for chaining. + */ + public Builder clearIsAlarm() { + bitField0_ = (bitField0_ & ~0x00000400); + isAlarm_ = false; + onChanged(); + return this; + } + + // @@protoc_insertion_point(builder_scope:galaxy_repository.v1.GalaxyAttribute) + } + + // @@protoc_insertion_point(class_scope:galaxy_repository.v1.GalaxyAttribute) + private static final galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute(); + } + + public static galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public GalaxyAttribute parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + Builder builder = newBuilder(); + try { + builder.mergeFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(builder.buildPartial()); + } catch (com.google.protobuf.UninitializedMessageException e) { + throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException(e) + .setUnfinishedMessage(builder.buildPartial()); + } + return builder.buildPartial(); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + + } + + private static final com.google.protobuf.Descriptors.Descriptor + internal_static_galaxy_repository_v1_TestConnectionRequest_descriptor; + private static final + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_galaxy_repository_v1_TestConnectionRequest_fieldAccessorTable; + private static final com.google.protobuf.Descriptors.Descriptor + internal_static_galaxy_repository_v1_TestConnectionReply_descriptor; + private static final + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_galaxy_repository_v1_TestConnectionReply_fieldAccessorTable; + private static final com.google.protobuf.Descriptors.Descriptor + internal_static_galaxy_repository_v1_GetLastDeployTimeRequest_descriptor; + private static final + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_galaxy_repository_v1_GetLastDeployTimeRequest_fieldAccessorTable; + private static final com.google.protobuf.Descriptors.Descriptor + internal_static_galaxy_repository_v1_GetLastDeployTimeReply_descriptor; + private static final + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_galaxy_repository_v1_GetLastDeployTimeReply_fieldAccessorTable; + private static final com.google.protobuf.Descriptors.Descriptor + internal_static_galaxy_repository_v1_DiscoverHierarchyRequest_descriptor; + private static final + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_galaxy_repository_v1_DiscoverHierarchyRequest_fieldAccessorTable; + private static final com.google.protobuf.Descriptors.Descriptor + internal_static_galaxy_repository_v1_DiscoverHierarchyReply_descriptor; + private static final + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_galaxy_repository_v1_DiscoverHierarchyReply_fieldAccessorTable; + private static final com.google.protobuf.Descriptors.Descriptor + internal_static_galaxy_repository_v1_WatchDeployEventsRequest_descriptor; + private static final + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_galaxy_repository_v1_WatchDeployEventsRequest_fieldAccessorTable; + private static final com.google.protobuf.Descriptors.Descriptor + internal_static_galaxy_repository_v1_DeployEvent_descriptor; + private static final + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_galaxy_repository_v1_DeployEvent_fieldAccessorTable; + private static final com.google.protobuf.Descriptors.Descriptor + internal_static_galaxy_repository_v1_GalaxyObject_descriptor; + private static final + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_galaxy_repository_v1_GalaxyObject_fieldAccessorTable; + private static final com.google.protobuf.Descriptors.Descriptor + internal_static_galaxy_repository_v1_GalaxyAttribute_descriptor; + private static final + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_galaxy_repository_v1_GalaxyAttribute_fieldAccessorTable; + + public static com.google.protobuf.Descriptors.FileDescriptor + getDescriptor() { + return descriptor; + } + private static com.google.protobuf.Descriptors.FileDescriptor + descriptor; + static { + java.lang.String[] descriptorData = { + "\n\027galaxy_repository.proto\022\024galaxy_reposi" + + "tory.v1\032\037google/protobuf/timestamp.proto" + + "\"\027\n\025TestConnectionRequest\"!\n\023TestConnect" + + "ionReply\022\n\n\002ok\030\001 \001(\010\"\032\n\030GetLastDeployTim" + + "eRequest\"b\n\026GetLastDeployTimeReply\022\017\n\007pr" + + "esent\030\001 \001(\010\0227\n\023time_of_last_deploy\030\002 \001(\013" + + "2\032.google.protobuf.Timestamp\"\032\n\030Discover" + + "HierarchyRequest\"M\n\026DiscoverHierarchyRep" + + "ly\0223\n\007objects\030\001 \003(\0132\".galaxy_repository." + + "v1.GalaxyObject\"U\n\030WatchDeployEventsRequ" + + "est\0229\n\025last_seen_deploy_time\030\001 \001(\0132\032.goo" + + "gle.protobuf.Timestamp\"\335\001\n\013DeployEvent\022\020" + + "\n\010sequence\030\001 \001(\004\022/\n\013observed_at\030\002 \001(\0132\032." + + "google.protobuf.Timestamp\0227\n\023time_of_las" + + "t_deploy\030\003 \001(\0132\032.google.protobuf.Timesta" + + "mp\022#\n\033time_of_last_deploy_present\030\004 \001(\010\022" + + "\024\n\014object_count\030\005 \001(\005\022\027\n\017attribute_count" + + "\030\006 \001(\005\"\223\002\n\014GalaxyObject\022\022\n\ngobject_id\030\001 " + + "\001(\005\022\020\n\010tag_name\030\002 \001(\t\022\026\n\016contained_name\030" + + "\003 \001(\t\022\023\n\013browse_name\030\004 \001(\t\022\031\n\021parent_gob" + + "ject_id\030\005 \001(\005\022\017\n\007is_area\030\006 \001(\010\022\023\n\013catego" + + "ry_id\030\007 \001(\005\022\034\n\024hosted_by_gobject_id\030\010 \001(" + + "\005\022\026\n\016template_chain\030\t \003(\t\0229\n\nattributes\030" + + "\n \003(\0132%.galaxy_repository.v1.GalaxyAttri" + + "bute\"\250\002\n\017GalaxyAttribute\022\026\n\016attribute_na" + + "me\030\001 \001(\t\022\032\n\022full_tag_reference\030\002 \001(\t\022\024\n\014" + + "mx_data_type\030\003 \001(\005\022\026\n\016data_type_name\030\004 \001" + + "(\t\022\020\n\010is_array\030\005 \001(\010\022\027\n\017array_dimension\030" + + "\006 \001(\005\022\037\n\027array_dimension_present\030\007 \001(\010\022\035" + + "\n\025mx_attribute_category\030\010 \001(\005\022\037\n\027securit" + + "y_classification\030\t \001(\005\022\025\n\ris_historized\030" + + "\n \001(\010\022\020\n\010is_alarm\030\013 \001(\0102\314\003\n\020GalaxyReposi" + + "tory\022h\n\016TestConnection\022+.galaxy_reposito" + + "ry.v1.TestConnectionRequest\032).galaxy_rep" + + "ository.v1.TestConnectionReply\022q\n\021GetLas" + + "tDeployTime\022..galaxy_repository.v1.GetLa" + + "stDeployTimeRequest\032,.galaxy_repository." + + "v1.GetLastDeployTimeReply\022q\n\021DiscoverHie" + + "rarchy\022..galaxy_repository.v1.DiscoverHi" + + "erarchyRequest\032,.galaxy_repository.v1.Di" + + "scoverHierarchyReply\022h\n\021WatchDeployEvent" + + "s\022..galaxy_repository.v1.WatchDeployEven" + + "tsRequest\032!.galaxy_repository.v1.DeployE" + + "vent0\001B#\252\002 MxGateway.Contracts.Proto.Gal" + + "axyb\006proto3" + }; + descriptor = com.google.protobuf.Descriptors.FileDescriptor + .internalBuildGeneratedFileFrom(descriptorData, + new com.google.protobuf.Descriptors.FileDescriptor[] { + com.google.protobuf.TimestampProto.getDescriptor(), + }); + internal_static_galaxy_repository_v1_TestConnectionRequest_descriptor = + getDescriptor().getMessageType(0); + internal_static_galaxy_repository_v1_TestConnectionRequest_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_galaxy_repository_v1_TestConnectionRequest_descriptor, + new java.lang.String[] { }); + internal_static_galaxy_repository_v1_TestConnectionReply_descriptor = + getDescriptor().getMessageType(1); + internal_static_galaxy_repository_v1_TestConnectionReply_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_galaxy_repository_v1_TestConnectionReply_descriptor, + new java.lang.String[] { "Ok", }); + internal_static_galaxy_repository_v1_GetLastDeployTimeRequest_descriptor = + getDescriptor().getMessageType(2); + internal_static_galaxy_repository_v1_GetLastDeployTimeRequest_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_galaxy_repository_v1_GetLastDeployTimeRequest_descriptor, + new java.lang.String[] { }); + internal_static_galaxy_repository_v1_GetLastDeployTimeReply_descriptor = + getDescriptor().getMessageType(3); + internal_static_galaxy_repository_v1_GetLastDeployTimeReply_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_galaxy_repository_v1_GetLastDeployTimeReply_descriptor, + new java.lang.String[] { "Present", "TimeOfLastDeploy", }); + internal_static_galaxy_repository_v1_DiscoverHierarchyRequest_descriptor = + getDescriptor().getMessageType(4); + internal_static_galaxy_repository_v1_DiscoverHierarchyRequest_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_galaxy_repository_v1_DiscoverHierarchyRequest_descriptor, + new java.lang.String[] { }); + internal_static_galaxy_repository_v1_DiscoverHierarchyReply_descriptor = + getDescriptor().getMessageType(5); + internal_static_galaxy_repository_v1_DiscoverHierarchyReply_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_galaxy_repository_v1_DiscoverHierarchyReply_descriptor, + new java.lang.String[] { "Objects", }); + internal_static_galaxy_repository_v1_WatchDeployEventsRequest_descriptor = + getDescriptor().getMessageType(6); + internal_static_galaxy_repository_v1_WatchDeployEventsRequest_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_galaxy_repository_v1_WatchDeployEventsRequest_descriptor, + new java.lang.String[] { "LastSeenDeployTime", }); + internal_static_galaxy_repository_v1_DeployEvent_descriptor = + getDescriptor().getMessageType(7); + internal_static_galaxy_repository_v1_DeployEvent_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_galaxy_repository_v1_DeployEvent_descriptor, + new java.lang.String[] { "Sequence", "ObservedAt", "TimeOfLastDeploy", "TimeOfLastDeployPresent", "ObjectCount", "AttributeCount", }); + internal_static_galaxy_repository_v1_GalaxyObject_descriptor = + getDescriptor().getMessageType(8); + internal_static_galaxy_repository_v1_GalaxyObject_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_galaxy_repository_v1_GalaxyObject_descriptor, + new java.lang.String[] { "GobjectId", "TagName", "ContainedName", "BrowseName", "ParentGobjectId", "IsArea", "CategoryId", "HostedByGobjectId", "TemplateChain", "Attributes", }); + internal_static_galaxy_repository_v1_GalaxyAttribute_descriptor = + getDescriptor().getMessageType(9); + internal_static_galaxy_repository_v1_GalaxyAttribute_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_galaxy_repository_v1_GalaxyAttribute_descriptor, + new java.lang.String[] { "AttributeName", "FullTagReference", "MxDataType", "DataTypeName", "IsArray", "ArrayDimension", "ArrayDimensionPresent", "MxAttributeCategory", "SecurityClassification", "IsHistorized", "IsAlarm", }); + descriptor.resolveAllFeaturesImmutable(); + com.google.protobuf.TimestampProto.getDescriptor(); + } + + // @@protoc_insertion_point(outer_class_scope) +} diff --git a/clients/proto/descriptors/mxaccessgw-client-v1.protoset b/clients/proto/descriptors/mxaccessgw-client-v1.protoset index 0ef0cac..f04fccc 100644 Binary files a/clients/proto/descriptors/mxaccessgw-client-v1.protoset and b/clients/proto/descriptors/mxaccessgw-client-v1.protoset differ diff --git a/clients/proto/proto-inputs.json b/clients/proto/proto-inputs.json index 0042f94..c349f63 100644 --- a/clients/proto/proto-inputs.json +++ b/clients/proto/proto-inputs.json @@ -12,6 +12,10 @@ { "path": "mxaccess_worker.proto", "role": "gateway_worker_ipc" + }, + { + "path": "galaxy_repository.proto", + "role": "public_galaxy_repository" } ], "descriptorSet": "clients/proto/descriptors/mxaccessgw-client-v1.protoset", diff --git a/clients/python/README.md b/clients/python/README.md index 677e2ec..0b84b2d 100644 --- a/clients/python/README.md +++ b/clients/python/README.md @@ -98,6 +98,76 @@ MXAccess commands and preserve raw replies on typed command exceptions. Canceling a Python task cancels the client-side gRPC call or stream wait. It does not abort an in-flight MXAccess COM call inside the worker process. +## Galaxy Repository Browse + +The `GalaxyRepositoryClient` wraps the read-only `GalaxyRepository` gRPC +service. It lets callers test connectivity to the AVEVA System Platform +Galaxy Repository (ZB SQL database), read the last deploy timestamp, and +enumerate the deployed object hierarchy plus each object's dynamic +attributes: + +```python +from mxgateway import GalaxyRepositoryClient + +async with await GalaxyRepositoryClient.connect( + endpoint="localhost:5000", + api_key="", + plaintext=True, +) as galaxy: + if not await galaxy.test_connection(): + raise RuntimeError("gateway cannot reach the Galaxy Repository DB") + + last_deploy = await galaxy.get_last_deploy_time() + print(f"last deploy: {last_deploy}") + + for obj in await galaxy.discover_hierarchy(): + print(obj.tag_name, obj.contained_name) + for attr in obj.attributes: + print(" ", attr.attribute_name, "->", attr.full_tag_reference) +``` + +The methods return native Python types (`bool`, `datetime | None`, and a +`list[GalaxyObject]` of generated proto messages) so callers can index +into the hierarchy without learning the underlying stub class. The +service requires the `metadata:read` scope on the API key. + +### Watching deploy events + +`GalaxyRepositoryClient.watch_deploy_events` opens a server-streaming +subscription that emits the current cached deploy state immediately and +then one `DeployEvent` per new Galaxy deploy. `sequence` is monotonic per +gateway start; gaps mean events were dropped from the per-subscriber +buffer. Pass `last_seen_deploy_time` to suppress the bootstrap event when +the caller already has the current state cached: + +```python +from datetime import datetime, timezone +from mxgateway import DeployEvent, GalaxyRepositoryClient + +async with await GalaxyRepositoryClient.connect( + endpoint="localhost:5000", + api_key="", + plaintext=True, +) as galaxy: + last_seen: datetime | None = None + async for event in galaxy.watch_deploy_events(last_seen_deploy_time=last_seen): + assert isinstance(event, DeployEvent) + print( + f"#{event.sequence} deploy={event.time_of_last_deploy.ToDatetime(tzinfo=timezone.utc)} " + f"objects={event.object_count} attributes={event.attribute_count}" + ) + if event.time_of_last_deploy_present: + last_seen = event.time_of_last_deploy.ToDatetime(tzinfo=timezone.utc) +``` + +The method returns an async iterator yielding the generated `DeployEvent` +proto. Breaking out of the loop, calling `aclose()` on the iterator, or +cancelling the surrounding task closes the underlying gRPC stream +cleanly. The streaming RPC requires the same `metadata:read` scope as +the other Galaxy methods. The CLI does not currently expose a +streaming `watch-deploy-events` subcommand — use the library API +directly when subscribing to deploy events from Python. + ## Authentication And TLS `ClientOptions.api_key` adds this metadata to unary calls and streams: diff --git a/clients/python/generate-proto.ps1 b/clients/python/generate-proto.ps1 index 3eccd51..fdc41ff 100644 --- a/clients/python/generate-proto.ps1 +++ b/clients/python/generate-proto.ps1 @@ -19,4 +19,5 @@ Get-ChildItem -Path (Join-Path $outputRoot '*_pb2_grpc.py') -File | Remove-Item "--python_out=$outputRoot" ` "--grpc_python_out=$outputRoot" ` mxaccess_gateway.proto ` - mxaccess_worker.proto + mxaccess_worker.proto ` + galaxy_repository.proto diff --git a/clients/python/src/mxgateway/__init__.py b/clients/python/src/mxgateway/__init__.py index 7e248b1..af5e43d 100644 --- a/clients/python/src/mxgateway/__init__.py +++ b/clients/python/src/mxgateway/__init__.py @@ -2,6 +2,13 @@ from .auth import ApiKey, auth_metadata from .client import GatewayClient +from .galaxy import GalaxyRepositoryClient +from .generated.galaxy_repository_pb2 import ( + DeployEvent, + GalaxyAttribute, + GalaxyObject, + WatchDeployEventsRequest, +) from .errors import ( MxAccessError, MxGatewayAuthenticationError, @@ -20,6 +27,10 @@ from .version import __version__ __all__ = [ "ApiKey", "ClientOptions", + "DeployEvent", + "GalaxyAttribute", + "GalaxyObject", + "GalaxyRepositoryClient", "GatewayClient", "MxAccessError", "MxGatewayAuthenticationError", @@ -31,6 +42,7 @@ __all__ = [ "MxGatewayWorkerError", "MxValueView", "Session", + "WatchDeployEventsRequest", "__version__", "auth_metadata", "from_mx_value", diff --git a/clients/python/src/mxgateway/galaxy.py b/clients/python/src/mxgateway/galaxy.py new file mode 100644 index 0000000..5495279 --- /dev/null +++ b/clients/python/src/mxgateway/galaxy.py @@ -0,0 +1,199 @@ +"""Async Galaxy Repository client wrapper. + +Wraps the read-only ``GalaxyRepository`` gRPC service exposed by the +MxAccess Gateway. The service lets callers test connectivity to the AVEVA +System Platform Galaxy Repository (ZB SQL database), read the last +deployment timestamp, and enumerate the deployed object hierarchy plus the +attributes on each object. +""" + +from __future__ import annotations + +import asyncio +from collections.abc import AsyncIterator, Sequence +from datetime import datetime +from typing import Any + +import grpc +from google.protobuf.timestamp_pb2 import Timestamp + +from .auth import merge_metadata +from .errors import map_rpc_error +from .generated import galaxy_repository_pb2 as galaxy_pb +from .generated import galaxy_repository_pb2_grpc as galaxy_pb_grpc +from .options import ClientOptions, create_channel + + +class GalaxyRepositoryClient: + """Async client for the Galaxy Repository gRPC service.""" + + def __init__( + self, + *, + options: ClientOptions, + stub: Any, + channel: grpc.aio.Channel | None = None, + ) -> None: + self.options = options + self.raw_stub = stub + self._channel = channel + self._closed = False + + @classmethod + async def connect( + cls, + options: ClientOptions | None = None, + *, + endpoint: str | None = None, + api_key: str | None = None, + plaintext: bool = False, + ca_file: str | None = None, + server_name_override: str | None = None, + stub: Any | None = None, + ) -> "GalaxyRepositoryClient": + """Create a client with either a real async channel or a supplied fake stub.""" + + resolved = options or ClientOptions( + endpoint=endpoint or "", + api_key=api_key, + plaintext=plaintext, + ca_file=ca_file, + server_name_override=server_name_override, + ) + + if stub is not None: + return cls(options=resolved, stub=stub) + + channel = create_channel(resolved) + return cls( + options=resolved, + stub=galaxy_pb_grpc.GalaxyRepositoryStub(channel), + channel=channel, + ) + + async def __aenter__(self) -> "GalaxyRepositoryClient": + return self + + async def __aexit__(self, *_exc_info: object) -> None: + await self.close() + + async def close(self) -> None: + """Close the owned gRPC channel.""" + + if self._closed: + return + + if self._channel is not None: + await self._channel.close() + self._closed = True + + async def test_connection(self) -> bool: + """Return ``True`` when the gateway can reach the Galaxy Repository DB.""" + + reply = await self._unary( + "test connection", + self.raw_stub.TestConnection, + galaxy_pb.TestConnectionRequest(), + ) + return bool(reply.ok) + + async def get_last_deploy_time(self) -> datetime | None: + """Return the last Galaxy deploy timestamp or ``None`` when unset.""" + + reply = await self._unary( + "get last deploy time", + self.raw_stub.GetLastDeployTime, + galaxy_pb.GetLastDeployTimeRequest(), + ) + if not reply.present: + return None + return reply.time_of_last_deploy.ToDatetime() + + async def discover_hierarchy(self) -> list[galaxy_pb.GalaxyObject]: + """Return the deployed Galaxy object hierarchy as raw proto messages.""" + + reply = await self._unary( + "discover hierarchy", + self.raw_stub.DiscoverHierarchy, + galaxy_pb.DiscoverHierarchyRequest(), + ) + return list(reply.objects) + + def watch_deploy_events( + self, + last_seen_deploy_time: datetime | None = None, + *, + metadata: Sequence[tuple[str, str]] | None = None, + ) -> AsyncIterator[galaxy_pb.DeployEvent]: + """Stream Galaxy deploy events. + + On subscribe the gateway emits the current cached state and then one + event per new deploy time. ``sequence`` is monotonic per server start; + gaps mean events were dropped from the per-subscriber buffer. When + ``last_seen_deploy_time`` is supplied and matches the current cached + deploy time the bootstrap event is suppressed. + """ + + request = galaxy_pb.WatchDeployEventsRequest() + if last_seen_deploy_time is not None: + timestamp = Timestamp() + timestamp.FromDatetime(last_seen_deploy_time) + request.last_seen_deploy_time.CopyFrom(timestamp) + + kwargs: dict[str, Any] = {"metadata": merge_metadata(self.options.api_key, metadata)} + if self.options.stream_timeout is not None: + kwargs["timeout"] = self.options.stream_timeout + try: + call = self.raw_stub.WatchDeployEvents(request, **kwargs) + except TypeError as error: + if "timeout" not in kwargs or "unexpected keyword argument 'timeout'" not in str(error): + raise + kwargs.pop("timeout") + call = self.raw_stub.WatchDeployEvents(request, **kwargs) + + return _canceling_iterator(call) + + async def _unary( + self, + operation: str, + method: Any, + request: Any, + *, + metadata: Sequence[tuple[str, str]] | None = None, + ) -> Any: + kwargs: dict[str, Any] = {"metadata": merge_metadata(self.options.api_key, metadata)} + if self.options.call_timeout is not None: + kwargs["timeout"] = self.options.call_timeout + try: + call = method(request, **kwargs) + except TypeError as error: + if "timeout" not in kwargs or "unexpected keyword argument 'timeout'" not in str(error): + raise + kwargs.pop("timeout") + call = method(request, **kwargs) + try: + return await call + except asyncio.CancelledError: + cancel = getattr(call, "cancel", None) + if cancel is not None: + cancel() + raise + except grpc.RpcError as error: + raise map_rpc_error(operation, error) from error + + +async def _canceling_iterator(call: Any) -> AsyncIterator[galaxy_pb.DeployEvent]: + try: + async for event in call: + yield event + except asyncio.CancelledError: + cancel = getattr(call, "cancel", None) + if cancel is not None: + cancel() + raise + except grpc.RpcError as error: + raise map_rpc_error("watch deploy events", error) from error + finally: + cancel = getattr(call, "cancel", None) + if cancel is not None: + cancel() diff --git a/clients/python/src/mxgateway/generated/__init__.py b/clients/python/src/mxgateway/generated/__init__.py index f601f29..b2730bb 100644 --- a/clients/python/src/mxgateway/generated/__init__.py +++ b/clients/python/src/mxgateway/generated/__init__.py @@ -21,7 +21,15 @@ sys.modules.setdefault("mxaccess_worker_pb2", mxaccess_worker_pb2) mxaccess_worker_pb2_grpc = import_module(f"{__name__}.mxaccess_worker_pb2_grpc") sys.modules.setdefault("mxaccess_worker_pb2_grpc", mxaccess_worker_pb2_grpc) +galaxy_repository_pb2 = import_module(f"{__name__}.galaxy_repository_pb2") +sys.modules.setdefault("galaxy_repository_pb2", galaxy_repository_pb2) + +galaxy_repository_pb2_grpc = import_module(f"{__name__}.galaxy_repository_pb2_grpc") +sys.modules.setdefault("galaxy_repository_pb2_grpc", galaxy_repository_pb2_grpc) + __all__ = [ + "galaxy_repository_pb2", + "galaxy_repository_pb2_grpc", "mxaccess_gateway_pb2", "mxaccess_gateway_pb2_grpc", "mxaccess_worker_pb2", diff --git a/clients/python/src/mxgateway/generated/galaxy_repository_pb2.py b/clients/python/src/mxgateway/generated/galaxy_repository_pb2.py new file mode 100644 index 0000000..8abaf20 --- /dev/null +++ b/clients/python/src/mxgateway/generated/galaxy_repository_pb2.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: galaxy_repository.proto +# Protobuf Python Version: 6.31.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 1, + '', + 'galaxy_repository.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17galaxy_repository.proto\x12\x14galaxy_repository.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\x17\n\x15TestConnectionRequest\"!\n\x13TestConnectionReply\x12\n\n\x02ok\x18\x01 \x01(\x08\"\x1a\n\x18GetLastDeployTimeRequest\"b\n\x16GetLastDeployTimeReply\x12\x0f\n\x07present\x18\x01 \x01(\x08\x12\x37\n\x13time_of_last_deploy\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x1a\n\x18\x44iscoverHierarchyRequest\"M\n\x16\x44iscoverHierarchyReply\x12\x33\n\x07objects\x18\x01 \x03(\x0b\x32\".galaxy_repository.v1.GalaxyObject\"U\n\x18WatchDeployEventsRequest\x12\x39\n\x15last_seen_deploy_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xdd\x01\n\x0b\x44\x65ployEvent\x12\x10\n\x08sequence\x18\x01 \x01(\x04\x12/\n\x0bobserved_at\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x37\n\x13time_of_last_deploy\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12#\n\x1btime_of_last_deploy_present\x18\x04 \x01(\x08\x12\x14\n\x0cobject_count\x18\x05 \x01(\x05\x12\x17\n\x0f\x61ttribute_count\x18\x06 \x01(\x05\"\x93\x02\n\x0cGalaxyObject\x12\x12\n\ngobject_id\x18\x01 \x01(\x05\x12\x10\n\x08tag_name\x18\x02 \x01(\t\x12\x16\n\x0e\x63ontained_name\x18\x03 \x01(\t\x12\x13\n\x0b\x62rowse_name\x18\x04 \x01(\t\x12\x19\n\x11parent_gobject_id\x18\x05 \x01(\x05\x12\x0f\n\x07is_area\x18\x06 \x01(\x08\x12\x13\n\x0b\x63\x61tegory_id\x18\x07 \x01(\x05\x12\x1c\n\x14hosted_by_gobject_id\x18\x08 \x01(\x05\x12\x16\n\x0etemplate_chain\x18\t \x03(\t\x12\x39\n\nattributes\x18\n \x03(\x0b\x32%.galaxy_repository.v1.GalaxyAttribute\"\xa8\x02\n\x0fGalaxyAttribute\x12\x16\n\x0e\x61ttribute_name\x18\x01 \x01(\t\x12\x1a\n\x12\x66ull_tag_reference\x18\x02 \x01(\t\x12\x14\n\x0cmx_data_type\x18\x03 \x01(\x05\x12\x16\n\x0e\x64\x61ta_type_name\x18\x04 \x01(\t\x12\x10\n\x08is_array\x18\x05 \x01(\x08\x12\x17\n\x0f\x61rray_dimension\x18\x06 \x01(\x05\x12\x1f\n\x17\x61rray_dimension_present\x18\x07 \x01(\x08\x12\x1d\n\x15mx_attribute_category\x18\x08 \x01(\x05\x12\x1f\n\x17security_classification\x18\t \x01(\x05\x12\x15\n\ris_historized\x18\n \x01(\x08\x12\x10\n\x08is_alarm\x18\x0b \x01(\x08\x32\xcc\x03\n\x10GalaxyRepository\x12h\n\x0eTestConnection\x12+.galaxy_repository.v1.TestConnectionRequest\x1a).galaxy_repository.v1.TestConnectionReply\x12q\n\x11GetLastDeployTime\x12..galaxy_repository.v1.GetLastDeployTimeRequest\x1a,.galaxy_repository.v1.GetLastDeployTimeReply\x12q\n\x11\x44iscoverHierarchy\x12..galaxy_repository.v1.DiscoverHierarchyRequest\x1a,.galaxy_repository.v1.DiscoverHierarchyReply\x12h\n\x11WatchDeployEvents\x12..galaxy_repository.v1.WatchDeployEventsRequest\x1a!.galaxy_repository.v1.DeployEvent0\x01\x42#\xaa\x02 MxGateway.Contracts.Proto.Galaxyb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'galaxy_repository_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\252\002 MxGateway.Contracts.Proto.Galaxy' + _globals['_TESTCONNECTIONREQUEST']._serialized_start=82 + _globals['_TESTCONNECTIONREQUEST']._serialized_end=105 + _globals['_TESTCONNECTIONREPLY']._serialized_start=107 + _globals['_TESTCONNECTIONREPLY']._serialized_end=140 + _globals['_GETLASTDEPLOYTIMEREQUEST']._serialized_start=142 + _globals['_GETLASTDEPLOYTIMEREQUEST']._serialized_end=168 + _globals['_GETLASTDEPLOYTIMEREPLY']._serialized_start=170 + _globals['_GETLASTDEPLOYTIMEREPLY']._serialized_end=268 + _globals['_DISCOVERHIERARCHYREQUEST']._serialized_start=270 + _globals['_DISCOVERHIERARCHYREQUEST']._serialized_end=296 + _globals['_DISCOVERHIERARCHYREPLY']._serialized_start=298 + _globals['_DISCOVERHIERARCHYREPLY']._serialized_end=375 + _globals['_WATCHDEPLOYEVENTSREQUEST']._serialized_start=377 + _globals['_WATCHDEPLOYEVENTSREQUEST']._serialized_end=462 + _globals['_DEPLOYEVENT']._serialized_start=465 + _globals['_DEPLOYEVENT']._serialized_end=686 + _globals['_GALAXYOBJECT']._serialized_start=689 + _globals['_GALAXYOBJECT']._serialized_end=964 + _globals['_GALAXYATTRIBUTE']._serialized_start=967 + _globals['_GALAXYATTRIBUTE']._serialized_end=1263 + _globals['_GALAXYREPOSITORY']._serialized_start=1266 + _globals['_GALAXYREPOSITORY']._serialized_end=1726 +# @@protoc_insertion_point(module_scope) diff --git a/clients/python/src/mxgateway/generated/galaxy_repository_pb2_grpc.py b/clients/python/src/mxgateway/generated/galaxy_repository_pb2_grpc.py new file mode 100644 index 0000000..815002b --- /dev/null +++ b/clients/python/src/mxgateway/generated/galaxy_repository_pb2_grpc.py @@ -0,0 +1,244 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + +import galaxy_repository_pb2 as galaxy__repository__pb2 + +GRPC_GENERATED_VERSION = '1.80.0' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + ' but the generated code in galaxy_repository_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + + +class GalaxyRepositoryStub(object): + """Read-only browse over the AVEVA System Platform Galaxy Repository (ZB SQL + database). Lets clients enumerate the deployed object hierarchy and each + object's dynamic attributes so they know what tag references to subscribe + to via the MxAccessGateway service. + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.TestConnection = channel.unary_unary( + '/galaxy_repository.v1.GalaxyRepository/TestConnection', + request_serializer=galaxy__repository__pb2.TestConnectionRequest.SerializeToString, + response_deserializer=galaxy__repository__pb2.TestConnectionReply.FromString, + _registered_method=True) + self.GetLastDeployTime = channel.unary_unary( + '/galaxy_repository.v1.GalaxyRepository/GetLastDeployTime', + request_serializer=galaxy__repository__pb2.GetLastDeployTimeRequest.SerializeToString, + response_deserializer=galaxy__repository__pb2.GetLastDeployTimeReply.FromString, + _registered_method=True) + self.DiscoverHierarchy = channel.unary_unary( + '/galaxy_repository.v1.GalaxyRepository/DiscoverHierarchy', + request_serializer=galaxy__repository__pb2.DiscoverHierarchyRequest.SerializeToString, + response_deserializer=galaxy__repository__pb2.DiscoverHierarchyReply.FromString, + _registered_method=True) + self.WatchDeployEvents = channel.unary_stream( + '/galaxy_repository.v1.GalaxyRepository/WatchDeployEvents', + request_serializer=galaxy__repository__pb2.WatchDeployEventsRequest.SerializeToString, + response_deserializer=galaxy__repository__pb2.DeployEvent.FromString, + _registered_method=True) + + +class GalaxyRepositoryServicer(object): + """Read-only browse over the AVEVA System Platform Galaxy Repository (ZB SQL + database). Lets clients enumerate the deployed object hierarchy and each + object's dynamic attributes so they know what tag references to subscribe + to via the MxAccessGateway service. + """ + + def TestConnection(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetLastDeployTime(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def DiscoverHierarchy(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def WatchDeployEvents(self, request, context): + """Server-stream of deploy events. The server emits the current state immediately + on subscribe (so clients can bootstrap their cache without waiting for the next + deploy), then emits one event each time the gateway's hierarchy cache observes + a new galaxy.time_of_last_deploy. The sequence field is monotonically + increasing per server start; gaps indicate the per-subscriber buffer dropped + older events because the client was too slow. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_GalaxyRepositoryServicer_to_server(servicer, server): + rpc_method_handlers = { + 'TestConnection': grpc.unary_unary_rpc_method_handler( + servicer.TestConnection, + request_deserializer=galaxy__repository__pb2.TestConnectionRequest.FromString, + response_serializer=galaxy__repository__pb2.TestConnectionReply.SerializeToString, + ), + 'GetLastDeployTime': grpc.unary_unary_rpc_method_handler( + servicer.GetLastDeployTime, + request_deserializer=galaxy__repository__pb2.GetLastDeployTimeRequest.FromString, + response_serializer=galaxy__repository__pb2.GetLastDeployTimeReply.SerializeToString, + ), + 'DiscoverHierarchy': grpc.unary_unary_rpc_method_handler( + servicer.DiscoverHierarchy, + request_deserializer=galaxy__repository__pb2.DiscoverHierarchyRequest.FromString, + response_serializer=galaxy__repository__pb2.DiscoverHierarchyReply.SerializeToString, + ), + 'WatchDeployEvents': grpc.unary_stream_rpc_method_handler( + servicer.WatchDeployEvents, + request_deserializer=galaxy__repository__pb2.WatchDeployEventsRequest.FromString, + response_serializer=galaxy__repository__pb2.DeployEvent.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'galaxy_repository.v1.GalaxyRepository', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('galaxy_repository.v1.GalaxyRepository', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class GalaxyRepository(object): + """Read-only browse over the AVEVA System Platform Galaxy Repository (ZB SQL + database). Lets clients enumerate the deployed object hierarchy and each + object's dynamic attributes so they know what tag references to subscribe + to via the MxAccessGateway service. + """ + + @staticmethod + def TestConnection(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/galaxy_repository.v1.GalaxyRepository/TestConnection', + galaxy__repository__pb2.TestConnectionRequest.SerializeToString, + galaxy__repository__pb2.TestConnectionReply.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetLastDeployTime(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/galaxy_repository.v1.GalaxyRepository/GetLastDeployTime', + galaxy__repository__pb2.GetLastDeployTimeRequest.SerializeToString, + galaxy__repository__pb2.GetLastDeployTimeReply.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def DiscoverHierarchy(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/galaxy_repository.v1.GalaxyRepository/DiscoverHierarchy', + galaxy__repository__pb2.DiscoverHierarchyRequest.SerializeToString, + galaxy__repository__pb2.DiscoverHierarchyReply.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def WatchDeployEvents(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream( + request, + target, + '/galaxy_repository.v1.GalaxyRepository/WatchDeployEvents', + galaxy__repository__pb2.WatchDeployEventsRequest.SerializeToString, + galaxy__repository__pb2.DeployEvent.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/clients/python/tests/test_galaxy.py b/clients/python/tests/test_galaxy.py new file mode 100644 index 0000000..f176195 --- /dev/null +++ b/clients/python/tests/test_galaxy.py @@ -0,0 +1,320 @@ +"""Tests for the Galaxy Repository async client wrapper.""" + +from __future__ import annotations + +import asyncio +from datetime import datetime, timezone +from typing import Any + +import pytest +from google.protobuf.timestamp_pb2 import Timestamp + +from mxgateway import ClientOptions, DeployEvent, GalaxyRepositoryClient, WatchDeployEventsRequest +from mxgateway.generated import galaxy_repository_pb2 as galaxy_pb +from mxgateway.generated import galaxy_repository_pb2_grpc as galaxy_pb_grpc + + +def test_galaxy_messages_import() -> None: + request = galaxy_pb.DiscoverHierarchyRequest() + obj = galaxy_pb.GalaxyObject( + gobject_id=42, + tag_name="DelmiaReceiver_001", + contained_name="DelmiaReceiver", + browse_name="DelmiaReceiver", + parent_gobject_id=10, + is_area=False, + category_id=4, + hosted_by_gobject_id=10, + template_chain=["$ApplicationObject", "$DelmiaReceiver"], + attributes=[ + galaxy_pb.GalaxyAttribute( + attribute_name="DownloadPath", + full_tag_reference="DelmiaReceiver_001.DownloadPath", + mx_data_type=8, + data_type_name="String", + ), + ], + ) + + assert request.DESCRIPTOR is not None + assert obj.attributes[0].attribute_name == "DownloadPath" + assert hasattr(galaxy_pb_grpc, "GalaxyRepositoryStub") + + +@pytest.mark.asyncio +async def test_test_connection_returns_bool_and_sends_auth() -> None: + stub = FakeGalaxyStub() + stub.test_connection.replies = [galaxy_pb.TestConnectionReply(ok=True)] + client = await GalaxyRepositoryClient.connect( + ClientOptions(endpoint="fake", api_key="mxgw_test_secret", plaintext=True), + stub=stub, + ) + + result = await client.test_connection() + + assert result is True + assert stub.test_connection.metadata == (("authorization", "Bearer mxgw_test_secret"),) + assert isinstance(stub.test_connection.requests[0], galaxy_pb.TestConnectionRequest) + + +@pytest.mark.asyncio +async def test_get_last_deploy_time_returns_datetime_when_present() -> None: + timestamp = Timestamp() + timestamp.FromDatetime(datetime(2025, 4, 1, 12, 30, 45, tzinfo=timezone.utc)) + stub = FakeGalaxyStub() + stub.get_last_deploy_time.replies = [ + galaxy_pb.GetLastDeployTimeReply(present=True, time_of_last_deploy=timestamp), + ] + client = await GalaxyRepositoryClient.connect( + ClientOptions(endpoint="fake", plaintext=True), + stub=stub, + ) + + when = await client.get_last_deploy_time() + + assert when is not None + assert when.year == 2025 + assert when.month == 4 + assert when.day == 1 + assert when.hour == 12 + assert when.minute == 30 + assert when.second == 45 + + +@pytest.mark.asyncio +async def test_get_last_deploy_time_returns_none_when_not_present() -> None: + stub = FakeGalaxyStub() + stub.get_last_deploy_time.replies = [galaxy_pb.GetLastDeployTimeReply(present=False)] + client = await GalaxyRepositoryClient.connect( + ClientOptions(endpoint="fake", plaintext=True), + stub=stub, + ) + + assert await client.get_last_deploy_time() is None + + +@pytest.mark.asyncio +async def test_discover_hierarchy_returns_proto_objects() -> None: + stub = FakeGalaxyStub() + stub.discover_hierarchy.replies = [ + galaxy_pb.DiscoverHierarchyReply( + objects=[ + galaxy_pb.GalaxyObject( + gobject_id=1, + tag_name="TestMachine_001", + contained_name="TestMachine", + browse_name="TestMachine_001", + is_area=True, + ), + galaxy_pb.GalaxyObject( + gobject_id=2, + tag_name="DelmiaReceiver_001", + contained_name="DelmiaReceiver", + browse_name="DelmiaReceiver", + parent_gobject_id=1, + attributes=[ + galaxy_pb.GalaxyAttribute( + attribute_name="DownloadPath", + full_tag_reference="DelmiaReceiver_001.DownloadPath", + mx_data_type=8, + data_type_name="String", + ), + ], + ), + ], + ), + ] + client = await GalaxyRepositoryClient.connect( + ClientOptions(endpoint="fake", plaintext=True), + stub=stub, + ) + + objects = await client.discover_hierarchy() + + assert isinstance(objects, list) + assert len(objects) == 2 + assert objects[0].tag_name == "TestMachine_001" + assert objects[1].attributes[0].full_tag_reference == "DelmiaReceiver_001.DownloadPath" + + +@pytest.mark.asyncio +async def test_watch_deploy_events_yields_events_in_order() -> None: + ts1 = Timestamp() + ts1.FromDatetime(datetime(2025, 4, 1, 10, 0, 0, tzinfo=timezone.utc)) + ts2 = Timestamp() + ts2.FromDatetime(datetime(2025, 4, 1, 11, 0, 0, tzinfo=timezone.utc)) + events = [ + galaxy_pb.DeployEvent( + sequence=1, + observed_at=ts1, + time_of_last_deploy=ts1, + time_of_last_deploy_present=True, + object_count=10, + attribute_count=42, + ), + galaxy_pb.DeployEvent( + sequence=2, + observed_at=ts2, + time_of_last_deploy=ts2, + time_of_last_deploy_present=True, + object_count=11, + attribute_count=45, + ), + ] + stub = FakeGalaxyStub() + stub.watch_deploy_events.replies = list(events) + client = await GalaxyRepositoryClient.connect( + ClientOptions(endpoint="fake", api_key="mxgw_test_secret", plaintext=True), + stub=stub, + ) + + received: list[DeployEvent] = [] + async for event in client.watch_deploy_events(): + received.append(event) + + assert len(received) == 2 + assert received[0].sequence == 1 + assert received[1].sequence == 2 + assert received[0].object_count == 10 + assert received[1].attribute_count == 45 + assert stub.watch_deploy_events.metadata == (("authorization", "Bearer mxgw_test_secret"),) + assert isinstance(stub.watch_deploy_events.requests[0], galaxy_pb.WatchDeployEventsRequest) + # No last_seen_deploy_time was passed, so the request should leave it unset. + assert not stub.watch_deploy_events.requests[0].HasField("last_seen_deploy_time") + + +@pytest.mark.asyncio +async def test_watch_deploy_events_propagates_last_seen_deploy_time() -> None: + last_seen = datetime(2025, 4, 1, 12, 0, 0, tzinfo=timezone.utc) + stub = FakeGalaxyStub() + stub.watch_deploy_events.replies = [] + client = await GalaxyRepositoryClient.connect( + ClientOptions(endpoint="fake", plaintext=True), + stub=stub, + ) + + async for _ in client.watch_deploy_events(last_seen_deploy_time=last_seen): + pass + + request = stub.watch_deploy_events.requests[0] + assert isinstance(request, WatchDeployEventsRequest) + assert request.HasField("last_seen_deploy_time") + assert request.last_seen_deploy_time.ToDatetime(tzinfo=timezone.utc) == last_seen + + +@pytest.mark.asyncio +async def test_watch_deploy_events_cancellation_closes_stream() -> None: + ts = Timestamp() + ts.FromDatetime(datetime(2025, 4, 1, 10, 0, 0, tzinfo=timezone.utc)) + stub = FakeGalaxyStub() + # Use a "blocking" stream that never yields more after the first event. + stub.watch_deploy_events = FakeStream( + [galaxy_pb.DeployEvent(sequence=1, observed_at=ts)], + block_after_replies=True, + ) + client = await GalaxyRepositoryClient.connect( + ClientOptions(endpoint="fake", plaintext=True), + stub=stub, + ) + + iterator = client.watch_deploy_events() + + first = await iterator.__anext__() + assert first.sequence == 1 + + # Break the iterator by aclose() — this should drive the cancel path. + await iterator.aclose() + + assert stub.watch_deploy_events.cancel_called is True + + +@pytest.mark.asyncio +async def test_close_marks_channel_closed_when_no_real_channel() -> None: + stub = FakeGalaxyStub() + client = await GalaxyRepositoryClient.connect( + ClientOptions(endpoint="fake", plaintext=True), + stub=stub, + ) + + await client.close() + # Idempotent: a second close should not raise. + await client.close() + + +class FakeGalaxyStub: + def __init__(self) -> None: + self.test_connection = FakeUnary([galaxy_pb.TestConnectionReply(ok=False)]) + self.get_last_deploy_time = FakeUnary([galaxy_pb.GetLastDeployTimeReply(present=False)]) + self.discover_hierarchy = FakeUnary([galaxy_pb.DiscoverHierarchyReply()]) + self.watch_deploy_events = FakeStream([]) + self.TestConnection = self.test_connection + self.GetLastDeployTime = self.get_last_deploy_time + self.DiscoverHierarchy = self.discover_hierarchy + + @property + def WatchDeployEvents(self) -> "FakeStream": # noqa: N802 — gRPC naming + return self.watch_deploy_events + + +class FakeUnary: + def __init__(self, replies: list[Any]) -> None: + self.replies = replies + self.requests: list[Any] = [] + self.metadata: tuple[tuple[str, str], ...] | None = None + + async def __call__( + self, + request: Any, + *, + metadata: tuple[tuple[str, str], ...], + timeout: float | None = None, + ) -> Any: + self.requests.append(request) + self.metadata = metadata + return self.replies.pop(0) + + +class FakeStream: + """Sync-callable fake matching the gRPC unary-stream surface. + + Calling the stub returns ``self`` (an async iterator). After exhausting the + seeded ``replies``, iteration either ends (default) or blocks indefinitely + (``block_after_replies=True``) so cancellation paths can be exercised. + """ + + def __init__( + self, + replies: list[Any], + *, + block_after_replies: bool = False, + ) -> None: + self.replies = list(replies) + self.requests: list[Any] = [] + self.metadata: tuple[tuple[str, str], ...] | None = None + self.cancel_called = False + self._block_after_replies = block_after_replies + + def __call__( + self, + request: Any, + *, + metadata: tuple[tuple[str, str], ...], + timeout: float | None = None, + ) -> "FakeStream": + self.requests.append(request) + self.metadata = metadata + return self + + def __aiter__(self) -> "FakeStream": + return self + + async def __anext__(self) -> Any: + if self.replies: + return self.replies.pop(0) + if self._block_after_replies: + # Sleep forever until the consumer cancels us. + await asyncio.Event().wait() + raise StopAsyncIteration + + def cancel(self) -> None: + self.cancel_called = True diff --git a/clients/rust/Cargo.lock b/clients/rust/Cargo.lock index a0ead13..04b1528 100644 --- a/clients/rust/Cargo.lock +++ b/clients/rust/Cargo.lock @@ -595,6 +595,7 @@ dependencies = [ "clap", "futures-util", "mxgateway-client", + "prost-types", "serde", "serde_json", "tokio", @@ -886,6 +887,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "slab" version = "0.4.12" @@ -990,6 +1001,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", diff --git a/clients/rust/README.md b/clients/rust/README.md index 47eb700..0b34955 100644 --- a/clients/rust/README.md +++ b/clients/rust/README.md @@ -99,6 +99,76 @@ preserving the raw message for parity diagnostics. Command replies whose protocol status is not `PROTOCOL_STATUS_CODE_OK` become `Error::Command` and retain the raw `MxCommandReply`. +## Galaxy Repository browse + +The Galaxy Repository service exposes a read-only browse over the AVEVA System +Platform Galaxy Repository (ZB SQL database). It uses the same API-key auth as +the gateway service but requires the `metadata:read` scope on the server. + +[`GalaxyClient`](src/galaxy.rs) wraps the generated Galaxy bindings the same +way [`GatewayClient`](src/client.rs) wraps the gateway bindings: + +```rust +let mut galaxy = GalaxyClient::connect( + ClientOptions::new("http://localhost:5000") + .with_api_key(ApiKey::new(api_key)), +).await?; + +let ok = galaxy.test_connection().await?; +let last_deploy = galaxy.get_last_deploy_time().await?; // Option +let objects = galaxy.discover_hierarchy().await?; // Vec +``` + +`get_last_deploy_time` returns `None` when the server reports +`present = false`. `discover_hierarchy` returns the generated +`GalaxyObject` proto type (re-exported via +`mxgateway_client::generated::galaxy_repository::v1`) with all attributes +attached. + +The CLI ships matching subcommands under `galaxy`: + +```powershell +cargo run -p mxgw-cli -- galaxy test-connection --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json +cargo run -p mxgw-cli -- galaxy last-deploy-time --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json +cargo run -p mxgw-cli -- galaxy discover-hierarchy --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json +``` + +### Watching deploy events + +`watch_deploy_events` opens the `WatchDeployEvents` server stream. The +server emits a bootstrap [`DeployEvent`](src/galaxy.rs) describing the +current cache state on subscribe, then one event each time the cached +`galaxy.time_of_last_deploy` changes. `sequence` is monotonic per server +start; gaps signal that the per-subscriber buffer dropped older events. +Pass `last_seen_deploy_time` to suppress the bootstrap event when the +client's cached deploy time matches the server's. + +```rust +use futures_util::StreamExt; + +let mut stream = galaxy.watch_deploy_events(None).await?; +while let Some(event) = stream.next().await { + let event = event?; + println!( + "seq={} objects={} attributes={}", + event.sequence, event.object_count, event.attribute_count, + ); +} +// Drop the stream to cancel the gRPC call. +``` + +The matching CLI subcommand prints one line per event (`--json` switches to +one JSON object per event). `--last-seen-deploy-time` accepts an RFC3339 +timestamp and is forwarded to the server. `--max-events` (default 0 = no +cap) lets you stop after a fixed number of events; otherwise the command +runs until the stream ends or `Ctrl+C` is pressed. + +```powershell +cargo run -p mxgw-cli -- galaxy watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY +cargo run -p mxgw-cli -- galaxy watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json +cargo run -p mxgw-cli -- galaxy watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --last-seen-deploy-time 2026-04-28T15:30:00Z +``` + ## Integration Checks Run live checks only when a gateway and MXAccess-backed worker are available: diff --git a/clients/rust/build.rs b/clients/rust/build.rs index 2e17946..202b971 100644 --- a/clients/rust/build.rs +++ b/clients/rust/build.rs @@ -13,17 +13,23 @@ fn main() -> Result<(), Box> { let proto_root = repo_root.join("src/MxGateway.Contracts/Protos"); let gateway_proto = proto_root.join("mxaccess_gateway.proto"); let worker_proto = proto_root.join("mxaccess_worker.proto"); + let galaxy_proto = proto_root.join("galaxy_repository.proto"); let descriptor_path = PathBuf::from(env::var("OUT_DIR")?).join("mxaccessgw-client-v1.protoset"); println!("cargo:rerun-if-changed={}", gateway_proto.display()); println!("cargo:rerun-if-changed={}", worker_proto.display()); + println!("cargo:rerun-if-changed={}", galaxy_proto.display()); tonic_build::configure() .build_server(true) .build_client(true) .file_descriptor_set_path(descriptor_path) .compile_protos( - &[gateway_proto.as_path(), worker_proto.as_path()], + &[ + gateway_proto.as_path(), + worker_proto.as_path(), + galaxy_proto.as_path(), + ], &[proto_root.as_path()], )?; diff --git a/clients/rust/crates/mxgw-cli/Cargo.toml b/clients/rust/crates/mxgw-cli/Cargo.toml index 9145eca..cfd6e48 100644 --- a/clients/rust/crates/mxgw-cli/Cargo.toml +++ b/clients/rust/crates/mxgw-cli/Cargo.toml @@ -12,6 +12,7 @@ path = "src/main.rs" clap = { workspace = true } futures-util = { workspace = true } mxgateway-client = { path = "../.." } +prost-types = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -tokio = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal", "sync", "time"] } diff --git a/clients/rust/crates/mxgw-cli/src/main.rs b/clients/rust/crates/mxgw-cli/src/main.rs index 5f95809..c3de660 100644 --- a/clients/rust/crates/mxgw-cli/src/main.rs +++ b/clients/rust/crates/mxgw-cli/src/main.rs @@ -5,13 +5,14 @@ use std::time::Duration; use clap::{Args, Parser, Subcommand, ValueEnum}; use futures_util::StreamExt; +use mxgateway_client::generated::galaxy_repository::v1::DeployEvent; use mxgateway_client::generated::mxaccess_gateway::v1::{ CloseSessionRequest, MxCommand, MxCommandKind, MxCommandRequest, OpenSessionRequest, PingCommand, StreamEventsRequest, }; use mxgateway_client::{ - ApiKey, ClientOptions, Error, GatewayClient, MxValue, CLIENT_VERSION, GATEWAY_PROTOCOL_VERSION, - WORKER_PROTOCOL_VERSION, + ApiKey, ClientOptions, Error, GalaxyClient, GatewayClient, MxValue, CLIENT_VERSION, + GATEWAY_PROTOCOL_VERSION, WORKER_PROTOCOL_VERSION, }; use serde_json::json; use serde_json::Value; @@ -178,6 +179,51 @@ enum Command { #[arg(long)] json: bool, }, + #[command(subcommand)] + Galaxy(GalaxyCommand), +} + +#[derive(Debug, Subcommand)] +enum GalaxyCommand { + TestConnection { + #[command(flatten)] + connection: ConnectionArgs, + #[arg(long)] + json: bool, + }, + LastDeployTime { + #[command(flatten)] + connection: ConnectionArgs, + #[arg(long)] + json: bool, + }, + DiscoverHierarchy { + #[command(flatten)] + connection: ConnectionArgs, + #[arg(long)] + json: bool, + }, + /// Subscribe to the WatchDeployEvents server stream. + /// + /// Prints one line per received event (or one JSON object with `--json`). + /// Runs until the stream ends, the server fails the call, or the + /// process is interrupted (Ctrl+C). + #[command(alias = "watch-deploy-events")] + Watch { + #[command(flatten)] + connection: ConnectionArgs, + /// Optional RFC3339 timestamp (e.g. `2026-04-28T15:30:00Z`). When + /// supplied, the server suppresses the bootstrap event if its + /// cached deploy time matches this value. + #[arg(long)] + last_seen_deploy_time: Option, + /// Optional cap on the number of events to print before exiting. + /// 0 (the default) means run until cancelled or the stream ends. + #[arg(long, default_value_t = 0)] + max_events: usize, + #[arg(long)] + json: bool, + }, } #[derive(Debug, Args, Clone)] @@ -465,6 +511,7 @@ async fn run(cli: Cli) -> Result<(), Error> { .await?; print_ok("write2", json); } + Command::Galaxy(galaxy_command) => run_galaxy(galaxy_command).await?, Command::Smoke { connection, item, @@ -514,6 +561,133 @@ async fn connect(connection: ConnectionArgs) -> Result { GatewayClient::connect(connection.options()).await } +async fn connect_galaxy(connection: ConnectionArgs) -> Result { + GalaxyClient::connect(connection.options()).await +} + +async fn run_galaxy(command: GalaxyCommand) -> Result<(), Error> { + match command { + GalaxyCommand::TestConnection { connection, json } => { + let mut client = connect_galaxy(connection).await?; + let ok = client.test_connection().await?; + if json { + println!("{}", json!({ "ok": ok })); + } else if ok { + println!("ok"); + } else { + println!("not ok"); + } + } + GalaxyCommand::LastDeployTime { connection, json } => { + let mut client = connect_galaxy(connection).await?; + let timestamp = client.get_last_deploy_time().await?; + match (json, timestamp) { + (true, Some(ts)) => { + println!( + "{}", + json!({ + "present": true, + "seconds": ts.seconds, + "nanos": ts.nanos, + }) + ); + } + (true, None) => { + println!("{}", json!({ "present": false })); + } + (false, Some(ts)) => println!("{}.{:09}", ts.seconds, ts.nanos), + (false, None) => println!("(absent)"), + } + } + GalaxyCommand::Watch { + connection, + last_seen_deploy_time, + max_events, + json, + } => { + let mut client = connect_galaxy(connection).await?; + let last_seen = last_seen_deploy_time + .as_deref() + .map(parse_rfc3339_timestamp) + .transpose()?; + let mut stream = client.watch_deploy_events(last_seen).await?; + + let mut count = 0usize; + loop { + tokio::select! { + biased; + signal = tokio::signal::ctrl_c() => { + signal.map_err(|err| Error::InvalidArgument { + name: "ctrl_c".to_owned(), + detail: err.to_string(), + })?; + // Drop the stream below by breaking; tonic tears the + // gRPC call down cooperatively. + break; + } + next = stream.next() => { + let Some(event) = next else { break; }; + let event = event?; + count += 1; + print_deploy_event(&event, json); + if max_events != 0 && count >= max_events { + break; + } + } + } + } + } + GalaxyCommand::DiscoverHierarchy { connection, json } => { + let mut client = connect_galaxy(connection).await?; + let objects = client.discover_hierarchy().await?; + if json { + let payload: Vec<_> = objects + .iter() + .map(|object| { + json!({ + "gobjectId": object.gobject_id, + "tagName": object.tag_name, + "containedName": object.contained_name, + "browseName": object.browse_name, + "parentGobjectId": object.parent_gobject_id, + "isArea": object.is_area, + "categoryId": object.category_id, + "hostedByGobjectId": object.hosted_by_gobject_id, + "templateChain": object.template_chain, + "attributes": object.attributes.iter().map(|attribute| json!({ + "attributeName": attribute.attribute_name, + "fullTagReference": attribute.full_tag_reference, + "mxDataType": attribute.mx_data_type, + "dataTypeName": attribute.data_type_name, + "isArray": attribute.is_array, + "arrayDimension": attribute.array_dimension, + "arrayDimensionPresent": attribute.array_dimension_present, + "mxAttributeCategory": attribute.mx_attribute_category, + "securityClassification": attribute.security_classification, + "isHistorized": attribute.is_historized, + "isAlarm": attribute.is_alarm, + })).collect::>(), + }) + }) + .collect(); + println!("{}", json!({ "objects": payload })); + } else { + println!("{}", objects.len()); + for object in &objects { + println!( + "{} {} {} ({} attribute(s))", + object.gobject_id, + object.tag_name, + object.browse_name, + object.attributes.len() + ); + } + } + } + } + Ok(()) +} + async fn session_for( connection: ConnectionArgs, session_id: String, @@ -616,6 +790,208 @@ fn parse_value(value_type: CliValueType, value: &str) -> Result Ok(parsed) } +fn print_deploy_event(event: &DeployEvent, use_json: bool) { + if use_json { + println!( + "{}", + json!({ + "sequence": event.sequence, + "observedAt": event.observed_at.as_ref().map(|ts| json!({ + "seconds": ts.seconds, + "nanos": ts.nanos, + })), + "timeOfLastDeploy": event.time_of_last_deploy.as_ref().map(|ts| json!({ + "seconds": ts.seconds, + "nanos": ts.nanos, + })), + "timeOfLastDeployPresent": event.time_of_last_deploy_present, + "objectCount": event.object_count, + "attributeCount": event.attribute_count, + }) + ); + } else { + let observed = event + .observed_at + .as_ref() + .map(|ts| format!("{}.{:09}", ts.seconds, ts.nanos)) + .unwrap_or_else(|| "(absent)".to_owned()); + let last_deploy = if event.time_of_last_deploy_present { + event + .time_of_last_deploy + .as_ref() + .map(|ts| format!("{}.{:09}", ts.seconds, ts.nanos)) + .unwrap_or_else(|| "(absent)".to_owned()) + } else { + "(absent)".to_owned() + }; + println!( + "seq={} observed={} lastDeploy={} objects={} attributes={}", + event.sequence, observed, last_deploy, event.object_count, event.attribute_count, + ); + } +} + +/// Parse a small but practically-complete subset of RFC3339: +/// `YYYY-MM-DDTHH:MM:SS[.fffffffff][Z|+HH:MM|-HH:MM]`. Returns the +/// corresponding `prost_types::Timestamp` (Unix seconds + nanoseconds). +fn parse_rfc3339_timestamp(input: &str) -> Result { + fn invalid(detail: impl Into) -> Error { + Error::InvalidArgument { + name: "last-seen-deploy-time".to_owned(), + detail: detail.into(), + } + } + + let bytes = input.as_bytes(); + if bytes.len() < 20 || (bytes[10] != b'T' && bytes[10] != b't') { + return Err(invalid(format!( + "expected RFC3339 timestamp like 2026-04-28T15:30:00Z, got {input:?}" + ))); + } + + let read_u32 = |start: usize, len: usize| -> Result { + std::str::from_utf8(&bytes[start..start + len]) + .ok() + .and_then(|slice| slice.parse::().ok()) + .ok_or_else(|| invalid(format!("non-numeric digits at byte {start}"))) + }; + + let year = read_u32(0, 4)? as i32; + if bytes[4] != b'-' { + return Err(invalid("expected '-' after year")); + } + let month = read_u32(5, 2)?; + if bytes[7] != b'-' { + return Err(invalid("expected '-' after month")); + } + let day = read_u32(8, 2)?; + let hour = read_u32(11, 2)?; + if bytes[13] != b':' { + return Err(invalid("expected ':' after hour")); + } + let minute = read_u32(14, 2)?; + if bytes[16] != b':' { + return Err(invalid("expected ':' after minute")); + } + let second = read_u32(17, 2)?; + + let mut cursor = 19usize; + let mut nanos: u32 = 0; + if cursor < bytes.len() && bytes[cursor] == b'.' { + cursor += 1; + let frac_start = cursor; + while cursor < bytes.len() && bytes[cursor].is_ascii_digit() { + cursor += 1; + } + let frac_len = cursor - frac_start; + if frac_len == 0 { + return Err(invalid("expected fractional digits after '.'")); + } + let take = frac_len.min(9); + let frac = std::str::from_utf8(&bytes[frac_start..frac_start + take]) + .ok() + .and_then(|slice| slice.parse::().ok()) + .ok_or_else(|| invalid("invalid fractional digits"))?; + nanos = frac * 10u32.pow(9u32.saturating_sub(take as u32)); + } + + let mut offset_seconds: i64 = 0; + if cursor >= bytes.len() { + return Err(invalid("missing timezone designator (Z or +HH:MM)")); + } + match bytes[cursor] { + b'Z' | b'z' => cursor += 1, + sign @ (b'+' | b'-') => { + cursor += 1; + if cursor + 5 > bytes.len() { + return Err(invalid("offset must be ±HH:MM")); + } + let oh = std::str::from_utf8(&bytes[cursor..cursor + 2]) + .ok() + .and_then(|slice| slice.parse::().ok()) + .ok_or_else(|| invalid("invalid offset hour"))?; + if bytes[cursor + 2] != b':' { + return Err(invalid("offset must contain ':' between HH and MM")); + } + let om = std::str::from_utf8(&bytes[cursor + 3..cursor + 5]) + .ok() + .and_then(|slice| slice.parse::().ok()) + .ok_or_else(|| invalid("invalid offset minute"))?; + cursor += 5; + let signed = if sign == b'-' { -1 } else { 1 }; + offset_seconds = signed * (oh * 3600 + om * 60); + } + other => { + return Err(invalid(format!( + "unexpected timezone designator byte {other:?}" + ))); + } + } + if cursor != bytes.len() { + return Err(invalid("trailing characters after timezone")); + } + + let unix = ymdhms_to_unix(year, month, day, hour, minute, second)?; + let seconds = unix - offset_seconds; + + Ok(prost_types::Timestamp { + seconds, + nanos: nanos as i32, + }) +} + +fn ymdhms_to_unix( + year: i32, + month: u32, + day: u32, + hour: u32, + minute: u32, + second: u32, +) -> Result { + if !(1..=12).contains(&month) || day < 1 || hour > 23 || minute > 59 || second > 60 { + return Err(Error::InvalidArgument { + name: "last-seen-deploy-time".to_owned(), + detail: "calendar component out of range".to_owned(), + }); + } + fn is_leap(year: i32) -> bool { + (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 + } + const DAYS: [u32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + let mut max = DAYS[(month - 1) as usize]; + if month == 2 && is_leap(year) { + max = 29; + } + if day > max { + return Err(Error::InvalidArgument { + name: "last-seen-deploy-time".to_owned(), + detail: format!("day {day} out of range for month {month}/{year}"), + }); + } + + // Days from 1970-01-01 to year-month-day. + let mut total_days: i64 = 0; + if year >= 1970 { + for y in 1970..year { + total_days += if is_leap(y) { 366 } else { 365 }; + } + } else { + for y in year..1970 { + total_days -= if is_leap(y) { 366 } else { 365 }; + } + } + for m in 1..month { + let mut dim = DAYS[(m - 1) as usize]; + if m == 2 && is_leap(year) { + dim = 29; + } + total_days += dim as i64; + } + total_days += (day - 1) as i64; + + Ok(total_days * 86_400 + hour as i64 * 3600 + minute as i64 * 60 + second as i64) +} + fn parse_cli_value(value: &str) -> Result where T: std::str::FromStr, @@ -665,4 +1041,38 @@ mod tests { assert_eq!(value["gatewayProtocolVersion"], 1); assert_eq!(value["workerProtocolVersion"], 1); } + + #[test] + fn parses_galaxy_watch_command_with_last_seen_and_max_events() { + let parsed = Cli::try_parse_from([ + "mxgw", + "galaxy", + "watch", + "--last-seen-deploy-time", + "2026-04-28T15:30:00Z", + "--max-events", + "5", + "--json", + ]); + assert!(parsed.is_ok(), "parse failed: {parsed:?}"); + } + + #[test] + fn parses_galaxy_watch_deploy_events_alias() { + let parsed = Cli::try_parse_from(["mxgw", "galaxy", "watch-deploy-events"]); + assert!(parsed.is_ok(), "parse failed: {parsed:?}"); + } + + #[test] + fn rfc3339_parser_round_trips_z_and_offset_inputs() { + // 2026-04-28T15:30:00Z = 1_777_995_000 (sanity-checked once below) + let utc = super::parse_rfc3339_timestamp("2026-04-28T15:30:00Z").unwrap(); + let plus = super::parse_rfc3339_timestamp("2026-04-28T16:30:00+01:00").unwrap(); + let frac = super::parse_rfc3339_timestamp("2026-04-28T15:30:00.250Z").unwrap(); + + assert_eq!(utc.seconds, plus.seconds); + assert_eq!(utc.nanos, 0); + assert_eq!(frac.seconds, utc.seconds); + assert_eq!(frac.nanos, 250_000_000); + } } diff --git a/clients/rust/src/galaxy.rs b/clients/rust/src/galaxy.rs new file mode 100644 index 0000000..df3cdf0 --- /dev/null +++ b/clients/rust/src/galaxy.rs @@ -0,0 +1,591 @@ +//! Thin async wrapper for the `GalaxyRepository` gRPC service. +//! +//! The wrapper mirrors [`crate::client::GatewayClient`]: it owns a tonic +//! channel with the shared bearer-token interceptor and exposes the three +//! read-only RPCs as Rust async methods. Generated Galaxy proto types are +//! re-exported through [`crate::generated::galaxy_repository::v1`]. + +use std::fs; + +use prost_types::Timestamp; +use tonic::codegen::InterceptedService; +use tonic::transport::{Certificate, Channel, ClientTlsConfig}; +use tonic::Request; + +use crate::auth::AuthInterceptor; +use crate::error::Error; +use crate::generated::galaxy_repository::v1::galaxy_repository_client::GalaxyRepositoryClient; +use crate::generated::galaxy_repository::v1::{ + DeployEvent, DiscoverHierarchyRequest, GalaxyObject, GetLastDeployTimeRequest, + TestConnectionRequest, WatchDeployEventsRequest, +}; +use crate::options::ClientOptions; + +/// Convenience alias for the generated Galaxy client wrapped in the +/// authentication interceptor. +pub type RawGalaxyClient = GalaxyRepositoryClient>; + +/// Stream of `DeployEvent` values returned by +/// [`GalaxyClient::watch_deploy_events`]. Mirrors +/// [`crate::client::EventStream`]: a boxed `Stream` whose `tonic::Status` +/// errors have already been mapped onto [`Error`]. Dropping the stream +/// cancels the underlying gRPC call. +pub type DeployEventStream = std::pin::Pin< + Box> + Send + 'static>, +>; + +/// Thin async wrapper around the generated Galaxy Repository gRPC client. +/// +/// Construct it with [`GalaxyClient::connect`] using the same +/// [`ClientOptions`] that drive [`crate::client::GatewayClient`]. The +/// service is metadata-only (no sessions) and requires the `metadata:read` +/// API-key scope on the server side. +#[derive(Clone)] +pub struct GalaxyClient { + inner: RawGalaxyClient, + call_timeout: std::time::Duration, + stream_timeout: Option, +} + +impl GalaxyClient { + /// Connect to the gateway endpoint and build a Galaxy client. Mirrors + /// the TLS / plaintext / API-key handling used by `GatewayClient`. + pub async fn connect(options: ClientOptions) -> Result { + let mut endpoint = + Channel::from_shared(options.endpoint().to_owned()).map_err(|source| { + Error::InvalidEndpoint { + endpoint: options.endpoint().to_owned(), + detail: source.to_string(), + } + })?; + endpoint = endpoint.connect_timeout(options.connect_timeout()); + + if !options.plaintext() { + let mut tls = ClientTlsConfig::new(); + if let Some(server_name) = options.server_name_override() { + tls = tls.domain_name(server_name.to_owned()); + } + if let Some(ca_file) = options.ca_file() { + let certificate = fs::read(ca_file).map_err(|source| Error::InvalidEndpoint { + endpoint: options.endpoint().to_owned(), + detail: format!("failed to read CA file {}: {source}", ca_file.display()), + })?; + tls = tls.ca_certificate(Certificate::from_pem(certificate)); + } + endpoint = endpoint.tls_config(tls)?; + } + + let channel = endpoint.connect().await?; + let interceptor = AuthInterceptor::new(options.api_key().cloned()); + + Ok(Self { + inner: GalaxyRepositoryClient::with_interceptor(channel, interceptor), + call_timeout: options.call_timeout(), + stream_timeout: options.stream_timeout(), + }) + } + + /// Build a [`GalaxyClient`] that talks through an existing tonic + /// channel. Tests use this to wire up an in-memory transport. + pub fn from_channel(channel: Channel, options: &ClientOptions) -> Self { + let interceptor = AuthInterceptor::new(options.api_key().cloned()); + Self { + inner: GalaxyRepositoryClient::with_interceptor(channel, interceptor), + call_timeout: options.call_timeout(), + stream_timeout: options.stream_timeout(), + } + } + + /// Borrow the underlying generated client for advanced callers that need + /// access to features not surfaced by the wrapper. + pub fn raw_client(&mut self) -> &mut RawGalaxyClient { + &mut self.inner + } + + /// Consume the wrapper and return the generated client. + pub fn into_inner(self) -> RawGalaxyClient { + self.inner + } + + /// Probe the Galaxy Repository database connection. Returns the `ok` + /// flag from the server reply. + pub async fn test_connection(&mut self) -> Result { + let response = self + .inner + .test_connection(self.unary_request(TestConnectionRequest {})) + .await?; + Ok(response.into_inner().ok) + } + + /// Read the most recent Galaxy deployment timestamp. Returns `None` + /// when the server reports `present = false`. + pub async fn get_last_deploy_time(&mut self) -> Result, Error> { + let response = self + .inner + .get_last_deploy_time(self.unary_request(GetLastDeployTimeRequest {})) + .await?; + let reply = response.into_inner(); + if reply.present { + Ok(reply.time_of_last_deploy) + } else { + Ok(None) + } + } + + /// Walk the deployed object hierarchy. Each [`GalaxyObject`] contains + /// the object's identifying names plus its dynamic attributes. + pub async fn discover_hierarchy(&mut self) -> Result, Error> { + let response = self + .inner + .discover_hierarchy(self.unary_request(DiscoverHierarchyRequest {})) + .await?; + Ok(response.into_inner().objects) + } + + /// Subscribe to the server-streamed deploy-event feed. + /// + /// The server emits a bootstrap event describing the current cache state + /// immediately on subscribe, then one event per observed change to + /// `galaxy.time_of_last_deploy`. When `last_seen_deploy_time` matches the + /// current cache, the bootstrap event is suppressed and the stream stays + /// idle until the next deploy. + /// + /// Cancellation is cooperative: dropping the returned + /// [`DeployEventStream`] tears down the underlying gRPC call. Callers + /// drive consumption with `StreamExt::next` (or any other `Stream` + /// adapter). + pub async fn watch_deploy_events( + &mut self, + last_seen_deploy_time: Option, + ) -> Result { + let request = WatchDeployEventsRequest { + last_seen_deploy_time, + }; + let response = self + .inner + .watch_deploy_events(self.stream_request(request)) + .await?; + let stream = futures_util::StreamExt::map(response.into_inner(), |result| { + result.map_err(Error::from) + }); + Ok(Box::pin(stream)) + } + + fn unary_request(&self, message: T) -> Request { + let mut request = Request::new(message); + request.set_timeout(self.call_timeout); + request + } + + fn stream_request(&self, message: T) -> Request { + let mut request = Request::new(message); + if let Some(timeout) = self.stream_timeout { + request.set_timeout(timeout); + } + request + } +} + +#[cfg(test)] +mod tests { + use std::pin::Pin; + use std::sync::{Arc, Mutex}; + + use futures_util::StreamExt; + use tokio::net::TcpListener; + use tokio::sync::mpsc; + use tokio_stream::wrappers::{ReceiverStream, TcpListenerStream}; + use tonic::transport::Server; + use tonic::{Request, Response, Status}; + + use super::*; + use crate::auth::ApiKey; + use crate::generated::galaxy_repository::v1::galaxy_repository_server::{ + GalaxyRepository, GalaxyRepositoryServer, + }; + use crate::generated::galaxy_repository::v1::{ + DeployEvent, DiscoverHierarchyReply, DiscoverHierarchyRequest, GalaxyAttribute, + GalaxyObject, GetLastDeployTimeReply, GetLastDeployTimeRequest, TestConnectionReply, + TestConnectionRequest, WatchDeployEventsRequest, + }; + + type DeployEventTx = mpsc::Sender>; + + #[derive(Default)] + struct FakeState { + authorization: Mutex>, + present: Mutex, + last_deploy: Mutex>, + objects: Mutex>, + watch_requests: Mutex>, + watch_events: Mutex>, + watch_senders: Mutex>, + watch_drop_signal: Mutex>>, + } + + #[derive(Clone)] + struct FakeGalaxy { + state: Arc, + } + + #[tonic::async_trait] + impl GalaxyRepository for FakeGalaxy { + async fn test_connection( + &self, + request: Request, + ) -> Result, Status> { + *self.state.authorization.lock().unwrap() = request + .metadata() + .get("authorization") + .and_then(|value| value.to_str().ok()) + .map(str::to_owned); + Ok(Response::new(TestConnectionReply { ok: true })) + } + + async fn get_last_deploy_time( + &self, + _request: Request, + ) -> Result, Status> { + let present = *self.state.present.lock().unwrap(); + let time = self.state.last_deploy.lock().unwrap().clone(); + Ok(Response::new(GetLastDeployTimeReply { + present, + time_of_last_deploy: time, + })) + } + + async fn discover_hierarchy( + &self, + _request: Request, + ) -> Result, Status> { + Ok(Response::new(DiscoverHierarchyReply { + objects: self.state.objects.lock().unwrap().clone(), + })) + } + + type WatchDeployEventsStream = + Pin> + Send + 'static>>; + + async fn watch_deploy_events( + &self, + request: Request, + ) -> Result, Status> { + self.state + .watch_requests + .lock() + .unwrap() + .push(request.into_inner()); + + let preset = self.state.watch_events.lock().unwrap().clone(); + let (tx, rx) = mpsc::channel::>(16); + for event in preset { + tx.send(Ok(event)) + .await + .map_err(|err| Status::internal(err.to_string()))?; + } + self.state.watch_senders.lock().unwrap().push(tx.clone()); + + let drop_signal = self.state.watch_drop_signal.lock().unwrap().clone(); + let stream = ReceiverStream::new(rx); + let stream: Pin + Send + 'static>> = + if let Some(signal) = drop_signal { + Box::pin(WatchStreamWithDropSignal { + inner: stream, + signal: Some(signal), + }) + } else { + Box::pin(stream) + }; + + Ok(Response::new(stream)) + } + } + + /// Wraps the receiver stream so we can detect when the server-side stream + /// future is dropped (the client cancelled or dropped the stream). Used by + /// `watch_drop_tears_down_call`. + struct WatchStreamWithDropSignal { + inner: S, + signal: Option>, + } + + impl tokio_stream::Stream for WatchStreamWithDropSignal { + type Item = S::Item; + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + Pin::new(&mut self.inner).poll_next(cx) + } + } + + impl Drop for WatchStreamWithDropSignal { + fn drop(&mut self) { + if let Some(signal) = self.signal.take() { + let _ = signal.send(()); + } + } + } + + async fn spawn_fake(state: Arc) -> String { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = listener.local_addr().unwrap(); + let incoming = TcpListenerStream::new(listener); + let service = GalaxyRepositoryServer::new(FakeGalaxy { state }); + tokio::spawn(async move { + Server::builder() + .add_service(service) + .serve_with_incoming(incoming) + .await + .unwrap(); + }); + format!("http://{address}") + } + + #[tokio::test] + async fn test_connection_attaches_bearer_metadata_and_returns_ok() { + let state = Arc::new(FakeState::default()); + let endpoint = spawn_fake(state.clone()).await; + + let mut client = GalaxyClient::connect( + ClientOptions::new(endpoint).with_api_key(ApiKey::new("mxgw_galaxy_secret")), + ) + .await + .unwrap(); + + let ok = client.test_connection().await.unwrap(); + + assert!(ok); + assert_eq!( + state.authorization.lock().unwrap().as_deref(), + Some("Bearer mxgw_galaxy_secret") + ); + } + + #[tokio::test] + async fn get_last_deploy_time_returns_none_when_not_present() { + let state = Arc::new(FakeState::default()); + *state.present.lock().unwrap() = false; + *state.last_deploy.lock().unwrap() = Some(Timestamp { + seconds: 1_700_000_000, + nanos: 0, + }); + let endpoint = spawn_fake(state.clone()).await; + + let mut client = GalaxyClient::connect(ClientOptions::new(endpoint)) + .await + .unwrap(); + + let result = client.get_last_deploy_time().await.unwrap(); + + assert!( + result.is_none(), + "present=false on the wire must surface as None, got {result:?}" + ); + } + + #[tokio::test] + async fn get_last_deploy_time_returns_timestamp_when_present() { + let state = Arc::new(FakeState::default()); + *state.present.lock().unwrap() = true; + *state.last_deploy.lock().unwrap() = Some(Timestamp { + seconds: 1_700_000_000, + nanos: 250_000_000, + }); + let endpoint = spawn_fake(state.clone()).await; + + let mut client = GalaxyClient::connect(ClientOptions::new(endpoint)) + .await + .unwrap(); + + let result = client.get_last_deploy_time().await.unwrap(); + + let timestamp = result.expect("present=true should yield a timestamp"); + assert_eq!(timestamp.seconds, 1_700_000_000); + assert_eq!(timestamp.nanos, 250_000_000); + } + + #[tokio::test] + async fn discover_hierarchy_returns_objects_with_attributes() { + let state = Arc::new(FakeState::default()); + *state.objects.lock().unwrap() = vec![GalaxyObject { + gobject_id: 42, + tag_name: "DelmiaReceiver_001".to_owned(), + contained_name: "DelmiaReceiver".to_owned(), + browse_name: "TestMachine_001/DelmiaReceiver".to_owned(), + parent_gobject_id: 7, + is_area: false, + category_id: 3, + hosted_by_gobject_id: 1, + template_chain: vec!["$UserDefined".to_owned(), "$DelmiaReceiver".to_owned()], + attributes: vec![GalaxyAttribute { + attribute_name: "DownloadPath".to_owned(), + full_tag_reference: "DelmiaReceiver_001.DownloadPath".to_owned(), + mx_data_type: 8, + data_type_name: "MxString".to_owned(), + is_array: false, + array_dimension: 0, + array_dimension_present: false, + mx_attribute_category: 2, + security_classification: 1, + is_historized: false, + is_alarm: false, + }], + }]; + let endpoint = spawn_fake(state.clone()).await; + + let mut client = GalaxyClient::connect(ClientOptions::new(endpoint)) + .await + .unwrap(); + + let objects = client.discover_hierarchy().await.unwrap(); + + assert_eq!(objects.len(), 1); + assert_eq!(objects[0].tag_name, "DelmiaReceiver_001"); + assert_eq!(objects[0].attributes.len(), 1); + assert_eq!(objects[0].attributes[0].attribute_name, "DownloadPath"); + assert_eq!( + objects[0].attributes[0].full_tag_reference, + "DelmiaReceiver_001.DownloadPath" + ); + } + + #[tokio::test] + async fn watch_deploy_events_yields_events_in_order() { + let state = Arc::new(FakeState::default()); + *state.watch_events.lock().unwrap() = vec![ + DeployEvent { + sequence: 1, + observed_at: Some(Timestamp { + seconds: 1_700_000_000, + nanos: 0, + }), + time_of_last_deploy: Some(Timestamp { + seconds: 1_699_000_000, + nanos: 0, + }), + time_of_last_deploy_present: true, + object_count: 12, + attribute_count: 80, + }, + DeployEvent { + sequence: 2, + observed_at: Some(Timestamp { + seconds: 1_700_000_500, + nanos: 0, + }), + time_of_last_deploy: Some(Timestamp { + seconds: 1_699_500_000, + nanos: 0, + }), + time_of_last_deploy_present: true, + object_count: 13, + attribute_count: 85, + }, + ]; + let endpoint = spawn_fake(state.clone()).await; + + let mut client = GalaxyClient::connect(ClientOptions::new(endpoint)) + .await + .unwrap(); + + let mut stream = client.watch_deploy_events(None).await.unwrap(); + + let first = stream + .next() + .await + .expect("bootstrap event") + .expect("ok deploy event"); + let second = stream + .next() + .await + .expect("second event") + .expect("ok deploy event"); + + assert_eq!(first.sequence, 1); + assert_eq!(first.object_count, 12); + assert_eq!(second.sequence, 2); + assert_eq!(second.object_count, 13); + assert!(first.time_of_last_deploy_present); + } + + #[tokio::test] + async fn watch_deploy_events_propagates_last_seen_deploy_time() { + let state = Arc::new(FakeState::default()); + let endpoint = spawn_fake(state.clone()).await; + + let mut client = GalaxyClient::connect(ClientOptions::new(endpoint)) + .await + .unwrap(); + + let last_seen = Timestamp { + seconds: 1_699_999_999, + nanos: 123_456_789, + }; + let stream = client.watch_deploy_events(Some(last_seen)).await.unwrap(); + + // Drop the stream right away — the test is solely about the request + // payload reaching the server. + drop(stream); + + // Give the server task a moment to record the request. + for _ in 0..20 { + if !state.watch_requests.lock().unwrap().is_empty() { + break; + } + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + } + + let requests = state.watch_requests.lock().unwrap().clone(); + assert_eq!(requests.len(), 1); + let recorded = requests[0] + .last_seen_deploy_time + .as_ref() + .expect("last_seen_deploy_time forwarded"); + assert_eq!(recorded.seconds, last_seen.seconds); + assert_eq!(recorded.nanos, last_seen.nanos); + } + + #[tokio::test] + async fn watch_deploy_events_drop_tears_down_call() { + let state = Arc::new(FakeState::default()); + let (signal_tx, mut signal_rx) = mpsc::unbounded_channel(); + *state.watch_drop_signal.lock().unwrap() = Some(signal_tx); + // Seed one event so the client gets something on the stream before we + // drop it; this proves the call is live. + *state.watch_events.lock().unwrap() = vec![DeployEvent { + sequence: 7, + observed_at: None, + time_of_last_deploy: None, + time_of_last_deploy_present: false, + object_count: 0, + attribute_count: 0, + }]; + let endpoint = spawn_fake(state.clone()).await; + + let mut client = GalaxyClient::connect(ClientOptions::new(endpoint)) + .await + .unwrap(); + + let mut stream = client.watch_deploy_events(None).await.unwrap(); + let event = stream + .next() + .await + .expect("bootstrap event") + .expect("ok deploy event"); + assert_eq!(event.sequence, 7); + + // Dropping the client-side stream must trigger the server-side stream + // future to be dropped as well, signalling cancellation. + drop(stream); + + let drop_seen = tokio::time::timeout(std::time::Duration::from_secs(2), signal_rx.recv()) + .await + .expect("server-side stream future was not dropped within 2s"); + assert!( + drop_seen.is_some(), + "drop signal channel closed unexpectedly" + ); + } +} diff --git a/clients/rust/src/generated.rs b/clients/rust/src/generated.rs index 6351f7c..fbd9dbc 100644 --- a/clients/rust/src/generated.rs +++ b/clients/rust/src/generated.rs @@ -13,3 +13,11 @@ pub mod mxaccess_worker { tonic::include_proto!("mxaccess_worker.v1"); } } + +pub mod galaxy_repository { + pub mod v1 { + #![allow(clippy::large_enum_variant)] + + tonic::include_proto!("galaxy_repository.v1"); + } +} diff --git a/clients/rust/src/lib.rs b/clients/rust/src/lib.rs index d1f4b55..ac95672 100644 --- a/clients/rust/src/lib.rs +++ b/clients/rust/src/lib.rs @@ -7,6 +7,7 @@ pub mod auth; pub mod client; pub mod error; +pub mod galaxy; pub mod generated; pub mod options; pub mod session; @@ -16,6 +17,7 @@ pub mod version; pub use auth::{ApiKey, AuthInterceptor}; pub use client::{EventStream, GatewayClient}; pub use error::{CommandError, Error}; +pub use galaxy::{DeployEventStream, GalaxyClient}; pub use options::ClientOptions; pub use session::Session; pub use value::{MxArrayProjection, MxArrayValue, MxStatus, MxValue, MxValueProjection}; diff --git a/docs/Authentication.md b/docs/Authentication.md new file mode 100644 index 0000000..2732fe8 --- /dev/null +++ b/docs/Authentication.md @@ -0,0 +1,266 @@ +# Gateway Authentication + +The gateway authentication subsystem verifies inbound API key credentials against a SQLite-backed key store, hashes secrets with a configurable pepper, and records administrative and verification events to an audit trail. + +## Token Format + +API keys travel in the HTTP `Authorization` header as a bearer token shaped `mxgw__`. The `mxgw_` prefix scopes parsing to gateway tokens, the `` segment is the public identifier used for lookup, and `` is the high-entropy portion that the gateway verifies against a stored hash. + +`ApiKeyParser` enforces the format and rejects malformed tokens before any database round-trip: + +```csharp +public bool TryParseAuthorizationHeader(string? authorizationHeader, out ParsedApiKey? apiKey) +{ + apiKey = null; + + if (string.IsNullOrWhiteSpace(authorizationHeader) + || !authorizationHeader.StartsWith(BearerPrefix, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + string token = authorizationHeader[BearerPrefix.Length..].Trim(); + + if (!token.StartsWith(TokenPrefix, StringComparison.OrdinalIgnoreCase)) + { + return false; + } +``` + +A successful parse produces a `ParsedApiKey(KeyId, Secret)` record. The `IApiKeyParser` interface exists so verification consumers can be tested without depending on header-format details. + +## Parsing and Secrets + +### Secret generation + +`ApiKeySecretGenerator.Generate()` is the single source of new secret material. It uses 32 bytes from `RandomNumberGenerator.Fill` and encodes with URL-safe base64 (no padding) so secrets can be embedded in headers without escaping: + +```csharp +public static string Generate() +{ + Span bytes = stackalloc byte[32]; + RandomNumberGenerator.Fill(bytes); + + return Convert.ToBase64String(bytes) + .TrimEnd('=') + .Replace('+', '-') + .Replace('/', '_'); +} +``` + +### Peppered hashing + +`ApiKeySecretHasher` (registered behind `IApiKeySecretHasher`) hashes secrets with `HMACSHA256` keyed by a server-side pepper. The pepper lives outside the database and is resolved by `IConfiguration` lookup against the configured `PepperSecretName`: + +```csharp +public byte[] HashSecret(string secret) +{ + string pepper = GetPepper(); + byte[] pepperBytes = Encoding.UTF8.GetBytes(pepper); + byte[] secretBytes = Encoding.UTF8.GetBytes(secret); + + using HMACSHA256 hmac = new(pepperBytes); + + return hmac.ComputeHash(secretBytes); +} +``` + +The pepper is intentionally not stored alongside the hash: an attacker who exfiltrates only the SQLite file holds the hashes but lacks the keying material to brute-force candidate secrets, even if the stored hash algorithm and salt scheme are known. If the pepper is missing the hasher throws `ApiKeyPepperUnavailableException`, which the verifier converts to a distinct failure code rather than treating it as a credential mismatch. + +## Verification + +`ApiKeyVerifier` (`IApiKeyVerifier`) implements the verification flow: + +1. Parse the `Authorization` header into a `ParsedApiKey`. +2. Look up the `ApiKeyRecord` by `KeyId` through `IApiKeyStore.FindByKeyIdAsync`. +3. Reject revoked records (`RevokedUtc is not null`). +4. Hash the presented secret with the configured pepper. +5. Compare hashes with `CryptographicOperations.FixedTimeEquals` to avoid timing oracles. +6. Record a `LastUsedUtc` timestamp via `MarkKeyUsedAsync` and return an `ApiKeyIdentity`. + +```csharp +if (!CryptographicOperations.FixedTimeEquals(presentedHash, storedKey.SecretHash)) +{ + return ApiKeyVerificationResult.Fail(ApiKeyVerificationFailure.SecretMismatch); +} + +await keyStore.MarkKeyUsedAsync(storedKey.KeyId, DateTimeOffset.UtcNow, cancellationToken) + .ConfigureAwait(false); + +return ApiKeyVerificationResult.Success(new ApiKeyIdentity( + KeyId: storedKey.KeyId, + KeyPrefix: storedKey.KeyPrefix, + DisplayName: storedKey.DisplayName, + Scopes: storedKey.Scopes)); +``` + +`ApiKeyVerificationResult` carries either an `ApiKeyIdentity` or a discriminated `ApiKeyVerificationFailure` value. The failure enum distinguishes parse errors, missing pepper, missing or revoked keys, and secret mismatch so the calling middleware can emit precise audit detail without leaking which check failed to the client. + +`ApiKeyIdentity` exposes only non-secret fields (`KeyId`, `KeyPrefix`, `DisplayName`, `Scopes`) and is the type downstream authorization code consumes. + +## Storage + +The gateway keeps API key state in a dedicated SQLite database. SQLite is sufficient because credential volume is small, the gateway runs as a single process, and the file is straightforward to back up and rotate independently of the main application data. + +### Connection factory + +`AuthSqliteConnectionFactory` reads `GatewayOptions.Authentication.SqlitePath`, ensures the parent directory exists, and opens the connection in `ReadWriteCreate` mode so first-run installations can create the file without manual provisioning: + +```csharp +public SqliteConnection CreateConnection() +{ + string sqlitePath = options.Value.Authentication.SqlitePath; + string? directory = Path.GetDirectoryName(sqlitePath); + + if (!string.IsNullOrWhiteSpace(directory)) + { + Directory.CreateDirectory(directory); + } + + SqliteConnectionStringBuilder builder = new() + { + DataSource = sqlitePath, + Mode = SqliteOpenMode.ReadWriteCreate + }; + + return new SqliteConnection(builder.ToString()); +} +``` + +### Schema + +`SqliteAuthSchema` declares table names and the current schema version as constants. Three tables are involved: + +- `api_keys` stores `key_id`, `key_prefix`, the `secret_hash` blob, `display_name`, serialized `scopes`, and the `created_utc`, `last_used_utc`, and `revoked_utc` timestamps. +- `api_key_audit` is an append-only log keyed by an autoincrement `audit_id` with `key_id`, `event_type`, `remote_address`, `created_utc`, and `details` columns. +- `schema_version` carries a single row whose `version` column is matched against `SqliteAuthSchema.CurrentVersion`. + +### Read paths + +`SqliteApiKeyStore` (`IApiKeyStore`) handles the two reads needed at request time: `FindByKeyIdAsync` returns any record (so revoked keys can be reported distinctly) and `FindActiveByKeyIdAsync` filters to non-revoked rows. `MarkKeyUsedAsync` updates `last_used_utc` only for non-revoked rows so a freshly revoked key cannot have its timestamp refreshed by a racing verification. + +`ApiKeyRecord` is the in-memory projection. `ApiKeyRecordReader.Read` is shared by every read path so column ordering is defined in one place: + +```csharp +public static ApiKeyRecord Read(SqliteDataReader reader) +{ + return new ApiKeyRecord( + KeyId: reader.GetString(0), + KeyPrefix: reader.GetString(1), + SecretHash: (byte[])reader["secret_hash"], + DisplayName: reader.GetString(3), + Scopes: ApiKeyScopeSerializer.Deserialize(reader.GetString(4)), + CreatedUtc: DateTimeOffset.Parse(reader.GetString(5), System.Globalization.CultureInfo.InvariantCulture), + LastUsedUtc: ReadNullableDateTimeOffset(reader, 6), + RevokedUtc: ReadNullableDateTimeOffset(reader, 7)); +} +``` + +### Write paths + +`SqliteApiKeyAdminStore` (`IApiKeyAdminStore`) implements administrative mutations: `CreateAsync` accepts an `ApiKeyCreateRequest`, `RevokeAsync` sets `revoked_utc` only when not already revoked, and `RotateAsync` replaces `secret_hash`, clears `last_used_utc`, and clears `revoked_utc` so a rotated key is immediately usable. + +### Audit trail + +`SqliteApiKeyAuditStore` (`IApiKeyAuditStore`) appends `ApiKeyAuditEntry` values to the `api_key_audit` table and stamps each row with a UTC timestamp inside the store rather than trusting the caller. `ListRecentAsync` returns the most recent rows ordered by `audit_id` descending and projects them into `ApiKeyAuditRecord`. Rows are kept even after the referenced key is revoked because the audit history is the durable record of administrative action; the `key_id` column is nullable to accommodate non-key-scoped events such as `init-db`. + +## Migration + +Schema bring-up is centralised behind `IAuthStoreMigrator`. `SqliteAuthStoreMigrator` executes the migration inside a single transaction so a partial failure leaves the database untouched, refuses to start when the on-disk schema version is newer than the binary supports, and idempotently creates the v1 schema: + +```csharp +if (existingVersion > SqliteAuthSchema.CurrentVersion) +{ + throw new AuthStoreMigrationException( + $"Auth database schema version {existingVersion} is newer than supported version {SqliteAuthSchema.CurrentVersion}."); +} + +await ApplyVersionOneAsync(connection, transaction, cancellationToken).ConfigureAwait(false); + +await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); +``` + +`AuthStoreMigrationHostedService` runs the migrator at startup, but only when API-key authentication is enabled and `RunMigrationsOnStartup` is true. Operators who manage schema out-of-band can disable the hosted run and use the admin CLI's `init-db` command instead. + +`AuthStoreMigrationException` is a sealed `InvalidOperationException` so it can be caught precisely without swallowing unrelated failures. + +## Admin CLI + +`ApiKeyAdminCommandLineParser.Parse` recognises a leading `apikey` argument and dispatches to one of the subcommands declared by `ApiKeyAdminCommandKind`. Each parsed invocation produces an `ApiKeyAdminCommand` (or an `ApiKeyAdminParseResult` carrying an error). `ApiKeyAdminCliRunner` then executes the command, runs the migrator first, calls the relevant store method, appends an audit row, and writes either text or JSON output via `ApiKeyAdminOutput`. The returned `ApiKeyAdminListedKey` projection deliberately omits the `secret_hash` so listing a database does not surface hash material. + +The supported subcommands match `ApiKeyAdminCommandKind` exactly: + +| Subcommand | Required options | Behaviour | +|------------|------------------|-----------| +| `init-db` | none | Runs the migrator and records an audit entry. | +| `create-key` | `--key-id`, `--display-name` | Generates a new secret, stores its peppered hash, and prints the assembled `mxgw__` token. | +| `list-keys` | none | Lists every stored key with its scopes and revocation state. | +| `revoke-key` | `--key-id` | Sets `revoked_utc` if the key is currently active. | +| `rotate-key` | `--key-id` | Replaces the secret hash and prints the new token. | + +Examples: + +```bash +mxgateway apikey init-db +mxgateway apikey create-key --key-id ops.alice --display-name "Alice (ops)" --scopes read,write +mxgateway apikey list-keys --json +mxgateway apikey revoke-key --key-id ops.alice +mxgateway apikey rotate-key --key-id ops.alice +``` + +Key ids are restricted by the parser to ASCII letters, digits, periods, and hyphens so they remain safe to embed in the token format and in URL paths used by administrative tooling. + +## Scope Serialization + +Scopes are persisted as a single TEXT column rather than a join table because the set is small, never queried by membership at the database level, and changes atomically with the owning row. `ApiKeyScopeSerializer.Serialize` writes a JSON array sorted with `StringComparer.Ordinal` so equivalent scope sets produce byte-identical column values, which makes audit diffing and database comparisons deterministic: + +```csharp +public static string Serialize(IReadOnlySet scopes) +{ + return JsonSerializer.Serialize(scopes.Order(StringComparer.Ordinal)); +} + +public static IReadOnlySet Deserialize(string value) +{ + if (string.IsNullOrWhiteSpace(value)) + { + return new HashSet(StringComparer.Ordinal); + } + + string[]? scopes = JsonSerializer.Deserialize(value); + + return new HashSet(scopes ?? [], StringComparer.Ordinal); +} +``` + +`Deserialize` tolerates an empty column by returning an empty set so older rows or hand-edited records do not crash the verifier. + +## Registration + +`AuthStoreServiceCollectionExtensions.AddSqliteAuthStore` wires every service in this subsystem as a singleton and registers the migration hosted service: + +```csharp +public static IServiceCollection AddSqliteAuthStore(this IServiceCollection services) +{ + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddHostedService(); + + return services; +} +``` + +Singletons are safe because each operation opens its own short-lived `SqliteConnection` through the factory; there is no shared mutable state inside the services. + +## Related Documentation + +- [Gateway Configuration](./GatewayConfiguration.md) +- [Authorization](./Authorization.md) +- [Diagnostics](./Diagnostics.md) diff --git a/docs/Authorization.md b/docs/Authorization.md new file mode 100644 index 0000000..9265313 --- /dev/null +++ b/docs/Authorization.md @@ -0,0 +1,214 @@ +# Gateway gRPC Authorization + +The authorization subsystem enforces per-RPC scope checks against the authenticated `ApiKeyIdentity` produced by the authentication layer, so service implementations never need to repeat permission logic. + +## Overview + +Authorization runs as a single gRPC server interceptor registered for every call on the gateway. It pulls the authenticated identity for the current request, derives the scope that the request type requires, and either lets the call continue or fails the call with a gRPC status. The pipeline keeps service classes free of cross-cutting checks, which matches the AGENTS.md "thin gRPC layer" rule that service handlers translate between contracts and domain code without owning policy. + +The participating types live under `src/MxGateway.Server/Security/Authorization/`: + +- `GatewayGrpcAuthorizationInterceptor` runs the authenticate-then-authorize pipeline for unary and server-streaming calls. +- `GatewayGrpcScopeResolver` maps a request message (and, for `MxCommandRequest`, the inner `MxCommandKind`) to the scope string that must be present on the caller. +- `GatewayScopes` exposes the canonical scope constants used by the resolver and any downstream consumer. +- `GatewayRequestIdentityAccessor` and `IGatewayRequestIdentityAccessor` expose the verified identity to handlers and any service code that runs inside the call. +- `GrpcAuthorizationServiceCollectionExtensions` wires the components into the DI container and the gRPC pipeline. + +The `ApiKeyIdentity` consumed here is produced by the authentication layer; see [Authentication](./Authentication.md) for how it is built and how scopes are persisted. + +## Why an Interceptor + +Centralizing the policy in `GatewayGrpcAuthorizationInterceptor` produces three concrete benefits: + +1. Every RPC defined in `MxAccessGatewayService` is covered by construction. A new RPC inherits the check the moment its request type is added to `GatewayGrpcScopeResolver`, instead of relying on each service method to remember to call an authorization helper. +2. The service class stays a thin translator between proto contracts and domain calls. RPC methods do not branch on identity or scope, which keeps the AGENTS.md guideline that gRPC handlers contain no policy. +3. Authentication and authorization happen in one place, so the gRPC `Status` mapping is consistent. A failed key check always returns `Unauthenticated`, and a missing scope always returns `PermissionDenied` with the offending scope name. + +## Interceptor Flow + +`GatewayGrpcAuthorizationInterceptor` overrides both `UnaryServerHandler` and `ServerStreamingServerHandler`. Both call the same private `AuthenticateAndAuthorizeAsync` helper before invoking the continuation, then push the resolved identity onto the accessor for the duration of the call. + +```csharp +public override async Task UnaryServerHandler( + TRequest request, + ServerCallContext context, + UnaryServerMethod continuation) +{ + ApiKeyIdentity? identity = await AuthenticateAndAuthorizeAsync(request, context).ConfigureAwait(false); + IDisposable? identityScope = identity is null ? null : identityAccessor.Push(identity); + using (identityScope) + { + return await continuation(request, context).ConfigureAwait(false); + } +} +``` + +The shared helper performs the actual decision: + +```csharp +if (options.Value.Authentication.Mode == AuthenticationMode.Disabled) +{ + return null; +} + +string? authorizationHeader = context.RequestHeaders.GetValue("authorization"); +ApiKeyVerificationResult verificationResult = await apiKeyVerifier + .VerifyAsync(authorizationHeader, context.CancellationToken) + .ConfigureAwait(false); + +if (!verificationResult.Succeeded || verificationResult.Identity is null) +{ + throw new RpcException(new Status( + StatusCode.Unauthenticated, + "Missing or invalid API key.")); +} + +string requiredScope = scopeResolver.ResolveRequiredScope(request); +if (!verificationResult.Identity.Scopes.Contains(requiredScope)) +{ + throw new RpcException(new Status( + StatusCode.PermissionDenied, + $"API key is missing required scope '{requiredScope}'.")); +} + +return verificationResult.Identity; +``` + +The flow is: + +1. If `GatewayOptions.Authentication.Mode` is `AuthenticationMode.Disabled`, the helper returns `null` immediately. No identity is pushed onto the accessor and the continuation runs without scope enforcement. This matches the `AuthenticationMode` enum, which only defines `ApiKey` and `Disabled`. +2. Otherwise, the `authorization` request header is read directly off `ServerCallContext.RequestHeaders` and handed to `IApiKeyVerifier.VerifyAsync`. A failed verification or a missing identity throws `RpcException` with `StatusCode.Unauthenticated`. +3. `GatewayGrpcScopeResolver.ResolveRequiredScope(request)` produces the scope string. If the identity's `Scopes` set does not contain it, the helper throws `RpcException` with `StatusCode.PermissionDenied` and embeds the missing scope name in `Status.Detail` so callers can diagnose the failure. +4. On success, the verified `ApiKeyIdentity` is returned and pushed onto `IGatewayRequestIdentityAccessor` for the lifetime of the call. + +The status codes are deliberately distinct: `Unauthenticated` signals "we do not know who you are," and `PermissionDenied` signals "we know who you are, but you cannot do this." Treating the two as the same code would make troubleshooting harder for client implementations. + +## Scope Resolution + +`GatewayGrpcScopeResolver` is a stateless singleton that switches on the runtime request type. Top-level RPC requests map directly: + +```csharp +public string ResolveRequiredScope(object request) +{ + return request switch + { + OpenSessionRequest => GatewayScopes.SessionOpen, + CloseSessionRequest => GatewayScopes.SessionClose, + StreamEventsRequest => GatewayScopes.EventsRead, + MxCommandRequest commandRequest => ResolveCommandScope(commandRequest.Command?.Kind ?? MxCommandKind.Unspecified), + _ => GatewayScopes.Admin + }; +} +``` + +The `_ => GatewayScopes.Admin` fallback is intentional: any future request type that the resolver does not recognize fails closed, requiring the strongest scope until the resolver is updated. + +`MxCommandRequest` is special because it multiplexes many MxAccess operations through a single RPC. The resolver inspects the embedded `MxCommandKind` so each operation gets its own scope: + +```csharp +private static string ResolveCommandScope(MxCommandKind kind) +{ + return kind switch + { + MxCommandKind.Write or + MxCommandKind.Write2 => GatewayScopes.InvokeWrite, + + MxCommandKind.WriteSecured or + MxCommandKind.WriteSecured2 or + MxCommandKind.AuthenticateUser => GatewayScopes.InvokeSecure, + + MxCommandKind.ArchestraUserToId or + MxCommandKind.GetSessionState or + MxCommandKind.GetWorkerInfo => GatewayScopes.MetadataRead, + + MxCommandKind.DrainEvents => GatewayScopes.EventsRead, + MxCommandKind.ShutdownWorker => GatewayScopes.Admin, + + _ => GatewayScopes.InvokeRead + }; +} +``` + +Reads (`Register`, `AddItem`, `Advise`, and any other unspecified kind) fall through to `InvokeRead`, which keeps the matrix small while still separating reads from writes, secured writes, metadata lookups, event drains, and worker shutdown. + +## Scope Catalog + +`GatewayScopes` is the single source of truth for scope strings. Every entry is currently mapped by either the resolver or another security component: + +| Constant | Value | Required For | +|----------|-------|--------------| +| `SessionOpen` | `session:open` | `OpenSessionRequest` | +| `SessionClose` | `session:close` | `CloseSessionRequest` | +| `EventsRead` | `events:read` | `StreamEventsRequest`, `MxCommandKind.DrainEvents` | +| `InvokeRead` | `invoke:read` | `MxCommandRequest` for read-style command kinds (`Register`, `AddItem`, `Advise`, and any kind not otherwise mapped) | +| `InvokeWrite` | `invoke:write` | `MxCommandKind.Write`, `MxCommandKind.Write2` | +| `InvokeSecure` | `invoke:secure` | `MxCommandKind.WriteSecured`, `MxCommandKind.WriteSecured2`, `MxCommandKind.AuthenticateUser` | +| `MetadataRead` | `metadata:read` | `MxCommandKind.ArchestraUserToId`, `MxCommandKind.GetSessionState`, `MxCommandKind.GetWorkerInfo`, `GalaxyRepository.TestConnection`, `GalaxyRepository.GetLastDeployTime`, `GalaxyRepository.DiscoverHierarchy`, `GalaxyRepository.WatchDeployEvents` | +| `Admin` | `admin` | `MxCommandKind.ShutdownWorker`, the default for any unrecognized request type, and the dashboard authorization policy | + +The `Admin` constant is also referenced by `DashboardAuthenticator` and `DashboardAuthorizationHandler` so that the dashboard and the gRPC layer agree on what "admin" means. + +## Identity Access for Downstream Layers + +Once authorization passes, `GatewayGrpcAuthorizationInterceptor` calls `identityAccessor.Push(identity)` and disposes the returned scope when the continuation completes. `GatewayRequestIdentityAccessor` stores the active identity in an `AsyncLocal`, so the value flows across `await` boundaries and child tasks belonging to the same request. + +```csharp +public sealed class GatewayRequestIdentityAccessor : IGatewayRequestIdentityAccessor +{ + private readonly AsyncLocal currentIdentity = new(); + + public ApiKeyIdentity? Current => currentIdentity.Value; + + public IDisposable Push(ApiKeyIdentity identity) + { + ArgumentNullException.ThrowIfNull(identity); + + ApiKeyIdentity? previousIdentity = currentIdentity.Value; + currentIdentity.Value = identity; + + return new IdentityScope(this, previousIdentity); + } +} +``` + +The returned `IdentityScope` restores the previous value on dispose rather than clearing it. This makes the accessor safe for nested pushes, even though the current interceptor only pushes once per call. Disposing twice is a no-op because of the `disposed` guard inside `IdentityScope`. + +Downstream code consumes the accessor through the `IGatewayRequestIdentityAccessor` interface: + +```csharp +public interface IGatewayRequestIdentityAccessor +{ + ApiKeyIdentity? Current { get; } + + IDisposable Push(ApiKeyIdentity identity); +} +``` + +`MxAccessGatewayService` takes `IGatewayRequestIdentityAccessor` as a constructor dependency and reads `Current` whenever it needs to attach the calling identity to a domain operation, which keeps the service free of header parsing or scope checks. + +When `AuthenticationMode.Disabled` is configured, no identity is pushed, so `Current` returns `null`. Downstream code must tolerate that, just as it tolerates the absence of a scope check. + +## Registration + +`GrpcAuthorizationServiceCollectionExtensions.AddGatewayGrpcAuthorization` is the single entry point that registers every component and inserts the interceptor into the gRPC pipeline: + +```csharp +public static IServiceCollection AddGatewayGrpcAuthorization(this IServiceCollection services) +{ + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddGrpc(options => options.Interceptors.Add()); + + return services; +} +``` + +Singleton lifetimes are appropriate because none of the three classes hold per-request state on instance fields; the request-scoped value lives inside the `AsyncLocal` on `GatewayRequestIdentityAccessor`. `GatewayApplication` calls `builder.Services.AddGatewayGrpcAuthorization()` during startup, and the call also performs `AddGrpc`, so the gateway never registers gRPC without the interceptor attached. + +## Related Documentation + +- [Authentication](./Authentication.md) +- [Grpc](./Grpc.md) +- [GatewayConfiguration](./GatewayConfiguration.md) +- [Galaxy Repository Browse](./GalaxyRepository.md) diff --git a/docs/Contracts.md b/docs/Contracts.md index 0369015..0b1f8d9 100644 --- a/docs/Contracts.md +++ b/docs/Contracts.md @@ -28,6 +28,13 @@ worker IPC envelope and control messages. It imports `mxaccess_gateway.proto` so the worker and gateway use the same command, reply, event, value, and status shapes. +`src/MxGateway.Contracts/Protos/galaxy_repository.proto` defines the +`GalaxyRepository` service used by clients to browse the Galaxy Repository +(deployed object hierarchy and dynamic attributes). The service is metadata- +only and does not share types with `mxaccess_gateway.proto`. See +[Galaxy Repository Browse](./GalaxyRepository.md) for the RPC catalog and +behavior. + Generated C# output is written to `src/MxGateway.Contracts/Generated/`. Do not hand-edit generated files. diff --git a/docs/Diagnostics.md b/docs/Diagnostics.md new file mode 100644 index 0000000..bc9d3b9 --- /dev/null +++ b/docs/Diagnostics.md @@ -0,0 +1,222 @@ +# Gateway Diagnostics + +The diagnostics subsystem provides structured logging, credential redaction, and request-scoped log enrichment for the gateway. It lives under `src/MxGateway.Server/Diagnostics/` and is wired into the ASP.NET Core pipeline so every gRPC and HTTP request carries the same correlation fields. + +## Goals + +The subsystem exists to satisfy two security rules from `AGENTS.md`: never log passwords or raw credential values for `AuthenticateUser`, `WriteSecured`, or related secured operations, and never log full MXAccess values by default. Code paths that touch credentials or tag values must therefore route through `GatewayLogRedactor` rather than emitting them directly. + +A second goal is parity-test diagnosability. Because MXAccess sessions, workers, correlation ids, and command methods are the units of comparison, every log entry produced inside a request scope must carry those identifiers without each call site having to format them. + +## Log Scopes + +`GatewayLogScope` is a record that captures the fields attached to a logger scope. It only emits keys whose values are non-null, so callers can supply just the identifiers they know about: + +```csharp +public sealed record GatewayLogScope( + string? SessionId = null, + int? WorkerProcessId = null, + ulong? CorrelationId = null, + string? CommandMethod = null, + string? ClientIdentity = null) +{ + public IReadOnlyDictionary ToDictionary() + { + Dictionary values = []; + + AddIfPresent(values, "SessionId", SessionId); + AddIfPresent(values, "WorkerProcessId", WorkerProcessId); + AddIfPresent(values, "CorrelationId", CorrelationId); + AddIfPresent(values, "CommandMethod", CommandMethod); + AddIfPresent(values, "ClientIdentity", GatewayLogRedactor.RedactClientIdentity(ClientIdentity)); + + return values; + } +``` + +`ClientIdentity` is passed through `GatewayLogRedactor.RedactClientIdentity` inside `ToDictionary` rather than at the call site. This guarantees that any logger scope built from a `GatewayLogScope` cannot accidentally surface a raw API key, even when a caller forgets to redact before constructing the scope. + +### How scopes are pushed + +`GatewayLoggerExtensions` exposes a single method that converts a `GatewayLogScope` into the dictionary form expected by `ILogger.BeginScope`: + +```csharp +public static class GatewayLoggerExtensions +{ + public static IDisposable? BeginGatewayScope( + this ILogger logger, + GatewayLogScope scope) + { + ArgumentNullException.ThrowIfNull(logger); + ArgumentNullException.ThrowIfNull(scope); + + return logger.BeginScope(scope.ToDictionary()); + } +} +``` + +The returned `IDisposable?` follows the standard `BeginScope` contract: callers wrap it in a `using` to bound the scope to a request, command, or worker interaction. + +## Redaction Rules + +`GatewayLogRedactor` centralizes every redaction decision so that policy changes live in one file. Three categories of input are handled differently because each has different "safe to log" prefixes. + +### Sensitive command methods + +A static set names the MXAccess commands that are known to carry credentials in their payloads: + +```csharp +private static readonly HashSet SensitiveCommandMethods = new(StringComparer.OrdinalIgnoreCase) +{ + "AuthenticateUser", + "WriteSecured", + "WriteSecured2" +}; + +public static bool IsCredentialBearingCommand(string? commandMethod) +{ + return commandMethod is not null + && SensitiveCommandMethods.Contains(commandMethod); +} +``` + +The names match the MXAccess command list in `AGENTS.md` exactly. `Write` and `Write2` are not in the set because their payloads are tag values, not credentials, and are governed by the `valueLoggingEnabled` flag described below. + +### API key redaction + +`RedactApiKey` is built around the `mxgw_` API key format issued by the gateway. It preserves the bearer scheme and the key id segment so that operators can correlate a log entry to a specific principal, but always strips the secret tail: + +```csharp +public static string? RedactApiKey(string? authorizationHeader) +{ + if (string.IsNullOrWhiteSpace(authorizationHeader)) + { + return authorizationHeader; + } + + const string bearerPrefix = "Bearer "; + if (!authorizationHeader.StartsWith(bearerPrefix, StringComparison.OrdinalIgnoreCase)) + { + return RedactedValue; + } + + string token = authorizationHeader[bearerPrefix.Length..].Trim(); + + if (!token.StartsWith("mxgw_", StringComparison.OrdinalIgnoreCase)) + { + return $"{bearerPrefix}{RedactedValue}"; + } + + string[] tokenParts = token.Split('_', 3, StringSplitOptions.RemoveEmptyEntries); + if (tokenParts.Length < 2) + { + return $"{bearerPrefix}mxgw_{RedactedValue}"; + } + + return $"{bearerPrefix}mxgw_{tokenParts[1]}_{RedactedValue}"; +} +``` + +The split uses `count: 3` because the secret portion may itself contain underscores; only the first two segments (`mxgw` and the key id) are kept verbatim. Authorization headers that are not bearer tokens are reduced to `[redacted]` rather than passed through, since the gateway cannot reason about their structure. + +`RedactClientIdentity` is the entry point used by `GatewayLogScope` and `DashboardRedactor`. It only invokes `RedactApiKey` when the input contains the `mxgw_` marker, leaving non-key identities (for example, Windows account names) untouched. + +### Command value redaction + +`RedactCommandValue` enforces the "values are opt-in and redacted by default" rule: + +```csharp +public static object? RedactCommandValue( + string? commandMethod, + object? value, + bool valueLoggingEnabled = false) +{ + if (value is null) + { + return null; + } + + if (!valueLoggingEnabled || IsCredentialBearingCommand(commandMethod)) + { + return RedactedValue; + } + + return value; +} +``` + +Two rules combine here. First, when `valueLoggingEnabled` is `false` (the default), every value is replaced with `[redacted]`. Second, even when value logging is enabled, credential-bearing commands still redact. The credential check is therefore unconditional and cannot be overridden by configuration. + +The shared `RedactedValue` constant is `"[redacted]"`. `DashboardRedactor` reuses it so that gateway logs and dashboard renders use the same placeholder. + +## Request Logging Middleware + +`GatewayRequestLoggingMiddlewareExtensions.UseGatewayRequestLoggingScope` registers the middleware that pushes a `GatewayLogScope` for the duration of every request: + +```csharp +public static IApplicationBuilder UseGatewayRequestLoggingScope(this IApplicationBuilder app) +{ + ArgumentNullException.ThrowIfNull(app); + + return app.Use(async (context, next) => + { + ILogger logger = context.RequestServices + .GetRequiredService() + .CreateLogger("MxGateway.Request"); + + using IDisposable? scope = logger.BeginGatewayScope(new GatewayLogScope( + SessionId: ReadHeader(context, SessionIdHeaderName), + WorkerProcessId: ReadInt32Header(context, WorkerProcessIdHeaderName), + CorrelationId: ReadUInt64Header(context, CorrelationIdHeaderName), + CommandMethod: ReadHeader(context, CommandMethodHeaderName), + ClientIdentity: ReadHeader(context, "authorization"))); + + await next(context); + }); +} +``` + +The scope is keyed off four custom headers and the standard `authorization` header: + +| Header | Scope field | Type | +|--------|-------------|------| +| `x-session-id` | `SessionId` | string | +| `x-worker-process-id` | `WorkerProcessId` | int | +| `x-correlation-id` | `CorrelationId` | ulong | +| `x-command-method` | `CommandMethod` | string | +| `authorization` | `ClientIdentity` | string (redacted) | + +The numeric headers use `int.TryParse` and `ulong.TryParse`; missing or unparseable values become `null` and are dropped by `GatewayLogScope.ToDictionary`. This keeps the middleware tolerant of clients that do not yet emit every header, which matters because the earliest call in a session (`OpenSession`) has no `SessionId` to send. + +The logger category is `MxGateway.Request`, which lets operators filter the request scope events independently from per-component categories. + +### Pipeline ordering + +`GatewayApplication.Build` registers the middleware before authentication, authorization, and endpoint mapping: + +```csharp +app.UseGatewayRequestLoggingScope(); +app.UseStaticFiles(); +app.UseAuthentication(); +app.UseAuthorization(); +app.UseAntiforgery(); +app.MapGatewayEndpoints(); +``` + +The order matters: putting the logging scope first ensures that authentication failures, authorization denials, and endpoint exceptions all run inside the request scope, so failure logs still carry the correlation id and session id headers that the caller sent. The `ClientIdentity` field is redacted before logging, so reading the `authorization` header at this stage does not leak the bearer secret into authentication failure logs. + +## Consumers + +`GatewayLoggerExtensions.BeginGatewayScope` is consumed by `GatewayRequestLoggingMiddlewareExtensions` to attach the per-request scope. Component-level call sites build narrower `GatewayLogScope` instances (for example, with a known `WorkerProcessId` after a worker launch) and push a nested scope on top of the request scope. + +`GatewayLogRedactor` is consumed in three places: + +- `GatewayLogScope.ToDictionary` redacts `ClientIdentity` whenever a scope is materialized. +- `DashboardRedactor.Redact` delegates to `RedactClientIdentity` for any value containing the `mxgw_` marker, then falls back to a marker-keyword check for fields like `password` or `token`. This keeps dashboard renders aligned with log redaction. +- `MxGateway.Tests/Diagnostics/GatewayLogRedactorTests.cs` covers each redaction branch, including the assertion that `WriteSecured` values stay redacted even when `valueLoggingEnabled` is true. + +## Related Documentation + +- [Sessions](./Sessions.md) +- [gRPC](./Grpc.md) +- [Authentication](./Authentication.md) diff --git a/docs/GalaxyRepository.md b/docs/GalaxyRepository.md new file mode 100644 index 0000000..6141f2b --- /dev/null +++ b/docs/GalaxyRepository.md @@ -0,0 +1,271 @@ +# Galaxy Repository Browse + +The gateway exposes a read-only browse surface over the AVEVA System Platform +Galaxy Repository (the SQL Server database named `ZB`). Clients use it to +enumerate the deployed object hierarchy and each object's dynamic attributes +before subscribing to runtime values via the existing `MxAccessGateway` RPCs. + +This is a metadata layer: it never reads or writes runtime tag values, never +goes through MXAccess COM, and never needs the x86 worker. It runs entirely +inside the .NET 10 gateway process and talks to the Galaxy Repository over +plain SQL. + +## Why It Exists + +Without browse, a client must already know `tag_name.AttributeName` strings to +issue `AddItem`. The Galaxy Repository is the authoritative source for those +names — the same database that System Platform itself reads when the +ArchestrA IDE renders the deployment tree. Surfacing that data over gRPC lets +remote clients build a navigable address space without any coupling to the +COM layer or the host platform. + +The query bodies are kept byte-for-byte identical to the equivalent OPC UA +server in the OtOpcUa project so the two consumers see the same row sets. + +## RPC Surface + +The service is defined in +`src/MxGateway.Contracts/Protos/galaxy_repository.proto` under package +`galaxy_repository.v1`. + +| RPC | Purpose | +|-----|---------| +| `TestConnection` | Connectivity probe. Returns `{ ok: bool }` after a `SELECT 1`. Does not throw on SQL failure — returns `ok = false`. Always hits SQL directly so it remains a true health check. | +| `GetLastDeployTime` | Returns the cached `galaxy.time_of_last_deploy`. Served from the shared hierarchy cache; refreshed in the background. | +| `DiscoverHierarchy` | Returns the full deployed hierarchy plus every object's dynamic attributes. **Served from cache** — see [Hierarchy Cache](#hierarchy-cache). | +| `WatchDeployEvents` | **Server-streaming.** The server emits the current state immediately on subscribe (so clients can bootstrap without waiting), then emits one event per detected deploy change. See [Deploy Notifications](#deploy-notifications). | + +`DiscoverHierarchy` is intentionally a single unary RPC rather than a stream: +the row set is small (thousands of objects, low tens-of-thousands of +attributes for typical Galaxies) and clients almost always want the whole tree +at once. + +## Hierarchy Cache + +The gateway holds a single shared `IGalaxyHierarchyCache` +(`src/MxGateway.Server/Galaxy/GalaxyHierarchyCache.cs`) — every +`DiscoverHierarchy` and `GetLastDeployTime` request reads from this cache +rather than hitting SQL. Many clients can browse concurrently with at most +one SQL query in flight. + +Refresh strategy is **deploy-time gated**: + +1. The hosted `GalaxyHierarchyRefreshService` ticks every + `MxGateway:Galaxy:DashboardRefreshIntervalSeconds` seconds (default 30). +2. Each tick queries the cheap `SELECT time_of_last_deploy FROM galaxy` first. +3. If the deploy timestamp is unchanged, the heavy hierarchy + attributes + queries are **skipped**. The cache simply marks `LastSuccessAt`. +4. If the deploy timestamp changed (or no data has loaded yet), the cache + pulls hierarchy + attributes, materializes a `DiscoverHierarchyReply` + once, replaces the entry atomically, and publishes a deploy event. + +Materializing the reply at refresh time means subsequent `DiscoverHierarchy` +calls return a pre-built proto message — no per-request projection, no +per-request allocations beyond the gRPC serializer's frame. + +When SQL is unreachable, the cache retains the previous data and flips +`Status` to `Stale` (or `Unavailable` if no data was ever loaded). A +`SqlException` never bubbles out as the client-facing error. + +### First-load behavior + +If a client calls `DiscoverHierarchy` before the background service has +populated the cache, the gRPC handler waits up to 5 seconds for the first +load to complete before returning. If the first load fails or times out, +the client gets `Unavailable` with a short reason. Once any load completes +(success or failure), this wait is skipped on subsequent calls. + +## Deploy Notifications + +`WatchDeployEvents` is a server-streaming RPC backed by +`IGalaxyDeployNotifier` (`src/MxGateway.Server/Galaxy/GalaxyDeployNotifier.cs`). +The notifier maintains a private bounded channel per subscriber so a slow +client cannot back-pressure other subscribers or the publisher. + +Subscriber lifecycle: + +1. On subscribe, the notifier emits the **current** event (the state of the + most recent successful refresh) so the subscriber can sync its local cache + without waiting for the next deploy. Clients that already know about that + deploy can pass `last_seen_deploy_time` in the request to suppress the + bootstrap event. +2. As the cache observes new deploy timestamps, it publishes one event per + change. Each event carries: + - `sequence` — monotonic per server start; gaps signal a dropped event. + - `observed_at` — server wall-clock when the cache saw the deploy. + - `time_of_last_deploy` (+ `time_of_last_deploy_present`) — the Galaxy + timestamp; absent only when the source row reports null. + - `object_count`, `attribute_count` — counts on the new deploy, useful for + dashboards and "did anything important change" gates without re-pulling. +3. If the subscriber's per-subscriber buffer fills (bound = 16 events with + `DropOldest`), older events are dropped. Clients use the `sequence` field + to detect this. +4. Cancellation (or transport disconnect) removes the subscriber. + +Typical client pattern: + +```text +1. Open WatchDeployEvents stream (with last_seen_deploy_time if you have one). +2. On each event, decide whether to call DiscoverHierarchy to refresh local cache. +3. If sequence skipped a number, treat it as a dropped event and refresh. +``` + +### Reply Shape + +```proto +message GalaxyObject { + int32 gobject_id = 1; + string tag_name = 2; + string contained_name = 3; + string browse_name = 4; // contained_name when present, else tag_name + int32 parent_gobject_id = 5; + bool is_area = 6; + int32 category_id = 7; + int32 hosted_by_gobject_id = 8; + repeated string template_chain = 9; + repeated GalaxyAttribute attributes = 10; +} + +message GalaxyAttribute { + string attribute_name = 1; + string full_tag_reference = 2; // e.g. "DelmiaReceiver_001.DownloadPath" + int32 mx_data_type = 3; // raw Galaxy mx_data_type integer + string data_type_name = 4; + bool is_array = 5; + int32 array_dimension = 6; + bool array_dimension_present = 7; // distinguishes "no dimension" from 0 + int32 mx_attribute_category = 8; + int32 security_classification = 9; + bool is_historized = 10; + bool is_alarm = 11; +} +``` + +### Contained Name vs Tag Name + +Galaxy objects carry two names. `tag_name` is globally unique and is what +MXAccess expects in `AddItem`. `contained_name` is the human-readable name +used in the IDE browse tree, scoped to the parent. The browse RPC exposes +both: clients display `browse_name` to users and pass `tag_name` (or +`full_tag_reference`) into MXAccess subscriptions. When `contained_name` is +empty (top-level objects), `browse_name` falls back to `tag_name`. + +### Data Types + +`mx_data_type` is returned as the raw Galaxy integer rather than mapped to a +language-neutral enum. The gateway makes no assumption about the client's +target type system — clients map to OPC UA, JSON, .NET CLR types, or +something else as appropriate. The Galaxy `data_type` table description is +also passed through as `data_type_name`. + +`array_dimension_present` is a separate boolean because protobuf scalar +fields cannot express null. Use it to distinguish "no dimension reported" from +"dimension is zero." + +## Architecture + +```text +gRPC client(s) + -> GalaxyRepositoryGrpcService (src/MxGateway.Server/Grpc/) + DiscoverHierarchy, GetLastDeployTime -> IGalaxyHierarchyCache.Current + WatchDeployEvents -> IGalaxyDeployNotifier + TestConnection -> GalaxyRepository (direct SQL) + +GalaxyHierarchyRefreshService (BackgroundService) + -> IGalaxyHierarchyCache.RefreshAsync + -> GalaxyRepository.GetLastDeployTimeAsync (cheap, every tick) + -> GalaxyRepository.GetHierarchyAsync (only on deploy change) + -> GalaxyRepository.GetAttributesAsync (only on deploy change) + -> GalaxyProtoMapper.MapObject (materialize DiscoverHierarchyReply once) + -> IGalaxyDeployNotifier.Publish (only on deploy change) +``` + +Component breakdown: + +- `GalaxyRepository` (`src/MxGateway.Server/Galaxy/GalaxyRepository.cs`) holds + the SQL. Its constants `HierarchySql` and `AttributesSql` are copied verbatim + from the OtOpcUa project; do not edit them in isolation here. The two + queries walk template-derivation and package-derivation chains via + recursive CTEs and pick the most-derived attribute override per object. +- `GalaxyHierarchyCache` + (`src/MxGateway.Server/Galaxy/GalaxyHierarchyCache.cs`) holds the most + recent immutable `GalaxyHierarchyCacheEntry` (rows + materialized proto + reply + counts + status). All gRPC clients share the same entry. +- `GalaxyHierarchyRefreshService` + (`src/MxGateway.Server/Galaxy/GalaxyHierarchyRefreshService.cs`) is a + hosted `BackgroundService` that drives `RefreshAsync` on the configured + interval, with deploy-time gating to avoid unnecessary heavy queries. +- `GalaxyDeployNotifier` + (`src/MxGateway.Server/Galaxy/GalaxyDeployNotifier.cs`) is a thin + per-subscriber-channel fan-out for streaming clients. +- `GalaxyProtoMapper` + (`src/MxGateway.Server/Grpc/GalaxyProtoMapper.cs`) converts row models to + proto messages. Used by the cache during refresh to materialize the reply + once. +- `GalaxyRepositoryGrpcService` + (`src/MxGateway.Server/Grpc/GalaxyRepositoryGrpcService.cs`) implements + the four RPCs. + +## Configuration + +Bound to `MxGateway:Galaxy` via `GalaxyRepositoryOptions`. + +| Option | Default | Description | +|--------|---------|-------------| +| `MxGateway:Galaxy:ConnectionString` | `Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;` | SQL Server connection string for the Galaxy Repository. Integrated Security against `localhost` is the dev default; production deployments should override this through the standard double-underscore environment variable form, e.g. `MxGateway__Galaxy__ConnectionString`. | +| `MxGateway:Galaxy:CommandTimeoutSeconds` | `60` | Per-command SQL timeout. Applies to all three RPCs. | + +The connection string is not treated as a secret in dev (`Integrated +Security`), but production deployments that use SQL authentication should set +the override via environment variable rather than committing credentials to +`appsettings.json`. + +## Authorization + +All four Galaxy RPCs (including `WatchDeployEvents`) require the +`metadata:read` API-key scope. Browse is read-only metadata, equivalent in +privilege to `MxCommandKind.GetSessionState` or `MxCommandKind.GetWorkerInfo`. +The mapping lives in `GatewayGrpcScopeResolver`; see +[Authorization](./Authorization.md) for the full scope catalog. + +A request without an API key returns `Unauthenticated`. A request with a key +that lacks `metadata:read` returns `PermissionDenied` with the missing scope +embedded in the status detail. + +## Dashboard Surface + +The gateway's Blazor dashboard surfaces a Galaxy summary in two places: + +- An overview card on `/dashboard` showing connectivity status, last deploy + timestamp, object count (with area count), attribute total, historized and + alarm counts, and last successful refresh. +- A dedicated `/dashboard/galaxy` page with object-category and top-template + breakdowns plus a Sync Info table covering last successful refresh, last + attempt, refresh interval, redacted connection string, and command timeout. + +Both views are projected from the same `IGalaxyHierarchyCache` that backs the +gRPC service. The dashboard does not run its own refresh — when the +background `GalaxyHierarchyRefreshService` updates the cache, both the +overview card and the `/dashboard/galaxy` page pick up the new state on the +next dashboard tick. When SQL is unreachable, the cache retains the previous +data and flips `Status` to `Stale` or `Unavailable`; the dashboard surfaces +that as a yellow or red status badge plus the truncated error. + +## Operational Notes + +- The service is registered alongside `MxAccessGatewayService` in + `GatewayApplication.MapGatewayEndpoints`. Both services share the same + authorization interceptor and authentication policy. +- Failures to reach the Galaxy database surface as `Unavailable`. Detailed + SQL exceptions are logged at `Warning` and never returned to clients. +- Integration tests live in + `src/MxGateway.IntegrationTests/Galaxy/GalaxyRepositoryLiveTests.cs`. Set + `MXGATEWAY_RUN_LIVE_GALAXY_TESTS=1` (and optionally + `MXGATEWAY_LIVE_GALAXY_CONN`) to run them; otherwise they skip. + +## Related Documentation + +- [Contracts](./Contracts.md) +- [Grpc](./Grpc.md) +- [Authorization](./Authorization.md) +- [Gateway Configuration](./GatewayConfiguration.md) diff --git a/docs/GatewayConfiguration.md b/docs/GatewayConfiguration.md index 478cbc1..d57bac5 100644 --- a/docs/GatewayConfiguration.md +++ b/docs/GatewayConfiguration.md @@ -53,6 +53,11 @@ paths, timeouts, queue sizes, enum values, or protocol values are invalid. }, "Protocol": { "WorkerProtocolVersion": 1 + }, + "Galaxy": { + "ConnectionString": "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;", + "CommandTimeoutSeconds": 60, + "DashboardRefreshIntervalSeconds": 30 } } } @@ -146,9 +151,21 @@ The protocol option is exposed for diagnostics and explicit deployment configuration, not for compatibility negotiation. A mismatch fails validation at startup. +## Galaxy Options + +| Option | Default | Description | +|--------|---------|-------------| +| `MxGateway:Galaxy:ConnectionString` | `Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;` | SQL Server connection string for the Galaxy Repository (`ZB`) used by the `GalaxyRepository` browse RPCs. Override in production via `MxGateway__Galaxy__ConnectionString`. | +| `MxGateway:Galaxy:CommandTimeoutSeconds` | `60` | Per-command SQL timeout for all Galaxy browse RPCs. | +| `MxGateway:Galaxy:DashboardRefreshIntervalSeconds` | `30` | Interval between background refreshes of the dashboard Galaxy summary cache. SQL is hit at most once per interval regardless of dashboard render rate. | + +See [Galaxy Repository Browse](./GalaxyRepository.md) for the RPC surface and +behavior. + ## Related Documentation - [Gateway Process Detailed Design](./gateway-process-design.md) - [Gateway Dashboard Detailed Design](./gateway-dashboard-design.md) - [Worker Process Launcher](./WorkerProcessLauncher.md) - [Worker Frame Protocol](./WorkerFrameProtocol.md) +- [Galaxy Repository Browse](./GalaxyRepository.md) diff --git a/docs/Grpc.md b/docs/Grpc.md new file mode 100644 index 0000000..e72f45e --- /dev/null +++ b/docs/Grpc.md @@ -0,0 +1,236 @@ +# Gateway gRPC Service Layer + +The gRPC service layer is the public entry point for client traffic. It is intentionally thin: handlers validate the incoming request, look up or open a session, dispatch to the worker through the session manager, and translate worker replies and events back into public proto types. + +## Layer Responsibilities + +The architecture rule (from `AGENTS.md`) is that the gRPC layer must "validate request, find session, call the session worker client, map worker replies to public replies, and stream events". Anything else — caching, retries, worker process lifetime, event ordering — lives behind `ISessionManager` and the worker client. Keeping the layer thin lets the same session/worker code be reused by future transports (for example, an in-process host or an alternate IPC) without having to re-derive validation or mapping rules. + +The layer is composed of four collaborators: + +| Type | Lifetime | Role | +|------|----------|------| +| `MxAccessGatewayService` | scoped (gRPC) | Implements the four `MxAccessGateway` RPCs, performs exception mapping. | +| `MxAccessGrpcRequestValidator` | singleton | Rejects malformed requests before any session work runs. | +| `MxAccessGrpcMapper` | singleton | Converts public proto types to internal `WorkerCommand`/`WorkerEvent` types and back. | +| `IEventStreamService` (`EventStreamService`) | singleton | Owns the event stream pipeline, including bounded queue and backpressure handling. | + +Registration happens in `GatewayApplication`: + +```csharp +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +``` + +The service itself is mapped as a normal gRPC endpoint via `endpoints.MapGrpcService()`. + +A second gRPC service, `GalaxyRepositoryGrpcService`, is mapped alongside it. It exposes the read-only Galaxy Repository browse surface and is documented separately in [Galaxy Repository Browse](./GalaxyRepository.md). It shares the authorization interceptor and authentication policy used by `MxAccessGatewayService`, but it does not go through the session manager or worker — it talks to SQL Server directly. + +## RPC Handlers + +`MxAccessGatewayService` derives from the generated `MxAccessGateway.MxAccessGatewayBase` and implements every RPC declared in `mxaccess_gateway.proto`. The proto contract itself is documented in [Contracts](./Contracts.md); this section covers only what the server-side handler does on top of that contract. + +### `OpenSession` + +`OpenSession` validates the request, asks `ISessionManager` to open a session under the caller's identity, and returns a reply that advertises both protocol versions and the capabilities the gateway supports. Capability strings are static because the gateway has a fixed feature set per build; clients use them as a forward-compatibility hint rather than runtime negotiation. + +```csharp +GatewaySession session = await sessionManager + .OpenSessionAsync( + SessionOpenRequest.FromContract(request), + ResolveClientIdentity(), + context.CancellationToken) + .ConfigureAwait(false); + +OpenSessionReply reply = new() +{ + SessionId = session.SessionId, + BackendName = session.BackendName, + WorkerProcessId = session.WorkerProcessId ?? 0, + WorkerProtocolVersion = GatewayContractInfo.WorkerProtocolVersion, + GatewayProtocolVersion = GatewayContractInfo.GatewayProtocolVersion, + DefaultCommandTimeout = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan(session.CommandTimeout), + ProtocolStatus = MxAccessGrpcMapper.Ok(), +}; +``` + +`ResolveClientIdentity()` reads `IGatewayRequestIdentityAccessor.Current` and prefers `DisplayName`, falling back to `KeyId`. The accessor is populated by the authorization interceptor before the handler runs (see [Authorization](./Authorization.md)). + +### `CloseSession` + +The handler delegates to `sessionManager.CloseSessionAsync` and converts the resulting `SessionCloseResult` into a `CloseSessionReply`. The handler distinguishes the two outcomes via the protocol status message — `"Session was already closed."` versus `"Session closed."` — so callers do not need to reason about idempotency from a status code alone. + +### `Invoke` + +`Invoke` is the unary command path. It runs the validator (which enforces payload-vs-kind matching), uses the mapper to wrap the public `MxCommand` in a `WorkerCommand` with an enqueue timestamp, calls `sessionManager.InvokeAsync`, and unwraps the worker reply. + +```csharp +requestValidator.ValidateInvoke(request); +WorkerCommand workerCommand = mapper.MapCommand(request); +WorkerCommandReply workerReply = await sessionManager + .InvokeAsync(request.SessionId, workerCommand, context.CancellationToken) + .ConfigureAwait(false); + +return mapper.MapCommandReply(workerReply); +``` + +Carrying the enqueue timestamp into the worker layer is what lets queue-wait time be measured separately from worker-side execution time when troubleshooting timeouts. + +### `StreamEvents` + +`StreamEvents` is a server-streaming RPC. The handler delegates the full pipeline to `IEventStreamService` and just forwards each `MxEvent` onto the response stream. Keeping the channel and producer/consumer machinery out of the handler means cancellation, exception mapping, and metric bookkeeping live in one place. + +## Validation Rules + +`MxAccessGrpcRequestValidator` rejects requests with `StatusCode.InvalidArgument` before any session work happens. The rules are intentionally narrow — anything that requires session state (for example, "session does not exist") is left for `ISessionManager` so the validator can stay synchronous and side-effect free. + +| RPC | Rule | Status | +|-----|------|--------| +| `OpenSession` | `command_timeout`, when set, must be `> 0`. | `InvalidArgument` | +| `CloseSession` | `session_id` must be non-empty. | `InvalidArgument` | +| `StreamEvents` | `session_id` must be non-empty. | `InvalidArgument` | +| `Invoke` | `session_id` non-empty, `command` present, `kind` not `Unspecified`, payload oneof must match `kind`. | `InvalidArgument` | + +The payload-vs-kind check matters because the `MxCommand.payload` oneof is non-discriminated on the wire — a misaligned client could send `kind = Write` with a `Register` payload and silently confuse the worker. The validator turns that into a clear client error: + +```csharp +private static void ValidateCommandPayload(MxCommand command) +{ + MxCommand.PayloadOneofCase expectedPayload = ExpectedPayload(command.Kind); + if (command.PayloadCase != expectedPayload) + { + throw InvalidArgument( + $"Command kind {command.Kind} requires payload {expectedPayload} but received {command.PayloadCase}."); + } +} +``` + +`ExpectedPayload` enumerates every `MxCommandKind` that has a matching payload case; unknown kinds map to `PayloadOneofCase.None`, which forces a mismatch and therefore a rejection. + +## Mapping Rules + +`MxAccessGrpcMapper` is the only place that translates between public proto types and internal worker types. Two design choices are worth calling out. + +The mapper clones the inbound `MxCommand` rather than reusing the reference. This isolates the worker pipeline from any later mutation of the request graph by the gRPC framework or interceptors: + +```csharp +public WorkerCommand MapCommand(MxCommandRequest request) +{ + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(request.Command); + + return new WorkerCommand + { + Command = request.Command.Clone(), + EnqueueTimestamp = Timestamp.FromDateTimeOffset(_timeProvider.GetUtcNow()), + }; +} +``` + +When the worker reply or event payload is missing, the mapper returns a synthetic public message with `ProtocolStatusCode.ProtocolViolation` (for replies) or a sentinel `MxEvent` with `MxEventFamily.Unspecified` (for events). The gateway never relays a partial frame to clients — anything missing is reported as a protocol violation against the worker, not a transport error against the client. + +The mapper also exposes static factory methods for every `ProtocolStatusCode` (`Ok`, `InvalidRequest`, `SessionNotFound`, `SessionNotReady`, `WorkerUnavailable`, `Timeout`, `Canceled`, `ProtocolViolation`) so that handlers and tests can produce status payloads without duplicating the enum-to-string mapping. + +## Exception to Status Mapping + +Every handler wraps its body in `try { ... } catch (Exception exception) when (exception is not RpcException) { throw MapException(exception); }`. `RpcException` is allowed to propagate untouched (the validator already produces them with the right code). All other exceptions are translated by `MapException`, which knows three categories: + +- `OperationCanceledException` becomes `StatusCode.Cancelled`. +- `SessionManagerException` is mapped by its `ErrorCode`. +- `WorkerClientException` is mapped by its `ErrorCode`. + +Anything else is logged at warning and surfaced as `Unavailable` with a generic message — clients see a retryable status, and the unexpected exception is captured in gateway logs rather than leaked over the wire. + +```csharp +StatusCode statusCode = exception.ErrorCode switch +{ + SessionManagerErrorCode.SessionNotFound => StatusCode.NotFound, + SessionManagerErrorCode.SessionNotReady => StatusCode.FailedPrecondition, + SessionManagerErrorCode.EventSubscriberAlreadyActive => StatusCode.ResourceExhausted, + SessionManagerErrorCode.EventQueueOverflow => StatusCode.ResourceExhausted, + SessionManagerErrorCode.SessionLimitExceeded => StatusCode.ResourceExhausted, + SessionManagerErrorCode.OpenFailed => StatusCode.Unavailable, + SessionManagerErrorCode.CloseFailed => StatusCode.Unavailable, + _ => StatusCode.Unavailable, +}; +``` + +`WorkerClientException` follows the same pattern: `CommandTimeout` becomes `DeadlineExceeded`, `GatewayShutdown` becomes `Cancelled`, `InvalidState` becomes `FailedPrecondition`, `ProtocolViolation` becomes `Internal`, and unmapped codes fall through to `Unavailable`. + +## Event Streaming Model + +`EventStreamService` implements the `StreamEvents` pipeline. It exists as a separate service (rather than living inside `MxAccessGatewayService`) so that the channel, backpressure policy, and metric bookkeeping can be unit-tested without spinning up a Kestrel host. + +The pipeline has three stages: + +1. The session is resolved via `sessionManager.TryGetSession`. A miss raises `SessionManagerException(SessionNotFound)` so the handler reports `StatusCode.NotFound`. +2. `session.AttachEventSubscriber(...)` enforces the single-subscriber-per-session rule (or allows multiple subscribers if `Sessions:AllowMultipleEventSubscribers` is enabled). The returned `IDisposable` is released in the `finally` block, ensuring the subscriber slot is freed even when the client cancels mid-stream. +3. A `Channel` decouples the worker-side producer from the gRPC writer. The channel is bounded by `Events:QueueCapacity` and configured for a single reader and writer: + +```csharp +Channel eventQueue = Channel.CreateBounded( + new BoundedChannelOptions(options.Value.Events.QueueCapacity) + { + SingleReader = true, + SingleWriter = true, + FullMode = BoundedChannelFullMode.Wait, + AllowSynchronousContinuations = false, + }); +``` + +The producer reads `WorkerEvent`s from the session, maps them, and writes to the channel. Per-session ordering is preserved end-to-end: events arrive from the worker in `WorkerSequence` order, the producer is the single writer, and the consumer drains FIFO. The producer also honours the request's `AfterWorkerSequence` cursor by skipping any event whose `WorkerSequence` is at or before the requested cutoff, which lets clients resume after a disconnect without server-side replay state. + +### Backpressure + +When `TryWrite` fails the queue is full. The handling depends on `Events:BackpressurePolicy`: + +```csharp +if (!writer.TryWrite(publicEvent)) +{ + string message = $"Session {session.SessionId} event stream queue overflowed."; + metrics.QueueOverflow("grpc-event-stream"); + if (options.Value.Events.BackpressurePolicy == EventBackpressurePolicy.FailFast) + { + session.MarkFaulted(message); + metrics.Fault(SessionManagerErrorCode.EventQueueOverflow.ToString()); + } + else + { + logger.LogDebug( + "Disconnecting event stream for session {SessionId} after queue overflow.", + session.SessionId); + } + + writer.TryComplete(new SessionManagerException( + SessionManagerErrorCode.EventQueueOverflow, + message)); + return; +} +``` + +Under `FailFast` the session is faulted so subsequent commands return `FailedPrecondition`; the client must reopen. Under the default policy only the stream is dropped and the session continues to accept commands, leaving recovery to the client (typically a fresh `StreamEvents` call with an updated `AfterWorkerSequence`). Either way, the consumer side observes `StatusCode.ResourceExhausted` via the `EventQueueOverflow` mapping above. + +### Cancellation and Cleanup + +The handler creates a linked cancellation token (`streamCts`) so that completing the consumer (client disconnect, error, or graceful end-of-stream) also cancels the producer. The `finally` block cancels the source, disposes the subscriber slot, awaits the producer (swallowing the expected cancellation), and emits `StreamDisconnected("Detached")` so dashboards see the disconnection regardless of cause. + +`WorkerClientException` thrown by the producer marks the session as faulted before completing the channel — the worker is presumed gone, and any subsequent command on that session must observe the fault rather than silently retry. + +## Authorization Interceptor Integration + +Authorization is applied as a gRPC interceptor, registered in `GrpcAuthorizationServiceCollectionExtensions`: + +```csharp +services.AddSingleton(); +services.AddGrpc(options => options.Interceptors.Add()); +``` + +Because the interceptor runs before any handler, `MxAccessGatewayService` can safely assume the call has been authorized and that `IGatewayRequestIdentityAccessor.Current` is populated. The handler's only responsibility is to read the identity for `OpenSession` so the session is owned by the authenticated principal; it does not perform any authorization checks of its own. See [Authorization](./Authorization.md) for the policy and identity model. + +## Related Documentation + +- [Contracts](./Contracts.md) +- [Sessions](./Sessions.md) +- [Authorization](./Authorization.md) +- [Gateway Process Design](./gateway-process-design.md) diff --git a/docs/Metrics.md b/docs/Metrics.md new file mode 100644 index 0000000..91fe6e1 --- /dev/null +++ b/docs/Metrics.md @@ -0,0 +1,210 @@ +# Gateway Metrics + +The metrics subsystem exposes counters, histograms, and observable gauges that describe gateway throughput, queue health, and worker lifecycle. Both the `System.Diagnostics.Metrics` pipeline and the in-memory `GatewayMetricsSnapshot` consume the same underlying state, so external collectors and the dashboard see consistent numbers. + +## Overview + +`GatewayMetrics` is a singleton (registered in `GatewayApplication.cs`) that owns a single `Meter` named `MxGateway.Server` and a set of synchronised counters, histograms, and observable gauges. Subsystems call typed mutator methods (`SessionOpened`, `CommandFailed`, `EventReceived`, etc.) rather than touching the `Meter` directly, which keeps the OpenTelemetry instrument names and tag conventions in one place. A `lock (_syncRoot)` block guards the scalar fields used by `GetSnapshot`, while per-event maps use `ConcurrentDictionary` so the hot event path avoids the lock. + +## Meter and OpenTelemetry Compatibility + +The meter name is exposed as a constant so that hosting code can register it with an OpenTelemetry exporter: + +```csharp +public sealed class GatewayMetrics : IDisposable +{ + public const string MeterName = "MxGateway.Server"; + + public GatewayMetrics() + { + _meter = new Meter(MeterName, typeof(GatewayMetrics).Assembly.GetName().Version?.ToString()); + _sessionsOpenedCounter = _meter.CreateCounter("mxgateway.sessions.opened"); + ... + } +} +``` + +The meter version is the gateway assembly version, which gives exporters a stable identifier per build. All instrument names use the dotted `mxgateway..` convention so they group cleanly under a single namespace in tools such as Prometheus, OTLP collectors, or `dotnet-counters`. + +## Instrument Inventory + +### Counters + +All counters are `Counter`. Tag values come from the call sites listed under [Recording Sites](#recording-sites). + +| Instrument | Tags | What it measures | +|------------|------|------------------| +| `mxgateway.sessions.opened` | none | Successful `SessionManager.OpenSession` completions. | +| `mxgateway.sessions.closed` | none | Sessions closed cleanly via `SessionManager`. | +| `mxgateway.commands.started` | `method` | Command dispatches initiated by `WorkerClient`. | +| `mxgateway.commands.succeeded` | `method` | Commands acknowledged with success by the worker. | +| `mxgateway.commands.failed` | `method`, `category` | Command failures, where `category` is the `WorkerClientErrorCode` or exception type name. | +| `mxgateway.events.received` | `family` | Worker events accepted into the event pipeline. | +| `mxgateway.queues.overflows` | `queue` | Drops when a bounded queue rejects a message (e.g. `grpc-event-stream`). | +| `mxgateway.faults` | `category` | Faults reported by session, event, or worker code paths. The category is a `SessionManagerErrorCode` or `WorkerClientErrorCode` name. | +| `mxgateway.workers.killed` | `reason` | Forced terminations of worker processes. | +| `mxgateway.workers.exited` | `reason` | Clean or fault-driven worker exits. | +| `mxgateway.heartbeats.failed` | `session_id` | Worker heartbeat misses tracked per session. | +| `mxgateway.grpc.streams.disconnected` | `reason` | Detachments of the dashboard or client gRPC event stream. | +| `mxgateway.retries.attempted` | `area` | Resilience retries executed by gateway components. | + +### Histograms + +Histograms record durations in milliseconds (the `unit` argument on `CreateHistogram`): + +```csharp +_workerStartupLatencyHistogram = _meter.CreateHistogram("mxgateway.workers.startup.duration", "ms"); +_commandLatencyHistogram = _meter.CreateHistogram("mxgateway.commands.duration", "ms"); +_eventStreamSendLatencyHistogram = _meter.CreateHistogram("mxgateway.events.stream_send.duration", "ms"); +``` + +| Instrument | Tags | What it measures | +|------------|------|------------------| +| `mxgateway.workers.startup.duration` | none | Time from `WorkerClient` launch to worker-ready. | +| `mxgateway.commands.duration` | `method`, optional `category` | Command round-trip time. The `category` tag is added on failure so success and failure latencies stay distinguishable. | +| `mxgateway.events.stream_send.duration` | `family` | Time spent writing each public event to the gRPC response stream in `MxAccessGatewayService.StreamEvents`. | + +### Observable Gauges + +Observable gauges are pull-based; the `Meter` invokes the supplied callback whenever a listener samples it. Each callback re-acquires `_syncRoot` so the gauge value matches the snapshot taken at the same instant. + +| Instrument | Source field | Description | +|------------|--------------|-------------| +| `mxgateway.sessions.open` | `_openSessions` | Currently open sessions tracked by `SessionManager`. | +| `mxgateway.workers.running` | `_workersRunning` | Worker clients in a running state. | +| `mxgateway.events.worker_queue.depth` | `_workerEventQueueDepth` | Last reported depth of the worker-side event queue. | +| `mxgateway.events.grpc_stream_queue.depth` | `_grpcEventStreamQueueDepth` | Backlog held by `EventStreamService` for the active gRPC stream consumer. | + +## Snapshot Shape + +`GatewayMetricsSnapshot` is the immutable view of the same state, returned by `GatewayMetrics.GetSnapshot()` while holding `_syncRoot`. The dictionaries are copied so the caller can iterate without further synchronisation. The dashboard service is the primary consumer. + +```csharp +public sealed record GatewayMetricsSnapshot( + int OpenSessions, + int WorkersRunning, + int WorkerEventQueueDepth, + int GrpcEventStreamQueueDepth, + long SessionsOpened, + long SessionsClosed, + long CommandsStarted, + long CommandsSucceeded, + long CommandsFailed, + long EventsReceived, + long QueueOverflows, + long Faults, + long WorkerKills, + long WorkerExits, + long HeartbeatFailures, + long StreamDisconnects, + long RetryAttempts, + IReadOnlyDictionary CommandFailuresByMethod, + IReadOnlyDictionary EventsByFamily, + IReadOnlyDictionary EventsBySession, + IReadOnlyDictionary RetryAttemptsByArea); +``` + +The scalar fields mirror the counters and gauges. The four dictionaries provide the breakdowns that counter tags would otherwise require an exporter to aggregate: + +- `CommandFailuresByMethod` keys by gRPC method name. +- `EventsByFamily` keys by event family (the `Family` enum on a worker event). +- `EventsBySession` keys by `sessionId`; entries are removed via `RemoveSessionEvents` when a session closes so the map does not grow without bound. +- `RetryAttemptsByArea` keys by the resilience `area` tag, e.g. `worker_startup`. + +`EventsReceived` is read with `Interlocked.Read(ref _eventsReceived)` because `EventReceived` increments it via `Interlocked.Increment` outside the lock to keep the event-ingestion path non-blocking. + +## Recording Sites + +The recording call sites describe the code paths that write into each instrument. This mapping makes it easier to trace an unexpected counter reading back to a subsystem. + +### Session manager + +`Sessions/SessionManager.cs` emits session lifecycle and fault counters: + +```csharp +_metrics.SessionOpened(); +... +_metrics.Fault(SessionManagerErrorCode.OpenFailed.ToString()); +... +_metrics.SessionClosed(); +... +_metrics.SessionRemoved(); +... +_metrics.Fault(SessionManagerErrorCode.CloseFailed.ToString()); +... +_metrics.RemoveSessionEvents(session.SessionId); +``` + +`SessionRemoved` decrements the open-session gauge without incrementing the closed counter, which covers cases where a session is evicted rather than closed by the client. + +### Worker client + +`Workers/WorkerClient.cs` records command throughput, worker lifecycle, heartbeat failures, and the worker-side event queue depth: + +- `CommandStarted(method)` and `CommandSucceeded(method, duration)` / `CommandFailed(method, category, duration)` around the worker request/response pair. +- `WorkerStarted(startupDuration)` once the worker reports ready. +- `RecordWorkerStoppedOnce` calls `WorkerStopped(reason)` exactly once per worker, guarding against double-counting on simultaneous fault and exit signals. +- `WorkerKilled(reason)` when the client forcibly terminates the worker. +- `HeartbeatFailed(SessionId)` per missed heartbeat. +- `SetWorkerEventQueueDepth(queueDepth)` after each event ingest. +- `EventReceived(SessionId, workerEvent.Event.Family.ToString())` for each worker event. + +### Worker process launcher + +`Workers/WorkerProcessLauncher.cs` records process-level kills and startup retries: + +```csharp +_metrics.WorkerKilled(reason); +... +_metrics.RetryAttempted("worker_startup"); +``` + +The `worker_startup` tag is hard-coded so the `RetryAttemptsByArea` snapshot reports launcher retries distinctly from other resilience areas. + +### Session worker client factory + +`Sessions/SessionWorkerClientFactory.cs` records the worker kill that follows a failed `OpenSession` handshake: + +```csharp +_metrics.WorkerKilled("OpenSessionFailed"); +``` + +This is the only fault path where the factory itself owns the kill decision; once the worker is bound to a session, the `WorkerClient` becomes responsible for lifecycle metrics. + +### gRPC event stream service + +`Grpc/EventStreamService.cs` records the dashboard/client event-stream backlog and disconnect counters: + +```csharp +metrics.AdjustGrpcEventStreamQueueDepth(1); +... +metrics.AdjustGrpcEventStreamQueueDepth(-1); +... +metrics.AdjustGrpcEventStreamQueueDepth(-remainingDepth); +metrics.StreamDisconnected("Detached"); +... +metrics.QueueOverflow("grpc-event-stream"); +metrics.Fault(SessionManagerErrorCode.EventQueueOverflow.ToString()); +... +metrics.Fault(WorkerClientErrorCode.WorkerFaulted.ToString()); +``` + +The service tracks per-message enqueues and dequeues, so `AdjustGrpcEventStreamQueueDepth` updates the aggregate stream backlog. The `Math.Max(0, ...)` clamp inside the adjuster prevents a negative depth if the bookkeeping ever drifts. + +`Grpc/MxAccessGatewayService.cs` records gRPC event send latency around each response-stream write: + +```csharp +Stopwatch stopwatch = Stopwatch.StartNew(); +await responseStream.WriteAsync(publicEvent).ConfigureAwait(false); +metrics.RecordEventStreamSend(publicEvent.Family.ToString(), stopwatch.Elapsed); +``` + +## Dashboard Consumption + +`Dashboard/DashboardSnapshotService.cs` calls `_metrics.GetSnapshot()` once per `GetSnapshot` invocation and projects it into the dashboard transport types together with the session registry view. The dashboard receives a single, internally consistent snapshot per tick rather than reading individual counters at separate times. See [Gateway Dashboard Design](./gateway-dashboard-design.md) and [Dashboard Interface Design](./DashboardInterfaceDesign.md) for the projection rules and wire format. + +## Related Documentation + +- [Gateway Dashboard Design](./gateway-dashboard-design.md) +- [Dashboard Interface Design](./DashboardInterfaceDesign.md) +- [Sessions](./Sessions.md) diff --git a/docs/Sessions.md b/docs/Sessions.md new file mode 100644 index 0000000..b4cbf9d --- /dev/null +++ b/docs/Sessions.md @@ -0,0 +1,271 @@ +# Gateway Sessions + +The sessions subsystem owns the in-memory representation of an active gateway-to-worker pairing and coordinates its lifecycle from open through close. Each `GatewaySession` corresponds to exactly one MXAccess worker process connected over a dedicated named pipe. + +## Overview + +A session is the gateway-side handle that callers use to invoke worker commands, stream worker events, and tear the worker down. The subsystem is split between the per-session state machine (`GatewaySession`), an in-memory directory (`SessionRegistry`), the orchestrator that opens and closes sessions (`SessionManager`), the worker construction step (`SessionWorkerClientFactory`), and a hosted service that drains sessions during host shutdown (`SessionShutdownHostedService`). + +All four interfaces (`ISessionManager`, `ISessionRegistry`, `ISessionWorkerClientFactory`) plus `SessionShutdownHostedService` are wired as singletons by `SessionServiceCollectionExtensions.AddGatewaySessions`. + +## Key Types + +### GatewaySession + +`GatewaySession` is a sealed class that holds the identity, configured timeouts, worker client reference, and current `SessionState` for one session. State is protected by a private `_syncRoot` lock so that property reads and transitions are observed atomically by concurrent gRPC calls and the lease sweeper. + +The session id is an opaque string in the form `session-{guid:N}` and the per-session pipe name is `mxaccess-gateway-{ProcessId}-{SessionId}`. Encoding the gateway PID into the pipe name avoids collisions when an old gateway process leaks pipes that the OS has not yet reclaimed. + +`SessionState` itself is the protobuf-generated enum from `MxGateway.Contracts.Proto`, so it is shared between the gateway and clients on the wire. + +```csharp +public void TransitionTo(SessionState nextState) +{ + lock (_syncRoot) + { + if (_state is SessionState.Closed) + { + return; + } + + if (_state is SessionState.Faulted && nextState is not SessionState.Closed) + { + return; + } + + _state = nextState; + } +} +``` + +`Closed` is terminal and `Faulted` only allows a transition to `Closed`. This guards against late callbacks (worker exit, heartbeat timeout) re-animating a session that is already torn down. + +### SessionManager (ISessionManager) + +`SessionManager` is the orchestrator. It exposes `OpenSessionAsync`, `TryGetSession`, `InvokeAsync`, `ReadEventsAsync`, `CloseSessionAsync`, `CloseExpiredLeasesAsync`, and `ShutdownAsync`. It composes `ISessionRegistry`, `ISessionWorkerClientFactory`, `GatewayMetrics`, and `GatewayOptions`. + +Concurrency is bounded by a `SemaphoreSlim` initialized to `GatewayOptions.Sessions.MaxSessions`. Open requests that exceed the bound throw `SessionManagerException` with `SessionLimitExceeded` rather than queuing; the caller is expected to retry. + +```csharp +private void EnsureSessionCapacity() +{ + if (!_sessionSlots.Wait(0)) + { + throw new SessionManagerException( + SessionManagerErrorCode.SessionLimitExceeded, + $"Gateway session limit {_options.Sessions.MaxSessions} has been reached."); + } +} +``` + +`SessionManager` also defines three close-reason constants — `DefaultCloseReason` (`"client-close"`), `GatewayShutdownReason` (`"gateway-shutdown"`), and `LeaseExpiredReason` (`"lease-expired"`) — so that the metrics and worker shutdown paths agree on a fixed vocabulary. + +### SessionRegistry (ISessionRegistry) + +`SessionRegistry` is a thin wrapper over a `ConcurrentDictionary` keyed by session id with `StringComparer.Ordinal`. `Snapshot` materializes the values into an array so iteration callers (lease sweeper, shutdown) do not race with concurrent `TryAdd` and `TryRemove` calls. + +`ActiveCount` filters out sessions whose state is `Closed`; this is consumed by metrics and the dashboard, where `Count` would otherwise momentarily over-report during teardown. + +### SessionWorkerClientFactory (ISessionWorkerClientFactory) + +`SessionWorkerClientFactory.CreateAsync` is the only path that builds an `IWorkerClient`. It drives the session through the protobuf `SessionState` substates in order: `StartingWorker`, `WaitingForPipe`, `Handshaking`, `InitializingWorker`. The substates are wire-visible so the dashboard and clients can render startup progress. + +A linked `CancellationTokenSource` enforces `session.StartupTimeout`. If startup fails or times out, the factory either kills the partially-constructed `WorkerClient` or, if the client was never built, kills the launched process and disposes the named pipe before rethrowing. A pure timeout is rewritten as `TimeoutException` so callers can distinguish it from caller-driven cancellation: + +```csharp +if (exception is OperationCanceledException + && startupCancellation.IsCancellationRequested + && !cancellationToken.IsCancellationRequested) +{ + throw new TimeoutException( + $"Worker session {session.SessionId} did not complete startup within {session.StartupTimeout}.", + exception); +} +``` + +The named pipe is created with `maxNumberOfServerInstances: 1` so a second worker cannot connect to the same pipe name even if the first launch is still pending. Combined with the per-session nonce passed to the worker, this is the gateway's defense against a foreign process answering a pipe. + +### SessionShutdownHostedService + +`SessionShutdownHostedService` is an `IHostedService` whose only job is to call `ISessionManager.ShutdownAsync` from `StopAsync`. It catches `OperationCanceledException` triggered by the host shutdown timeout and logs a warning so that an over-running shutdown does not surface as an unhandled exception. + +### SessionOpenRequest + +`SessionOpenRequest` is the gateway-internal record passed to `OpenSessionAsync`. It is constructed from the wire-level `OpenSessionRequest` via `SessionOpenRequest.FromContract`. Keeping a separate internal record means the gRPC layer can normalize input (defaulting backend, sanitizing strings) without leaking generated proto types into `SessionManager`. + +```csharp +public sealed record SessionOpenRequest( + string? RequestedBackend, + string? ClientSessionName, + string? ClientCorrelationId, + Duration? CommandTimeout) +{ + public static SessionOpenRequest FromContract(OpenSessionRequest request) + { + ArgumentNullException.ThrowIfNull(request); + + return new SessionOpenRequest( + request.RequestedBackend, + request.ClientSessionName, + request.ClientCorrelationId, + request.CommandTimeout); + } +} +``` + +### SessionCloseResult + +`SessionCloseResult` is the record returned from a successful close. `AlreadyClosed` distinguishes "this call closed the session" from "the session was already closed when we acquired the close lock", which the metrics layer uses to avoid double-counting. + +```csharp +public sealed record SessionCloseResult( + string SessionId, + SessionState FinalState, + bool AlreadyClosed); +``` + +### SessionCloseStartedException + +`SessionCloseStartedException` is `internal` and is only thrown from inside `GatewaySession.CloseAsync` when the close path has already begun mutating worker state and a subsequent step fails. `SessionManager.CloseSessionCoreAsync` catches it, marks the session faulted, increments the close-failed metric, removes the session from the registry, and rethrows it wrapped as `SessionManagerException` with `CloseFailed`. The intermediate type exists so the public API surface only ever exposes `SessionManagerException`. + +### SessionManagerException and SessionManagerErrorCode + +`SessionManagerException` is the single public error type emitted from this subsystem; the code is carried in the `ErrorCode` property and is also surfaced to metrics tags via `SessionManagerErrorCode.ToString()`. + +| Code | Meaning | +|------|---------| +| `SessionNotFound` | The session id is not in the registry. | +| `SessionNotReady` | The session or its `IWorkerClient` is not in `Ready` state. | +| `EventSubscriberAlreadyActive` | A second event subscriber attached when only one is allowed. | +| `EventQueueOverflow` | Reserved for the worker event channel overflow path. | +| `SessionLimitExceeded` | `MaxSessions` is in use. | +| `OpenFailed` | `OpenSessionAsync` failed; the inner exception carries the cause. | +| `CloseFailed` | A close started but did not complete cleanly; the session is removed and faulted. | + +## Lifecycle + +### Open + +`SessionManager.OpenSessionAsync` allocates a session slot, builds the `GatewaySession`, registers it, and asks the factory to bring up the worker. Failures roll back every preceding step: + +```csharp +catch (Exception exception) +{ + session?.MarkFaulted(exception.Message); + if (session is not null) + { + _registry.TryRemove(session.SessionId, out _); + await session.DisposeAsync().ConfigureAwait(false); + } + + ReleaseSessionSlot(); + _metrics.Fault(SessionManagerErrorCode.OpenFailed.ToString()); + _logger.LogWarning( + exception, + "Failed to open gateway session {SessionId}.", + session?.SessionId ?? ""); + + throw new SessionManagerException( + SessionManagerErrorCode.OpenFailed, + session is null ? "Failed to create session." : $"Failed to open session {session.SessionId}.", + exception); +} +``` + +The order — fault, deregister, dispose, release slot, record metric, log, rethrow — matters because releasing the semaphore before disposal would let the next open race the worker process tear-down on the same machine. + +### Run + +While `Ready`, callers reach the worker through `SessionManager.InvokeAsync` or `ReadEventsAsync`. Both delegate to `GatewaySession`, which checks the state under lock and updates `LastClientActivityAt` on every invocation. `GatewaySession` also exposes typed bulk helpers (`AddItemBulkAsync`, `SubscribeBulkAsync`, etc.) that wrap `WorkerCommand` round-trips and translate non-`Ok` `ProtocolStatus` replies into `SessionManagerException` with `SessionNotReady`. + +Event streaming uses `AttachEventSubscriber` which returns a disposable lease. When `allowMultipleSubscribers` is false the second attach throws `EventSubscriberAlreadyActive`; this prevents two gRPC streams from racing on the same worker event channel. + +`ExtendLease` and `IsLeaseExpired` cooperate with `SessionManager.CloseExpiredLeasesAsync`, which iterates a registry snapshot and closes any session whose lease has expired with `LeaseExpiredReason`. + +### Close + +`GatewaySession.CloseAsync` is serialized by a per-session `SemaphoreSlim` (`_closeLock`). It transitions to `Closing`, asks the worker client to shut down within `ShutdownTimeout`, and on success transitions to `Closed`. If `WorkerClient.ShutdownAsync` throws, the session falls back to `IWorkerClient.Kill` (forced close): + +```csharp +if (_workerClient is not null) +{ + try + { + await _workerClient.ShutdownAsync(ShutdownTimeout, cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) + { + try + { + _workerClient.Kill(reason); + } + catch (Exception killException) + { + throw new SessionCloseStartedException( + $"Session {SessionId} close failed after worker shutdown started.", + new AggregateException(exception, killException)); + } + + throw; + } +} +``` + +If both graceful shutdown and the kill fall-back fail, the original and kill exceptions are bundled into an `AggregateException` and surfaced as `SessionCloseStartedException`. `SessionManager.CloseSessionCoreAsync` then translates that into a `SessionManagerException` with `CloseFailed` and removes the session. + +`GatewaySession.KillWorker` is the unconditional forced-close path used by shutdown when graceful close itself throws. + +## Shutdown Coordination + +`SessionShutdownHostedService.StopAsync` calls `SessionManager.ShutdownAsync`, which closes every registered session with `GatewayShutdownReason`. The shutdown loop catches per-session exceptions, calls `KillWorker`, and removes the session so that one stuck worker cannot block the rest of the host: + +```csharp +public async Task ShutdownAsync(CancellationToken cancellationToken) +{ + foreach (GatewaySession session in _registry.Snapshot()) + { + try + { + await CloseSessionCoreAsync(session, GatewayShutdownReason, cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) + { + _logger.LogWarning( + exception, + "Graceful shutdown failed for session {SessionId}; killing worker.", + session.SessionId); + if (_registry.TryGet(session.SessionId, out _)) + { + session.KillWorker(GatewayShutdownReason); + await RemoveSessionAsync(session).ConfigureAwait(false); + } + } + } +} +``` + +Iterating over `Snapshot` rather than the live dictionary lets `RemoveSessionAsync` mutate the registry inside the loop without throwing. + +## Dependency Injection + +`SessionServiceCollectionExtensions.AddGatewaySessions` registers the four singletons and the hosted service: + +```csharp +public static IServiceCollection AddGatewaySessions(this IServiceCollection services) +{ + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddHostedService(); + + return services; +} +``` + +The registry must be a singleton because its `ConcurrentDictionary` is the source of truth for session state across the gRPC service, the lease sweeper, the dashboard, and the shutdown hosted service. Registering `SessionShutdownHostedService` last ensures it is constructed after `ISessionManager` and therefore drains sessions during host stop. + +## Related Documentation + +- [Gateway Process Design](./gateway-process-design.md) +- [Gateway Configuration](./GatewayConfiguration.md) +- [Worker Process Launcher](./WorkerProcessLauncher.md) diff --git a/docs/WorkerBootstrap.md b/docs/WorkerBootstrap.md new file mode 100644 index 0000000..c553eb9 --- /dev/null +++ b/docs/WorkerBootstrap.md @@ -0,0 +1,223 @@ +# Worker Bootstrap + +The bootstrap layer parses the command-line arguments and environment variables passed to the `MxGateway.Worker` process, validates them against the gateway contract, and produces either a populated `WorkerOptions` instance or a structured failure that maps to a `WorkerExitCode`. + +## Overview + +The worker process is a short-lived child of the gateway. The gateway side of this contract lives in [WorkerProcessLauncher](./WorkerProcessLauncher.md). On the worker side, `Program.cs` is a single line that delegates to `WorkerApplication.Run(args)`: + +```csharp +using MxGateway.Worker; + +return WorkerApplication.Run(args); +``` + +`WorkerApplication.Run` constructs the bootstrap dependencies (`EnvironmentVariableWorkerEnvironment`, `WorkerConsoleLogger` writing to `Console.Error`, and a `WorkerPipeClient`), runs `WorkerOptionsParser`, and routes the resulting `WorkerBootstrapResult` either into the pipe client or into a non-zero exit. Splitting parsing from process wiring lets tests substitute fakes for the environment, logger, and pipe client without spawning a child process. + +## Worker Options + +`WorkerOptions` is the validated input contract for a worker session. The gateway hands every field to the worker; the worker never reads configuration files. + +```csharp +public sealed class WorkerOptions +{ + public const string NonceEnvironmentVariableName = "MXGATEWAY_WORKER_NONCE"; + + public WorkerOptions( + string sessionId, + string pipeName, + uint protocolVersion, + string nonce) + { + SessionId = sessionId; + PipeName = pipeName; + ProtocolVersion = protocolVersion; + Nonce = nonce; + } + + public string SessionId { get; } + public string PipeName { get; } + public uint ProtocolVersion { get; } + public string Nonce { get; } +} +``` + +### Required inputs + +All four fields are required. Three arrive on the command line and one arrives via environment variable: + +| Source | Name | Maps to | +|--------|------|---------| +| Argument | `--session-id` | `SessionId` | +| Argument | `--pipe-name` | `PipeName` | +| Argument | `--protocol-version` | `ProtocolVersion` | +| Env var | `MXGATEWAY_WORKER_NONCE` | `Nonce` | + +The nonce travels via environment variable rather than an argument because process command lines are visible to other users on Windows through `wmic`, `Get-CimInstance Win32_Process`, and the kernel object table; environment variables of another process are not. Treating the nonce as a credential keeps it off the command line. + +There are no optional options. An unknown flag, a flag without a value, or a flag whose value starts with `--` is reported as an error rather than silently ignored. + +## The Parser + +`WorkerOptionsParser` walks `args` once, collects values into a case-insensitive dictionary, and accumulates errors so the caller sees every problem in a single failure rather than fixing them one at a time. + +```csharp +for (int index = 0; index < args.Length; index++) +{ + string arg = args[index]; + if (!IsKnownOption(arg)) + { + errors.Add($"Unknown option '{arg}'."); + continue; + } + + if (index + 1 >= args.Length || args[index + 1].StartsWith("--", StringComparison.Ordinal)) + { + errors.Add($"Option '{arg}' requires a value."); + continue; + } + + values[arg] = args[index + 1]; + index++; +} +``` + +After argument scanning, the parser cross-checks the protocol version against `GatewayContractInfo.WorkerProtocolVersion`. A version that parses as a `uint` but does not match the contract value is a hard failure with `WorkerExitCode.InvalidProtocolVersion`, separate from `InvalidArguments`, so the gateway can distinguish a malformed launch from a version mismatch and report a useful upgrade message. + +The nonce is read last so that argument-shape errors are reported before the parser asks the environment for a secret it might not need. + +## Bootstrap Result + +`WorkerBootstrapResult` is a discriminated success/failure carrier. `Options` is non-null when `Succeeded` is true; `Errors` is populated only on failure. + +```csharp +public static WorkerBootstrapResult Success(WorkerOptions options) +{ + return new WorkerBootstrapResult(WorkerExitCode.Success, options, []); +} + +public static WorkerBootstrapResult Failure(WorkerExitCode exitCode, IEnumerable errors) +{ + return new WorkerBootstrapResult(exitCode, null, errors.ToArray()); +} +``` + +`Succeeded` is defined as `ExitCode == WorkerExitCode.Success` rather than as a separate flag, so the exit code and the success state cannot disagree. + +## Exit Codes + +`WorkerExitCode` is the worker process's exit contract. The gateway-side launcher decodes these values to decide whether a relaunch is safe. + +| Value | Numeric | Produced when | +|-------|---------|---------------| +| `Success` | 0 | The pipe session ran to a clean close. | +| `UnexpectedFailure` | 1 | Any unhandled exception not matched by a more specific catch. | +| `InvalidArguments` | 2 | One or more `--session-id`, `--pipe-name`, or `--protocol-version` errors (missing, empty, unknown flag, or no value). | +| `InvalidProtocolVersion` | 3 | `--protocol-version` is not a `uint` or does not equal `GatewayContractInfo.WorkerProtocolVersion`. | +| `MissingNonce` | 4 | The `MXGATEWAY_WORKER_NONCE` environment variable is null, empty, or whitespace. | +| `PipeConnectionFailed` | 5 | An `IOException` or `TimeoutException` escapes the pipe client. | +| `ProtocolViolation` | 6 | A `WorkerFrameProtocolException` escapes the pipe client. | + +`InvalidArguments`, `InvalidProtocolVersion`, and `MissingNonce` originate in the parser; the others originate in `WorkerApplication.Run`'s `try/catch` around the pipe client. + +## Environment Abstraction + +`IWorkerEnvironment` exists so tests can supply a fake nonce without mutating the real process environment, which would be a shared mutable global across parallel test runs. + +```csharp +public interface IWorkerEnvironment +{ + string? GetEnvironmentVariable(string name); +} + +public sealed class EnvironmentVariableWorkerEnvironment : IWorkerEnvironment +{ + public string? GetEnvironmentVariable(string name) + { + return Environment.GetEnvironmentVariable(name); + } +} +``` + +The production binding in `WorkerApplication.Run(string[])` is `EnvironmentVariableWorkerEnvironment`, which is a thin pass-through to `System.Environment.GetEnvironmentVariable`. + +## Logging + +The worker writes structured key/value lines to standard error. Standard error is used rather than standard output because the gateway side reads worker stdout for diagnostic capture only, while stderr is reserved for log output that does not interfere with any future stdout-based channel. + +### The logger contract + +`IWorkerLogger` exposes only `Information` and `Error`. There is no `Debug` or `Trace` level, because the worker is launched per session and verbose tracing belongs to the gateway-side launcher. + +```csharp +public interface IWorkerLogger +{ + void Information(string eventName, IReadOnlyDictionary fields); + + void Error(string eventName, IReadOnlyDictionary fields); +} +``` + +`WorkerConsoleLogger` formats each call as `level= event= key=value key=value` after running the field dictionary through `WorkerLogRedactor`: + +```csharp +private void Write( + string level, + string eventName, + IReadOnlyDictionary fields) +{ + Dictionary redactedFields = WorkerLogRedactor.RedactFields(fields); + string fieldText = string.Join( + " ", + redactedFields.Select(field => $"{field.Key}={FormatValue(field.Value)}")); + + _writer.WriteLine($"level={level} event={eventName} {fieldText}".TrimEnd()); +} +``` + +### What the redactor redacts and why + +`AGENTS.md` "Security And Logging" requires that the worker never log raw credential values for `AuthenticateUser`, `WriteSecured`, or related secured operations. The bootstrap nonce is also a credential: anyone who reads it can impersonate the worker to the gateway pipe. `WorkerLogRedactor` enforces this by replacing values whose field name contains any of these substrings (case-insensitive) with the literal `[redacted]`: + +```csharp +private static readonly string[] SensitiveFieldNameParts = +[ + "nonce", + "secret", + "password", + "token", + "credential", + "apikey", + "api_key", +]; +``` + +The match is on substrings of the field name rather than an exact list, so a field called `auth_token` or `user_password` is redacted automatically without each call site having to remember to opt in. `null` values pass through unchanged so the absence of a value is still visible in logs. + +## How `Program.cs` Consumes The Result + +`WorkerApplication.Run` is the single consumer of `WorkerBootstrapResult`. On failure it logs a `WorkerBootstrapFailed` event and returns the numeric `ExitCode` directly: + +```csharp +WorkerOptionsParser parser = new(environment); +WorkerBootstrapResult result = parser.Parse(args); + +if (!result.Succeeded) +{ + logger.Error("WorkerBootstrapFailed", new Dictionary + { + ["exit_code"] = result.ExitCode, + ["errors"] = string.Join(";", result.Errors), + }); + + return (int)result.ExitCode; +} +``` + +On success it logs `WorkerBootstrapSucceeded` with the session fields (the `nonce` field is redacted by `WorkerLogRedactor` because of its name), hands the `WorkerOptions` to `IWorkerPipeClient.RunAsync`, and waits synchronously. The `try/catch` around the pipe call maps `WorkerFrameProtocolException` to `ProtocolViolation`, `IOException`/`TimeoutException` to `PipeConnectionFailed`, and any other exception to `UnexpectedFailure`, so every code path through `Run` returns one of the values in `WorkerExitCode`. + +## Related Documentation + +- [Worker Process Launcher](./WorkerProcessLauncher.md) +- [Worker STA](./WorkerSta.md) +- [Worker Frame Protocol](./WorkerFrameProtocol.md) diff --git a/docs/WorkerConversion.md b/docs/WorkerConversion.md new file mode 100644 index 0000000..24a4bc9 --- /dev/null +++ b/docs/WorkerConversion.md @@ -0,0 +1,262 @@ +# Worker Conversion Layer + +The conversion layer in `MxGateway.Worker.Conversion` projects COM `VARIANT` payloads, `HRESULT` codes, and `MXSTATUS_PROXY` records into the protobuf wire types in `MxGateway.Contracts.Proto`. The design is parity-first: every projection preserves enough raw metadata that the original COM observation can be reconstructed downstream. + +## Overview + +`AGENTS.md` (section "Value And Status Rules") requires that the wire format use a value union capable of representing COM `VARIANT` values and arrays, that lossy conversions retain both the typed projection and raw diagnostic metadata, and that `MXSTATUS_PROXY` arrays never collapse to a single success flag. The types in `src/MxGateway.Worker/Conversion/` are the worker-side enforcement of those rules. + +The layer is split into three concerns: + +- Value projection: `VariantConverter` +- HRESULT projection: `HResultConverter`, `HResultConversion` +- Status projection: `MxStatusProxyConverter`, `MxStatusDetailText`, `MxStatusConversionException` + +## VariantConverter + +`VariantConverter` projects scalar and array `VARIANT` payloads delivered by COM interop into `MxValue` and `MxArray`. It accepts an optional `expectedDataType` so that an MXAccess attribute hint (for example `MxDataType.Time` for a 64-bit FILETIME) overrides the default CLR-driven projection. + +### Scalar projection + +Scalars dispatch on `Type.GetTypeCode` and populate the matching field in the `MxValue` `oneof`. Each branch records both the typed value and the source `VARIANT` tag in `VariantType`, so consumers can distinguish a `short` from an `int` even after both project to `MxDataType.Integer`. + +```csharp +case TypeCode.Boolean: + return new MxValue + { + DataType = MxDataType.Boolean, + VariantType = variantType, + BoolValue = (bool)value, + }; + +case TypeCode.Byte: +case TypeCode.SByte: +case TypeCode.Int16: +case TypeCode.UInt16: +case TypeCode.Int32: + return new MxValue + { + DataType = MxDataType.Integer, + VariantType = variantType, + Int32Value = System.Convert.ToInt32(value, CultureInfo.InvariantCulture), + }; +``` + +`Decimal` and 64-bit unsigned integers that exceed `long.MaxValue` cannot project losslessly. The converter still emits a typed best-effort value but attaches a `RawDiagnostic` so the loss is recorded on the wire rather than silently absorbed. + +```csharp +case TypeCode.Decimal: + return new MxValue + { + DataType = MxDataType.Double, + VariantType = variantType, + DoubleValue = System.Convert.ToDouble(value, CultureInfo.InvariantCulture), + RawDiagnostic = "Decimal value projected to double.", + }; +``` + +Time-typed integers use the `expectedDataType` hint. `ConvertInt64Scalar` interprets a `long` as a Windows FILETIME when the caller asks for `MxDataType.Time`, otherwise it stays an integer. `ConvertUInt64Scalar` falls through to a raw projection if the value exceeds `long.MaxValue`. + +### Null and missing values + +`VARIANT` distinguishes `VT_EMPTY` from `VT_NULL`. The converter preserves both by inspecting whether the input is `DBNull` or a CLR null reference, and tags the result with `IsNull = true` so consumers do not misread a default value as data. + +```csharp +private static MxValue CreateNullValue( + object? value, + MxDataType expectedDataType) +{ + return new MxValue + { + DataType = expectedDataType == MxDataType.Unspecified ? MxDataType.NoData : expectedDataType, + VariantType = value is DBNull ? "VT_NULL" : "VT_EMPTY", + IsNull = true, + }; +} +``` + +### Array projection + +`ConvertArray` records the rank and per-dimension lengths so multi-dimensional `SAFEARRAY` shapes survive the round trip. The element type is resolved from the caller-supplied hint or the CLR element type via `ResolveArrayElementDataType`, then dispatched to the matching typed builder (`ConvertBoolArray`, `ConvertInt64Array`, `ConvertTimestampArray`, and so on). + +```csharp +for (int dimension = 0; dimension < array.Rank; dimension++) +{ + mxArray.Dimensions.Add((uint)array.GetLength(dimension)); +} + +System.Type? elementType = array.GetType().GetElementType(); +MxDataType elementDataType = ResolveArrayElementDataType(elementType, expectedElementDataType); +mxArray.ElementDataType = elementDataType; +``` + +When the element type cannot be classified, `ConvertArray` does not throw. It downgrades the result to `MxDataType.Unknown`, records the original expected type in `RawElementDataType`, and serializes each element via `ConvertRawArray` as a UTF-8 byte string. This satisfies the AGENTS.md requirement to keep both the best typed projection and the raw diagnostic metadata. + +```csharp +default: + mxArray.ElementDataType = MxDataType.Unknown; + mxArray.RawElementDataType = (int)expectedElementDataType; + mxArray.RawDiagnostic = CreateRawDiagnostic(array); + mxArray.RawValues = ConvertRawArray(array); + return mxArray; +``` + +### Variant type names + +`GetVariantTypeName` maps CLR types to canonical `VT_*` strings (`VT_BOOL`, `VT_I4`, `VT_BSTR`, `VT_DATE`, and so on). Unmapped CLR types fall back to `CLR:` so the wire format never silently invents a `VT_*` tag it cannot justify. Array element tags are wrapped as `SAFEARRAY()` by `CreateArrayVariantType`. + +### Why lossless preservation matters + +The MXAccess engine returns values whose semantic type only fully resolves after consulting the engine's own attribute metadata. Clients that round-trip these values through the gateway (replay, parity fixtures, diagnostics) need the original `VT_*` tag, the engine-declared `MxDataType`, and any conversion diagnostic; otherwise edge cases such as decimal-to-double rounding, ulong overflow, or an unknown SAFEARRAY element type become invisible bugs. Storing both the typed projection and the raw fields in the same `MxValue`/`MxArray` lets cross-language clients recover the original observation byte-for-byte where possible and detect lossy cases where it is not. + +## HResultConverter and HResultConversion + +`HResultConverter.Convert` wraps any `Exception` thrown across the COM boundary. It prefers `COMException.ErrorCode` over `Exception.HResult` because the runtime sometimes overwrites `Exception.HResult` while marshalling, and the `ErrorCode` field is the value the COM call actually returned. + +```csharp +public HResultConversion Convert(Exception exception) +{ + if (exception is null) + { + throw new ArgumentNullException(nameof(exception)); + } + + int hresult = exception is COMException comException + ? comException.ErrorCode + : exception.HResult; + + return new HResultConversion( + hresult, + CreateProtocolStatus(hresult, exception), + CreateSafeDiagnosticMessage(exception)); +} +``` + +`CreateProtocolStatus` maps `0` (`S_OK`) to `ProtocolStatusCode.Ok` and any non-zero HRESULT to `ProtocolStatusCode.MxaccessFailure`. The status `Message` includes the exception type name and the formatted HRESULT (`HRESULT 0x<8 hex digits>`), giving downstream operators the same identifier they would see in COM event logs. + +`HResultConversion` is an immutable carrier with three fields: + +| Field | Purpose | +|-------|---------| +| `HResult` | Raw signed 32-bit HRESULT, suitable for inclusion in a wire reply | +| `ProtocolStatus` | The `ProtocolStatus` projection used in command replies | +| `DiagnosticMessage` | A safe, fully-qualified diagnostic string for logs | + +The diagnostic message is built from the exception type's `FullName` and the formatted HRESULT only. It deliberately omits `Exception.Message` so user-supplied or localized strings do not leak into worker log channels that may be shipped to less trusted destinations. + +## MxStatusProxyConverter + +`MxStatusProxyConverter` projects MXAccess `MXSTATUS_PROXY` records into the `MxStatusProxy` proto message. Because the COM struct is exposed via interop reflection rather than a typed binding, the converter reads the `success`, `category`, `detectedBy`, and `detail` fields by name through `FieldInfo`. + +```csharp +public MxStatusProxy Convert(object status) +{ + if (status is null) + { + throw new ArgumentNullException(nameof(status)); + } + + Type statusType = status.GetType(); + int success = ReadInt32Field(status, statusType, "success"); + int rawCategory = ReadInt32Field(status, statusType, "category"); + int rawDetectedBy = ReadInt32Field(status, statusType, "detectedBy"); + int detail = ReadInt32Field(status, statusType, "detail"); + + return new MxStatusProxy + { + Success = success, + Category = MapCategory(rawCategory), + DetectedBy = MapSource(rawDetectedBy), + Detail = detail, + RawCategory = rawCategory, + RawDetectedBy = rawDetectedBy, + DiagnosticText = MxStatusDetailText.Lookup(detail), + }; +} +``` + +`MapCategory` and `MapSource` translate the integer codes documented for `MXSTATUS_PROXY` (for example `0 = Ok`, `3 = CommunicationError`, `0 = RequestingLmx`, `5 = RespondingAutomationObject`) into typed enums. The original integers are preserved alongside the typed projection in `RawCategory` and `RawDetectedBy`, so any code the runtime emits outside the documented range is still observable. + +### Status arrays + +`ConvertMany` walks an inbound `Array` of status structs and emits `IReadOnlyList`. Null entries become explicit `Unknown`/`Unknown` placeholders with a diagnostic text rather than being dropped, so the index of each status still aligns with the index of the value or item handle it describes. + +```csharp +foreach (object? status in statuses) +{ + if (status is null) + { + converted.Add(new MxStatusProxy + { + Category = MxStatusCategory.Unknown, + DetectedBy = MxStatusSource.Unknown, + DiagnosticText = "Null MXSTATUS_PROXY entry.", + }); + continue; + } + + converted.Add(Convert(status)); +} +``` + +### Why arrays are not collapsed + +A single MXAccess command (notably `Read`, `Write`, and event callbacks) can return one status per item handle. AGENTS.md requires that the wire format represent each entry independently, because collapsing them to a Boolean success flag hides partial failures: a 50-item write where one item fails would be indistinguishable from a 50-item write where every item failed. Preserving the array per-position lets clients correlate each `MxStatusProxy` with its item handle and `MxValue`. + +### Completion-only status fallback + +When MXAccess returns a status payload that is not a recognizable `MXSTATUS_PROXY` struct (for example a completion-only byte buffer from older runtimes), `PreserveCompletionOnlyStatusBytes` hex-encodes the raw bytes so they survive transport even though they cannot be typed. + +```csharp +public string PreserveCompletionOnlyStatusBytes(byte[] statusBytes) +{ + if (statusBytes is null) + { + throw new ArgumentNullException(nameof(statusBytes)); + } + + return $"completion_only_status_hex={BitConverter.ToString(statusBytes).Replace("-", string.Empty)}"; +} +``` + +## MxStatusDetailText + +`MxStatusDetailText` is an internal lookup that maps known `MXSTATUS_PROXY.detail` codes to short human-readable strings (for example `28 = "Index out of range"`, `42 = "Unable to convert string"`, `8017 = "Object must be offscan to modify attributes that have an MxSecurityConfigure security classification"`). `MxStatusProxyConverter.Convert` calls `Lookup` and writes the result to `DiagnosticText`. Unknown codes return `string.Empty`, leaving the numeric `Detail` field as the authoritative identifier. + +The mapping covers the engine-error range documented for MXAccess (16-50, 56-61, 541-542, 8017). Adding entries here is the supported way to enrich wire-level diagnostics without changing the proto schema. + +## MxStatusConversionException + +`MxStatusConversionException` is the worker-internal signal that a status struct could not be projected at all - for example, the interop type is missing one of the required fields, or the field is null. `MxStatusProxyConverter.ReadInt32Field` raises it with a message that names both the offending CLR type and the missing field. + +```csharp +private static int ReadInt32Field( + object value, + Type valueType, + string fieldName) +{ + FieldInfo? field = valueType.GetField(fieldName, BindingFlags.Instance | BindingFlags.Public); + if (field is null) + { + throw new MxStatusConversionException( + $"Status object type '{valueType.FullName}' does not expose required field '{fieldName}'."); + } + + object? fieldValue = field.GetValue(value); + if (fieldValue is null) + { + throw new MxStatusConversionException( + $"Status object field '{fieldName}' on type '{valueType.FullName}' is null."); + } + + return System.Convert.ToInt32(fieldValue, CultureInfo.InvariantCulture); +} +``` + +The exception is distinct from `COMException` so the worker's command pipeline can route it through a dedicated handler. Callers typically translate it into a `ProtocolStatus` with `ProtocolStatusCode.MxaccessFailure` and a synthetic HRESULT, then attach the exception message as a `RawDiagnostic` rather than letting the failure surface as a generic worker error. + +## Related Documentation + +- [MXAccess Worker Instance Design](./mxaccess-worker-instance-design.md) +- [Contracts](./Contracts.md) +- [Worker STA Threading](./WorkerSta.md) diff --git a/docs/WorkerSta.md b/docs/WorkerSta.md new file mode 100644 index 0000000..8160074 --- /dev/null +++ b/docs/WorkerSta.md @@ -0,0 +1,157 @@ +# Worker STA Runtime + +The worker STA runtime owns the dedicated single-threaded apartment thread that hosts the MXAccess COM object, runs a Windows message pump for COM event delivery, and serializes all gateway commands onto that thread. + +## Why an STA Is Required + +The installed MXAccess interop assembly declares an `Apartment` threading model (see `AGENTS.md` under "Worker Rules"). COM objects with that model must be created and called on a thread initialized as a single-threaded apartment, and any callbacks the object raises (event sink calls) are delivered through the thread's Windows message queue. A plain blocking queue is not sufficient: the STA loop must pump Windows messages so that the COM marshaler can deliver event invocations on the same thread that holds the object. Because of that constraint, every MXAccess operation in the worker is funneled through the types in `src/MxGateway.Worker/Sta/`. + +## Types + +| Type | Role | +|------|------| +| `StaRuntime` | Owns the STA `Thread`, the command queue, and the lifecycle gates. | +| `IStaComApartmentInitializer` / `StaComApartmentInitializer` | Calls `CoInitializeEx` / `CoUninitialize` so the thread enters and leaves the apartment-threaded apartment. | +| `StaMessagePump` | Wraps `MsgWaitForMultipleObjectsEx`, `PeekMessage`, `TranslateMessage`, and `DispatchMessage` so the STA loop can wait on work and drain the Windows message queue. | +| `IStaWorkItem` / `StaWorkItem` | Internal queue entries that capture a delegate, a `CancellationToken`, and a `TaskCompletionSource` for the caller. | +| `StaCommand` | Carries an `MxCommand` together with `SessionId`, `CorrelationId`, `EnqueueTimestamp`, and a `CancellationToken`. | +| `IStaCommandExecutor` | The boundary between the dispatcher and the MXAccess interop layer; returns `MxCommandReply`. | +| `StaCommandDispatcher` | Bounded asynchronous queue in front of `StaRuntime` that converts `StaCommand` into `MxCommandReply` and applies status normalization. | + +## STA Thread Initialization + +`StaRuntime`'s constructor configures a background `Thread` named `MxGateway.Worker.STA` and forces it into `ApartmentState.STA` before the thread starts. `Start()` releases the thread and then blocks on `startedEvent` so callers observe a fully-initialized apartment (or a captured `startupException`) before the first `InvokeAsync` call: + +```csharp +staThread = new Thread(ThreadMain) +{ + IsBackground = true, + Name = "MxGateway.Worker.STA" +}; +staThread.SetApartmentState(ApartmentState.STA); +``` + +`StaComApartmentInitializer.Initialize` calls `CoInitializeEx` with `COINIT_APARTMENTTHREADED` (`0x2`) and treats both `S_OK` and `S_FALSE` as success because `S_FALSE` indicates the apartment was already initialized on this thread. Any other HRESULT throws `COMException` so `ThreadMain` records the failure and signals `startedEvent`, which causes `Start()` to surface the exception. + +## The STA Loop + +`ThreadMain` runs the canonical "wait, pump, dispatch" loop. It enters COM, drains queued work, blocks until either a command arrives or a Windows message is posted, then pumps the message queue and records activity for the heartbeat: + +```csharp +StaThreadId = Thread.CurrentThread.ManagedThreadId; +comApartmentInitializer.Initialize(); +comInitialized = true; +MarkActivity(); +startedEvent.Set(); + +while (!IsShutdownRequested()) +{ + ProcessQueuedCommands(); + messagePump.WaitForWorkOrMessages(commandWakeEvent, idlePumpInterval); + messagePump.PumpPendingMessages(); + MarkActivity(); +} +``` + +`commandWakeEvent` is an `AutoResetEvent` set by `InvokeAsync` whenever a new work item is enqueued. The `idlePumpInterval` defaults to 50 ms so the pump still services Windows messages even when no commands are queued; this matters because COM event sink calls arrive as posted messages and would otherwise sit in the queue until the next command. `LastActivityUtc` is updated through `Volatile.Write` on every iteration so the worker's heartbeat path can read it without holding `gate`. + +### The message pump + +`StaMessagePump.WaitForWorkOrMessages` calls `MsgWaitForMultipleObjectsEx` with `QS_ALLINPUT` and `MWMO_INPUTAVAILABLE`, so the wait wakes for either a signal on `commandWakeEvent` or any new message in the thread's Windows queue. `PumpPendingMessages` then drains the queue with `PM_REMOVE`: + +```csharp +public int PumpPendingMessages() +{ + int pumpedMessages = 0; + + while (PeekMessage(out NativeMessage message, IntPtr.Zero, 0, 0, PmRemove)) + { + TranslateMessage(ref message); + DispatchMessage(ref message); + pumpedMessages++; + } + + return pumpedMessages; +} +``` + +`DispatchMessage` is what causes the COM marshaler to deliver event sink calls onto the STA thread; without this loop, MXAccess events never surface to managed code. + +## Work Items and the Command Queue + +`StaRuntime` exposes two `InvokeAsync` overloads. Both wrap the delegate in an internal `StaWorkItem`, enqueue it on a `ConcurrentQueue`, and signal `commandWakeEvent`: + +```csharp +StaWorkItem workItem = new(command, cancellationToken); + +lock (gate) +{ + if (shutdownRequested) + { + return Task.FromException( + new InvalidOperationException("The worker STA runtime is shutting down.")); + } + + commandQueue.Enqueue(workItem); +} + +commandWakeEvent.Set(); +return workItem.Task; +``` + +`StaWorkItem` uses an `Interlocked.CompareExchange` on `started` so that exactly one of three outcomes happens: the STA thread executes the delegate, an external `CancellationToken` cancellation fires first, or `CancelBeforeExecution` runs during shutdown. `Execute` runs on the STA thread, sets `Completion` from `command()`, and propagates exceptions through `TrySetException` so the awaiting caller observes them. + +`ProcessQueuedCommands` is the only consumer of the queue and runs on the STA thread, so each work item is guaranteed to execute on the apartment that owns the COM object. + +## Command Dispatch + +`StaCommandDispatcher` sits between the worker's IPC layer and `StaRuntime`. It converts an `StaCommand` into an `MxCommandReply` and enforces a bounded queue: when `commandQueue.Count` reaches `maxPendingCommands` (default `DefaultMaxPendingCommands = 128`) the dispatcher returns a synthetic `WorkerUnavailable` reply rather than queueing further work. This back-pressure keeps the STA from accumulating an unbounded backlog while it is busy with a long-running call. + +A single drain task pulls from `commandQueue` and submits each command to the STA via `staRuntime.InvokeAsync`: + +```csharp +SetCurrentCommand(command.CorrelationId); +try +{ + MxCommandReply reply = await staRuntime + .InvokeAsync(() => commandExecutor.Execute(command)) + .ConfigureAwait(false); + + queuedCommand.Complete(NormalizeReply(command, reply)); +} +catch (Exception exception) +{ + queuedCommand.Complete(CreateExceptionReply(command, exception)); +} +finally +{ + SetCurrentCommand(string.Empty); +} +``` + +`SetCurrentCommand` records the in-flight `CorrelationId` so `PopulateHeartbeat` can publish both `PendingCommandCount` and `CurrentCommandCorrelationId` on the worker heartbeat. Exceptions are converted through `HResultConverter` so the IPC reply still carries a structured `ProtocolStatus`, an HRESULT, and a diagnostic message instead of an unhandled fault. `NormalizeReply` backfills `SessionId`, `CorrelationId`, `Kind`, and a default `ProtocolStatusCode.Ok` so executors can return minimal replies without restating the envelope. + +`CancelQueuedCommand` walks the queue and completes a single matching entry with `ProtocolStatusCode.Canceled`. It cannot abort a command already running on the STA: per `AGENTS.md`, "Canceling a gRPC call should stop waiting in the gateway, but it cannot safely abort an in-flight COM call on the STA. Hard cancellation means killing the worker process." + +## Why the STA Loop Cannot Block on I/O + +`AGENTS.md` states explicitly: "Do not block the STA on pipe writes, gRPC calls, or slow consumers. Event handlers should convert event args, enqueue outbound events, and return to pumping messages." The STA thread is the only thread that can service COM event callbacks, so any work that blocks it stalls every event the MXAccess object would otherwise deliver. The runtime keeps to that rule by giving the STA only two responsibilities inside `ThreadMain`: executing already-queued work items and pumping messages. Outbound event delivery and pipe writes happen on threads that observe the queues populated from the STA, never on the STA itself. + +## Shutdown Sequence + +`StaRuntime.Shutdown(TimeSpan timeout)` performs an ordered shutdown: + +1. Sets `shutdownRequested` under `gate` so `InvokeAsync` rejects new work with `InvalidOperationException`. +2. Signals `commandWakeEvent` to break the STA out of `WaitForWorkOrMessages`. +3. Waits up to `timeout` on `stoppedEvent`, which the STA sets after it leaves `ThreadMain`. +4. Once the thread has stopped, drains the queue through `CancelQueuedCommands`, which calls `CancelBeforeExecution` on every remaining work item so awaiting callers observe `OperationCanceledException` instead of hanging. + +`ThreadMain`'s `finally` block guarantees that `comApartmentInitializer.Uninitialize` runs (when COM was successfully initialized) before `stoppedEvent.Set`, so the apartment is always torn down on the same thread that initialized it. `Dispose` calls `Shutdown` with a five-second budget and only disposes the wait handles when shutdown actually completed, which prevents a still-running STA thread from touching disposed handles. + +`StaCommandDispatcher.RequestShutdown` mirrors the same intent at the dispatcher layer: it sets `shutdownRequested`, drains its own queue, and completes every queued command with `ProtocolStatusCode.WorkerUnavailable` so callers receive a structured rejection during teardown. + +## Related Documentation + +- [MXAccess worker instance design](./mxaccess-worker-instance-design.md) +- [Worker conversion](./WorkerConversion.md) +- [Worker bootstrap](./WorkerBootstrap.md) diff --git a/docs/client-proto-generation.md b/docs/client-proto-generation.md index 74d14e9..745b54d 100644 --- a/docs/client-proto-generation.md +++ b/docs/client-proto-generation.md @@ -23,11 +23,15 @@ The source files listed by the manifest are: - `src/MxGateway.Contracts/Protos/mxaccess_gateway.proto` - `src/MxGateway.Contracts/Protos/mxaccess_worker.proto` +- `src/MxGateway.Contracts/Protos/galaxy_repository.proto` `mxaccess_gateway.proto` defines the public gRPC service and shared DTOs. `mxaccess_worker.proto` is included in the descriptor because worker-aware tests and fake-worker clients need the same command, reply, event, value, and -status shapes. +status shapes. `galaxy_repository.proto` defines the read-only Galaxy +Repository browse service used by clients to enumerate the deployed object +hierarchy and dynamic attributes; see +[Galaxy Repository Browse](./GalaxyRepository.md). ## Protocol Version diff --git a/docs/gateway-dashboard-design.md b/docs/gateway-dashboard-design.md index 0c521b3..cd05843 100644 --- a/docs/gateway-dashboard-design.md +++ b/docs/gateway-dashboard-design.md @@ -48,10 +48,20 @@ Endpoint layout: /dashboard/sessions/{sessionId} /dashboard/workers /dashboard/events +/dashboard/galaxy /dashboard/settings /dashboard/_blazor ``` +The `/dashboard/galaxy` page surfaces the Galaxy Repository browse summary +(deployed object hierarchy size, last deploy timestamp, attribute totals, +template usage, and connectivity sync info). The summary is fed by +`GalaxySummaryCache`, which is refreshed off the request path by +`GalaxySummaryRefreshService` on the +`MxGateway:Galaxy:DashboardRefreshIntervalSeconds` cadence so the dashboard +never blocks on SQL. See [Galaxy Repository Browse](./GalaxyRepository.md) for +the underlying gRPC service. + The app should redirect `/` to `/dashboard` only if the deployment wants the dashboard as the default web page. Otherwise leave gRPC/API hosting unaffected. diff --git a/gateway.md b/gateway.md index 7457ca3..0ffba5b 100644 --- a/gateway.md +++ b/gateway.md @@ -62,6 +62,9 @@ Detailed follow-up docs: Python, and Java. - `docs/implementation-plan-index.md` links the detailed implementation plans and recommended Gitea milestones/issues. +- `docs/GalaxyRepository.md` covers the read-only Galaxy Repository browse + RPCs that let clients enumerate the deployed object hierarchy and dynamic + attributes before subscribing via the MXAccess gateway service. Implementation style guides: diff --git a/scripts/publish-client-proto-inputs.ps1 b/scripts/publish-client-proto-inputs.ps1 index f4c5751..4457cda 100644 --- a/scripts/publish-client-proto-inputs.ps1 +++ b/scripts/publish-client-proto-inputs.ps1 @@ -78,7 +78,8 @@ try { "--include_source_info" ` "--descriptor_set_out=$outputPath" ` "mxaccess_gateway.proto" ` - "mxaccess_worker.proto" + "mxaccess_worker.proto" ` + "galaxy_repository.proto" if ($LASTEXITCODE -ne 0) { throw "protoc failed with exit code $LASTEXITCODE." diff --git a/src/MxGateway.Contracts/Generated/GalaxyRepository.cs b/src/MxGateway.Contracts/Generated/GalaxyRepository.cs new file mode 100644 index 0000000..517e7f9 --- /dev/null +++ b/src/MxGateway.Contracts/Generated/GalaxyRepository.cs @@ -0,0 +1,2900 @@ +// +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: galaxy_repository.proto +// +#pragma warning disable 1591, 0612, 3021, 8981 +#region Designer generated code + +using pb = global::Google.Protobuf; +using pbc = global::Google.Protobuf.Collections; +using pbr = global::Google.Protobuf.Reflection; +using scg = global::System.Collections.Generic; +namespace MxGateway.Contracts.Proto.Galaxy { + + /// Holder for reflection information generated from galaxy_repository.proto + public static partial class GalaxyRepositoryReflection { + + #region Descriptor + /// File descriptor for galaxy_repository.proto + public static pbr::FileDescriptor Descriptor { + get { return descriptor; } + } + private static pbr::FileDescriptor descriptor; + + static GalaxyRepositoryReflection() { + byte[] descriptorData = global::System.Convert.FromBase64String( + string.Concat( + "ChdnYWxheHlfcmVwb3NpdG9yeS5wcm90bxIUZ2FsYXh5X3JlcG9zaXRvcnku", + "djEaH2dvb2dsZS9wcm90b2J1Zi90aW1lc3RhbXAucHJvdG8iFwoVVGVzdENv", + "bm5lY3Rpb25SZXF1ZXN0IiEKE1Rlc3RDb25uZWN0aW9uUmVwbHkSCgoCb2sY", + "ASABKAgiGgoYR2V0TGFzdERlcGxveVRpbWVSZXF1ZXN0ImIKFkdldExhc3RE", + "ZXBsb3lUaW1lUmVwbHkSDwoHcHJlc2VudBgBIAEoCBI3ChN0aW1lX29mX2xh", + "c3RfZGVwbG95GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCIa", + "ChhEaXNjb3ZlckhpZXJhcmNoeVJlcXVlc3QiTQoWRGlzY292ZXJIaWVyYXJj", + "aHlSZXBseRIzCgdvYmplY3RzGAEgAygLMiIuZ2FsYXh5X3JlcG9zaXRvcnku", + "djEuR2FsYXh5T2JqZWN0IlUKGFdhdGNoRGVwbG95RXZlbnRzUmVxdWVzdBI5", + "ChVsYXN0X3NlZW5fZGVwbG95X3RpbWUYASABKAsyGi5nb29nbGUucHJvdG9i", + "dWYuVGltZXN0YW1wIt0BCgtEZXBsb3lFdmVudBIQCghzZXF1ZW5jZRgBIAEo", + "BBIvCgtvYnNlcnZlZF9hdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1l", + "c3RhbXASNwoTdGltZV9vZl9sYXN0X2RlcGxveRgDIAEoCzIaLmdvb2dsZS5w", + "cm90b2J1Zi5UaW1lc3RhbXASIwobdGltZV9vZl9sYXN0X2RlcGxveV9wcmVz", + "ZW50GAQgASgIEhQKDG9iamVjdF9jb3VudBgFIAEoBRIXCg9hdHRyaWJ1dGVf", + "Y291bnQYBiABKAUikwIKDEdhbGF4eU9iamVjdBISCgpnb2JqZWN0X2lkGAEg", + "ASgFEhAKCHRhZ19uYW1lGAIgASgJEhYKDmNvbnRhaW5lZF9uYW1lGAMgASgJ", + "EhMKC2Jyb3dzZV9uYW1lGAQgASgJEhkKEXBhcmVudF9nb2JqZWN0X2lkGAUg", + "ASgFEg8KB2lzX2FyZWEYBiABKAgSEwoLY2F0ZWdvcnlfaWQYByABKAUSHAoU", + "aG9zdGVkX2J5X2dvYmplY3RfaWQYCCABKAUSFgoOdGVtcGxhdGVfY2hhaW4Y", + "CSADKAkSOQoKYXR0cmlidXRlcxgKIAMoCzIlLmdhbGF4eV9yZXBvc2l0b3J5", + "LnYxLkdhbGF4eUF0dHJpYnV0ZSKoAgoPR2FsYXh5QXR0cmlidXRlEhYKDmF0", + "dHJpYnV0ZV9uYW1lGAEgASgJEhoKEmZ1bGxfdGFnX3JlZmVyZW5jZRgCIAEo", + "CRIUCgxteF9kYXRhX3R5cGUYAyABKAUSFgoOZGF0YV90eXBlX25hbWUYBCAB", + "KAkSEAoIaXNfYXJyYXkYBSABKAgSFwoPYXJyYXlfZGltZW5zaW9uGAYgASgF", + "Eh8KF2FycmF5X2RpbWVuc2lvbl9wcmVzZW50GAcgASgIEh0KFW14X2F0dHJp", + "YnV0ZV9jYXRlZ29yeRgIIAEoBRIfChdzZWN1cml0eV9jbGFzc2lmaWNhdGlv", + "bhgJIAEoBRIVCg1pc19oaXN0b3JpemVkGAogASgIEhAKCGlzX2FsYXJtGAsg", + "ASgIMswDChBHYWxheHlSZXBvc2l0b3J5EmgKDlRlc3RDb25uZWN0aW9uEisu", + "Z2FsYXh5X3JlcG9zaXRvcnkudjEuVGVzdENvbm5lY3Rpb25SZXF1ZXN0Giku", + "Z2FsYXh5X3JlcG9zaXRvcnkudjEuVGVzdENvbm5lY3Rpb25SZXBseRJxChFH", + "ZXRMYXN0RGVwbG95VGltZRIuLmdhbGF4eV9yZXBvc2l0b3J5LnYxLkdldExh", + "c3REZXBsb3lUaW1lUmVxdWVzdBosLmdhbGF4eV9yZXBvc2l0b3J5LnYxLkdl", + "dExhc3REZXBsb3lUaW1lUmVwbHkScQoRRGlzY292ZXJIaWVyYXJjaHkSLi5n", + "YWxheHlfcmVwb3NpdG9yeS52MS5EaXNjb3ZlckhpZXJhcmNoeVJlcXVlc3Qa", + "LC5nYWxheHlfcmVwb3NpdG9yeS52MS5EaXNjb3ZlckhpZXJhcmNoeVJlcGx5", + "EmgKEVdhdGNoRGVwbG95RXZlbnRzEi4uZ2FsYXh5X3JlcG9zaXRvcnkudjEu", + "V2F0Y2hEZXBsb3lFdmVudHNSZXF1ZXN0GiEuZ2FsYXh5X3JlcG9zaXRvcnku", + "djEuRGVwbG95RXZlbnQwAUIjqgIgTXhHYXRld2F5LkNvbnRyYWN0cy5Qcm90", + "by5HYWxheHliBnByb3RvMw==")); + descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, + new pbr::FileDescriptor[] { global::Google.Protobuf.WellKnownTypes.TimestampReflection.Descriptor, }, + new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo[] { + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest), global::MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest.Parser, null, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.TestConnectionReply), global::MxGateway.Contracts.Proto.Galaxy.TestConnectionReply.Parser, new[]{ "Ok" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest), global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest.Parser, null, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply), global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply.Parser, new[]{ "Present", "TimeOfLastDeploy" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest), global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest.Parser, null, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply), global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply.Parser, new[]{ "Objects" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest), global::MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest.Parser, new[]{ "LastSeenDeployTime" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.DeployEvent), global::MxGateway.Contracts.Proto.Galaxy.DeployEvent.Parser, new[]{ "Sequence", "ObservedAt", "TimeOfLastDeploy", "TimeOfLastDeployPresent", "ObjectCount", "AttributeCount" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.GalaxyObject), global::MxGateway.Contracts.Proto.Galaxy.GalaxyObject.Parser, new[]{ "GobjectId", "TagName", "ContainedName", "BrowseName", "ParentGobjectId", "IsArea", "CategoryId", "HostedByGobjectId", "TemplateChain", "Attributes" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.GalaxyAttribute), global::MxGateway.Contracts.Proto.Galaxy.GalaxyAttribute.Parser, new[]{ "AttributeName", "FullTagReference", "MxDataType", "DataTypeName", "IsArray", "ArrayDimension", "ArrayDimensionPresent", "MxAttributeCategory", "SecurityClassification", "IsHistorized", "IsAlarm" }, null, null, null, null) + })); + } + #endregion + + } + #region Messages + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class TestConnectionRequest : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new TestConnectionRequest()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[0]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public TestConnectionRequest() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public TestConnectionRequest(TestConnectionRequest other) : this() { + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public TestConnectionRequest Clone() { + return new TestConnectionRequest(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as TestConnectionRequest); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(TestConnectionRequest other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(TestConnectionRequest other) { + if (other == null) { + return; + } + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + } + } + } + #endif + + } + + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class TestConnectionReply : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new TestConnectionReply()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[1]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public TestConnectionReply() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public TestConnectionReply(TestConnectionReply other) : this() { + ok_ = other.ok_; + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public TestConnectionReply Clone() { + return new TestConnectionReply(this); + } + + /// Field number for the "ok" field. + public const int OkFieldNumber = 1; + private bool ok_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Ok { + get { return ok_; } + set { + ok_ = value; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as TestConnectionReply); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(TestConnectionReply other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (Ok != other.Ok) return false; + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + if (Ok != false) hash ^= Ok.GetHashCode(); + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + if (Ok != false) { + output.WriteRawTag(8); + output.WriteBool(Ok); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (Ok != false) { + output.WriteRawTag(8); + output.WriteBool(Ok); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + if (Ok != false) { + size += 1 + 1; + } + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(TestConnectionReply other) { + if (other == null) { + return; + } + if (other.Ok != false) { + Ok = other.Ok; + } + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + case 8: { + Ok = input.ReadBool(); + break; + } + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + case 8: { + Ok = input.ReadBool(); + break; + } + } + } + } + #endif + + } + + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class GetLastDeployTimeRequest : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new GetLastDeployTimeRequest()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[2]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public GetLastDeployTimeRequest() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public GetLastDeployTimeRequest(GetLastDeployTimeRequest other) : this() { + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public GetLastDeployTimeRequest Clone() { + return new GetLastDeployTimeRequest(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as GetLastDeployTimeRequest); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(GetLastDeployTimeRequest other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(GetLastDeployTimeRequest other) { + if (other == null) { + return; + } + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + } + } + } + #endif + + } + + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class GetLastDeployTimeReply : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new GetLastDeployTimeReply()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[3]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public GetLastDeployTimeReply() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public GetLastDeployTimeReply(GetLastDeployTimeReply other) : this() { + present_ = other.present_; + timeOfLastDeploy_ = other.timeOfLastDeploy_ != null ? other.timeOfLastDeploy_.Clone() : null; + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public GetLastDeployTimeReply Clone() { + return new GetLastDeployTimeReply(this); + } + + /// Field number for the "present" field. + public const int PresentFieldNumber = 1; + private bool present_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Present { + get { return present_; } + set { + present_ = value; + } + } + + /// Field number for the "time_of_last_deploy" field. + public const int TimeOfLastDeployFieldNumber = 2; + private global::Google.Protobuf.WellKnownTypes.Timestamp timeOfLastDeploy_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Google.Protobuf.WellKnownTypes.Timestamp TimeOfLastDeploy { + get { return timeOfLastDeploy_; } + set { + timeOfLastDeploy_ = value; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as GetLastDeployTimeReply); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(GetLastDeployTimeReply other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (Present != other.Present) return false; + if (!object.Equals(TimeOfLastDeploy, other.TimeOfLastDeploy)) return false; + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + if (Present != false) hash ^= Present.GetHashCode(); + if (timeOfLastDeploy_ != null) hash ^= TimeOfLastDeploy.GetHashCode(); + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + if (Present != false) { + output.WriteRawTag(8); + output.WriteBool(Present); + } + if (timeOfLastDeploy_ != null) { + output.WriteRawTag(18); + output.WriteMessage(TimeOfLastDeploy); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (Present != false) { + output.WriteRawTag(8); + output.WriteBool(Present); + } + if (timeOfLastDeploy_ != null) { + output.WriteRawTag(18); + output.WriteMessage(TimeOfLastDeploy); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + if (Present != false) { + size += 1 + 1; + } + if (timeOfLastDeploy_ != null) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(TimeOfLastDeploy); + } + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(GetLastDeployTimeReply other) { + if (other == null) { + return; + } + if (other.Present != false) { + Present = other.Present; + } + if (other.timeOfLastDeploy_ != null) { + if (timeOfLastDeploy_ == null) { + TimeOfLastDeploy = new global::Google.Protobuf.WellKnownTypes.Timestamp(); + } + TimeOfLastDeploy.MergeFrom(other.TimeOfLastDeploy); + } + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + case 8: { + Present = input.ReadBool(); + break; + } + case 18: { + if (timeOfLastDeploy_ == null) { + TimeOfLastDeploy = new global::Google.Protobuf.WellKnownTypes.Timestamp(); + } + input.ReadMessage(TimeOfLastDeploy); + break; + } + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + case 8: { + Present = input.ReadBool(); + break; + } + case 18: { + if (timeOfLastDeploy_ == null) { + TimeOfLastDeploy = new global::Google.Protobuf.WellKnownTypes.Timestamp(); + } + input.ReadMessage(TimeOfLastDeploy); + break; + } + } + } + } + #endif + + } + + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class DiscoverHierarchyRequest : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new DiscoverHierarchyRequest()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[4]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public DiscoverHierarchyRequest() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public DiscoverHierarchyRequest(DiscoverHierarchyRequest other) : this() { + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public DiscoverHierarchyRequest Clone() { + return new DiscoverHierarchyRequest(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as DiscoverHierarchyRequest); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(DiscoverHierarchyRequest other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(DiscoverHierarchyRequest other) { + if (other == null) { + return; + } + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + } + } + } + #endif + + } + + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class DiscoverHierarchyReply : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new DiscoverHierarchyReply()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[5]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public DiscoverHierarchyReply() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public DiscoverHierarchyReply(DiscoverHierarchyReply other) : this() { + objects_ = other.objects_.Clone(); + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public DiscoverHierarchyReply Clone() { + return new DiscoverHierarchyReply(this); + } + + /// Field number for the "objects" field. + public const int ObjectsFieldNumber = 1; + private static readonly pb::FieldCodec _repeated_objects_codec + = pb::FieldCodec.ForMessage(10, global::MxGateway.Contracts.Proto.Galaxy.GalaxyObject.Parser); + private readonly pbc::RepeatedField objects_ = new pbc::RepeatedField(); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public pbc::RepeatedField Objects { + get { return objects_; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as DiscoverHierarchyReply); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(DiscoverHierarchyReply other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if(!objects_.Equals(other.objects_)) return false; + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + hash ^= objects_.GetHashCode(); + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + objects_.WriteTo(output, _repeated_objects_codec); + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + objects_.WriteTo(ref output, _repeated_objects_codec); + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + size += objects_.CalculateSize(_repeated_objects_codec); + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(DiscoverHierarchyReply other) { + if (other == null) { + return; + } + objects_.Add(other.objects_); + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + case 10: { + objects_.AddEntriesFrom(input, _repeated_objects_codec); + break; + } + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + case 10: { + objects_.AddEntriesFrom(ref input, _repeated_objects_codec); + break; + } + } + } + } + #endif + + } + + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class WatchDeployEventsRequest : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new WatchDeployEventsRequest()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[6]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public WatchDeployEventsRequest() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public WatchDeployEventsRequest(WatchDeployEventsRequest other) : this() { + lastSeenDeployTime_ = other.lastSeenDeployTime_ != null ? other.lastSeenDeployTime_.Clone() : null; + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public WatchDeployEventsRequest Clone() { + return new WatchDeployEventsRequest(this); + } + + /// Field number for the "last_seen_deploy_time" field. + public const int LastSeenDeployTimeFieldNumber = 1; + private global::Google.Protobuf.WellKnownTypes.Timestamp lastSeenDeployTime_; + /// + /// Optional. When set, the bootstrap event is suppressed if the cached deploy + /// time matches this value. Future events are still emitted normally. + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Google.Protobuf.WellKnownTypes.Timestamp LastSeenDeployTime { + get { return lastSeenDeployTime_; } + set { + lastSeenDeployTime_ = value; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as WatchDeployEventsRequest); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(WatchDeployEventsRequest other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (!object.Equals(LastSeenDeployTime, other.LastSeenDeployTime)) return false; + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + if (lastSeenDeployTime_ != null) hash ^= LastSeenDeployTime.GetHashCode(); + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + if (lastSeenDeployTime_ != null) { + output.WriteRawTag(10); + output.WriteMessage(LastSeenDeployTime); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (lastSeenDeployTime_ != null) { + output.WriteRawTag(10); + output.WriteMessage(LastSeenDeployTime); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + if (lastSeenDeployTime_ != null) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(LastSeenDeployTime); + } + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(WatchDeployEventsRequest other) { + if (other == null) { + return; + } + if (other.lastSeenDeployTime_ != null) { + if (lastSeenDeployTime_ == null) { + LastSeenDeployTime = new global::Google.Protobuf.WellKnownTypes.Timestamp(); + } + LastSeenDeployTime.MergeFrom(other.LastSeenDeployTime); + } + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + case 10: { + if (lastSeenDeployTime_ == null) { + LastSeenDeployTime = new global::Google.Protobuf.WellKnownTypes.Timestamp(); + } + input.ReadMessage(LastSeenDeployTime); + break; + } + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + case 10: { + if (lastSeenDeployTime_ == null) { + LastSeenDeployTime = new global::Google.Protobuf.WellKnownTypes.Timestamp(); + } + input.ReadMessage(LastSeenDeployTime); + break; + } + } + } + } + #endif + + } + + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class DeployEvent : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new DeployEvent()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[7]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public DeployEvent() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public DeployEvent(DeployEvent other) : this() { + sequence_ = other.sequence_; + observedAt_ = other.observedAt_ != null ? other.observedAt_.Clone() : null; + timeOfLastDeploy_ = other.timeOfLastDeploy_ != null ? other.timeOfLastDeploy_.Clone() : null; + timeOfLastDeployPresent_ = other.timeOfLastDeployPresent_; + objectCount_ = other.objectCount_; + attributeCount_ = other.attributeCount_; + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public DeployEvent Clone() { + return new DeployEvent(this); + } + + /// Field number for the "sequence" field. + public const int SequenceFieldNumber = 1; + private ulong sequence_; + /// + /// Monotonically increasing per server start. Gaps indicate dropped events. + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public ulong Sequence { + get { return sequence_; } + set { + sequence_ = value; + } + } + + /// Field number for the "observed_at" field. + public const int ObservedAtFieldNumber = 2; + private global::Google.Protobuf.WellKnownTypes.Timestamp observedAt_; + /// + /// Server wall-clock when the cache observed the deploy. + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Google.Protobuf.WellKnownTypes.Timestamp ObservedAt { + get { return observedAt_; } + set { + observedAt_ = value; + } + } + + /// Field number for the "time_of_last_deploy" field. + public const int TimeOfLastDeployFieldNumber = 3; + private global::Google.Protobuf.WellKnownTypes.Timestamp timeOfLastDeploy_; + /// + /// Galaxy.time_of_last_deploy. Absent only when the Galaxy table reports null. + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Google.Protobuf.WellKnownTypes.Timestamp TimeOfLastDeploy { + get { return timeOfLastDeploy_; } + set { + timeOfLastDeploy_ = value; + } + } + + /// Field number for the "time_of_last_deploy_present" field. + public const int TimeOfLastDeployPresentFieldNumber = 4; + private bool timeOfLastDeployPresent_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool TimeOfLastDeployPresent { + get { return timeOfLastDeployPresent_; } + set { + timeOfLastDeployPresent_ = value; + } + } + + /// Field number for the "object_count" field. + public const int ObjectCountFieldNumber = 5; + private int objectCount_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int ObjectCount { + get { return objectCount_; } + set { + objectCount_ = value; + } + } + + /// Field number for the "attribute_count" field. + public const int AttributeCountFieldNumber = 6; + private int attributeCount_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int AttributeCount { + get { return attributeCount_; } + set { + attributeCount_ = value; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as DeployEvent); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(DeployEvent other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (Sequence != other.Sequence) return false; + if (!object.Equals(ObservedAt, other.ObservedAt)) return false; + if (!object.Equals(TimeOfLastDeploy, other.TimeOfLastDeploy)) return false; + if (TimeOfLastDeployPresent != other.TimeOfLastDeployPresent) return false; + if (ObjectCount != other.ObjectCount) return false; + if (AttributeCount != other.AttributeCount) return false; + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + if (Sequence != 0UL) hash ^= Sequence.GetHashCode(); + if (observedAt_ != null) hash ^= ObservedAt.GetHashCode(); + if (timeOfLastDeploy_ != null) hash ^= TimeOfLastDeploy.GetHashCode(); + if (TimeOfLastDeployPresent != false) hash ^= TimeOfLastDeployPresent.GetHashCode(); + if (ObjectCount != 0) hash ^= ObjectCount.GetHashCode(); + if (AttributeCount != 0) hash ^= AttributeCount.GetHashCode(); + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + if (Sequence != 0UL) { + output.WriteRawTag(8); + output.WriteUInt64(Sequence); + } + if (observedAt_ != null) { + output.WriteRawTag(18); + output.WriteMessage(ObservedAt); + } + if (timeOfLastDeploy_ != null) { + output.WriteRawTag(26); + output.WriteMessage(TimeOfLastDeploy); + } + if (TimeOfLastDeployPresent != false) { + output.WriteRawTag(32); + output.WriteBool(TimeOfLastDeployPresent); + } + if (ObjectCount != 0) { + output.WriteRawTag(40); + output.WriteInt32(ObjectCount); + } + if (AttributeCount != 0) { + output.WriteRawTag(48); + output.WriteInt32(AttributeCount); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (Sequence != 0UL) { + output.WriteRawTag(8); + output.WriteUInt64(Sequence); + } + if (observedAt_ != null) { + output.WriteRawTag(18); + output.WriteMessage(ObservedAt); + } + if (timeOfLastDeploy_ != null) { + output.WriteRawTag(26); + output.WriteMessage(TimeOfLastDeploy); + } + if (TimeOfLastDeployPresent != false) { + output.WriteRawTag(32); + output.WriteBool(TimeOfLastDeployPresent); + } + if (ObjectCount != 0) { + output.WriteRawTag(40); + output.WriteInt32(ObjectCount); + } + if (AttributeCount != 0) { + output.WriteRawTag(48); + output.WriteInt32(AttributeCount); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + if (Sequence != 0UL) { + size += 1 + pb::CodedOutputStream.ComputeUInt64Size(Sequence); + } + if (observedAt_ != null) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(ObservedAt); + } + if (timeOfLastDeploy_ != null) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(TimeOfLastDeploy); + } + if (TimeOfLastDeployPresent != false) { + size += 1 + 1; + } + if (ObjectCount != 0) { + size += 1 + pb::CodedOutputStream.ComputeInt32Size(ObjectCount); + } + if (AttributeCount != 0) { + size += 1 + pb::CodedOutputStream.ComputeInt32Size(AttributeCount); + } + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(DeployEvent other) { + if (other == null) { + return; + } + if (other.Sequence != 0UL) { + Sequence = other.Sequence; + } + if (other.observedAt_ != null) { + if (observedAt_ == null) { + ObservedAt = new global::Google.Protobuf.WellKnownTypes.Timestamp(); + } + ObservedAt.MergeFrom(other.ObservedAt); + } + if (other.timeOfLastDeploy_ != null) { + if (timeOfLastDeploy_ == null) { + TimeOfLastDeploy = new global::Google.Protobuf.WellKnownTypes.Timestamp(); + } + TimeOfLastDeploy.MergeFrom(other.TimeOfLastDeploy); + } + if (other.TimeOfLastDeployPresent != false) { + TimeOfLastDeployPresent = other.TimeOfLastDeployPresent; + } + if (other.ObjectCount != 0) { + ObjectCount = other.ObjectCount; + } + if (other.AttributeCount != 0) { + AttributeCount = other.AttributeCount; + } + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + case 8: { + Sequence = input.ReadUInt64(); + break; + } + case 18: { + if (observedAt_ == null) { + ObservedAt = new global::Google.Protobuf.WellKnownTypes.Timestamp(); + } + input.ReadMessage(ObservedAt); + break; + } + case 26: { + if (timeOfLastDeploy_ == null) { + TimeOfLastDeploy = new global::Google.Protobuf.WellKnownTypes.Timestamp(); + } + input.ReadMessage(TimeOfLastDeploy); + break; + } + case 32: { + TimeOfLastDeployPresent = input.ReadBool(); + break; + } + case 40: { + ObjectCount = input.ReadInt32(); + break; + } + case 48: { + AttributeCount = input.ReadInt32(); + break; + } + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + case 8: { + Sequence = input.ReadUInt64(); + break; + } + case 18: { + if (observedAt_ == null) { + ObservedAt = new global::Google.Protobuf.WellKnownTypes.Timestamp(); + } + input.ReadMessage(ObservedAt); + break; + } + case 26: { + if (timeOfLastDeploy_ == null) { + TimeOfLastDeploy = new global::Google.Protobuf.WellKnownTypes.Timestamp(); + } + input.ReadMessage(TimeOfLastDeploy); + break; + } + case 32: { + TimeOfLastDeployPresent = input.ReadBool(); + break; + } + case 40: { + ObjectCount = input.ReadInt32(); + break; + } + case 48: { + AttributeCount = input.ReadInt32(); + break; + } + } + } + } + #endif + + } + + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class GalaxyObject : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new GalaxyObject()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[8]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public GalaxyObject() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public GalaxyObject(GalaxyObject other) : this() { + gobjectId_ = other.gobjectId_; + tagName_ = other.tagName_; + containedName_ = other.containedName_; + browseName_ = other.browseName_; + parentGobjectId_ = other.parentGobjectId_; + isArea_ = other.isArea_; + categoryId_ = other.categoryId_; + hostedByGobjectId_ = other.hostedByGobjectId_; + templateChain_ = other.templateChain_.Clone(); + attributes_ = other.attributes_.Clone(); + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public GalaxyObject Clone() { + return new GalaxyObject(this); + } + + /// Field number for the "gobject_id" field. + public const int GobjectIdFieldNumber = 1; + private int gobjectId_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int GobjectId { + get { return gobjectId_; } + set { + gobjectId_ = value; + } + } + + /// Field number for the "tag_name" field. + public const int TagNameFieldNumber = 2; + private string tagName_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string TagName { + get { return tagName_; } + set { + tagName_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "contained_name" field. + public const int ContainedNameFieldNumber = 3; + private string containedName_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string ContainedName { + get { return containedName_; } + set { + containedName_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "browse_name" field. + public const int BrowseNameFieldNumber = 4; + private string browseName_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string BrowseName { + get { return browseName_; } + set { + browseName_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "parent_gobject_id" field. + public const int ParentGobjectIdFieldNumber = 5; + private int parentGobjectId_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int ParentGobjectId { + get { return parentGobjectId_; } + set { + parentGobjectId_ = value; + } + } + + /// Field number for the "is_area" field. + public const int IsAreaFieldNumber = 6; + private bool isArea_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool IsArea { + get { return isArea_; } + set { + isArea_ = value; + } + } + + /// Field number for the "category_id" field. + public const int CategoryIdFieldNumber = 7; + private int categoryId_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CategoryId { + get { return categoryId_; } + set { + categoryId_ = value; + } + } + + /// Field number for the "hosted_by_gobject_id" field. + public const int HostedByGobjectIdFieldNumber = 8; + private int hostedByGobjectId_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int HostedByGobjectId { + get { return hostedByGobjectId_; } + set { + hostedByGobjectId_ = value; + } + } + + /// Field number for the "template_chain" field. + public const int TemplateChainFieldNumber = 9; + private static readonly pb::FieldCodec _repeated_templateChain_codec + = pb::FieldCodec.ForString(74); + private readonly pbc::RepeatedField templateChain_ = new pbc::RepeatedField(); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public pbc::RepeatedField TemplateChain { + get { return templateChain_; } + } + + /// Field number for the "attributes" field. + public const int AttributesFieldNumber = 10; + private static readonly pb::FieldCodec _repeated_attributes_codec + = pb::FieldCodec.ForMessage(82, global::MxGateway.Contracts.Proto.Galaxy.GalaxyAttribute.Parser); + private readonly pbc::RepeatedField attributes_ = new pbc::RepeatedField(); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public pbc::RepeatedField Attributes { + get { return attributes_; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as GalaxyObject); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(GalaxyObject other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (GobjectId != other.GobjectId) return false; + if (TagName != other.TagName) return false; + if (ContainedName != other.ContainedName) return false; + if (BrowseName != other.BrowseName) return false; + if (ParentGobjectId != other.ParentGobjectId) return false; + if (IsArea != other.IsArea) return false; + if (CategoryId != other.CategoryId) return false; + if (HostedByGobjectId != other.HostedByGobjectId) return false; + if(!templateChain_.Equals(other.templateChain_)) return false; + if(!attributes_.Equals(other.attributes_)) return false; + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + if (GobjectId != 0) hash ^= GobjectId.GetHashCode(); + if (TagName.Length != 0) hash ^= TagName.GetHashCode(); + if (ContainedName.Length != 0) hash ^= ContainedName.GetHashCode(); + if (BrowseName.Length != 0) hash ^= BrowseName.GetHashCode(); + if (ParentGobjectId != 0) hash ^= ParentGobjectId.GetHashCode(); + if (IsArea != false) hash ^= IsArea.GetHashCode(); + if (CategoryId != 0) hash ^= CategoryId.GetHashCode(); + if (HostedByGobjectId != 0) hash ^= HostedByGobjectId.GetHashCode(); + hash ^= templateChain_.GetHashCode(); + hash ^= attributes_.GetHashCode(); + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + if (GobjectId != 0) { + output.WriteRawTag(8); + output.WriteInt32(GobjectId); + } + if (TagName.Length != 0) { + output.WriteRawTag(18); + output.WriteString(TagName); + } + if (ContainedName.Length != 0) { + output.WriteRawTag(26); + output.WriteString(ContainedName); + } + if (BrowseName.Length != 0) { + output.WriteRawTag(34); + output.WriteString(BrowseName); + } + if (ParentGobjectId != 0) { + output.WriteRawTag(40); + output.WriteInt32(ParentGobjectId); + } + if (IsArea != false) { + output.WriteRawTag(48); + output.WriteBool(IsArea); + } + if (CategoryId != 0) { + output.WriteRawTag(56); + output.WriteInt32(CategoryId); + } + if (HostedByGobjectId != 0) { + output.WriteRawTag(64); + output.WriteInt32(HostedByGobjectId); + } + templateChain_.WriteTo(output, _repeated_templateChain_codec); + attributes_.WriteTo(output, _repeated_attributes_codec); + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (GobjectId != 0) { + output.WriteRawTag(8); + output.WriteInt32(GobjectId); + } + if (TagName.Length != 0) { + output.WriteRawTag(18); + output.WriteString(TagName); + } + if (ContainedName.Length != 0) { + output.WriteRawTag(26); + output.WriteString(ContainedName); + } + if (BrowseName.Length != 0) { + output.WriteRawTag(34); + output.WriteString(BrowseName); + } + if (ParentGobjectId != 0) { + output.WriteRawTag(40); + output.WriteInt32(ParentGobjectId); + } + if (IsArea != false) { + output.WriteRawTag(48); + output.WriteBool(IsArea); + } + if (CategoryId != 0) { + output.WriteRawTag(56); + output.WriteInt32(CategoryId); + } + if (HostedByGobjectId != 0) { + output.WriteRawTag(64); + output.WriteInt32(HostedByGobjectId); + } + templateChain_.WriteTo(ref output, _repeated_templateChain_codec); + attributes_.WriteTo(ref output, _repeated_attributes_codec); + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + if (GobjectId != 0) { + size += 1 + pb::CodedOutputStream.ComputeInt32Size(GobjectId); + } + if (TagName.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(TagName); + } + if (ContainedName.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(ContainedName); + } + if (BrowseName.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(BrowseName); + } + if (ParentGobjectId != 0) { + size += 1 + pb::CodedOutputStream.ComputeInt32Size(ParentGobjectId); + } + if (IsArea != false) { + size += 1 + 1; + } + if (CategoryId != 0) { + size += 1 + pb::CodedOutputStream.ComputeInt32Size(CategoryId); + } + if (HostedByGobjectId != 0) { + size += 1 + pb::CodedOutputStream.ComputeInt32Size(HostedByGobjectId); + } + size += templateChain_.CalculateSize(_repeated_templateChain_codec); + size += attributes_.CalculateSize(_repeated_attributes_codec); + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(GalaxyObject other) { + if (other == null) { + return; + } + if (other.GobjectId != 0) { + GobjectId = other.GobjectId; + } + if (other.TagName.Length != 0) { + TagName = other.TagName; + } + if (other.ContainedName.Length != 0) { + ContainedName = other.ContainedName; + } + if (other.BrowseName.Length != 0) { + BrowseName = other.BrowseName; + } + if (other.ParentGobjectId != 0) { + ParentGobjectId = other.ParentGobjectId; + } + if (other.IsArea != false) { + IsArea = other.IsArea; + } + if (other.CategoryId != 0) { + CategoryId = other.CategoryId; + } + if (other.HostedByGobjectId != 0) { + HostedByGobjectId = other.HostedByGobjectId; + } + templateChain_.Add(other.templateChain_); + attributes_.Add(other.attributes_); + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + case 8: { + GobjectId = input.ReadInt32(); + break; + } + case 18: { + TagName = input.ReadString(); + break; + } + case 26: { + ContainedName = input.ReadString(); + break; + } + case 34: { + BrowseName = input.ReadString(); + break; + } + case 40: { + ParentGobjectId = input.ReadInt32(); + break; + } + case 48: { + IsArea = input.ReadBool(); + break; + } + case 56: { + CategoryId = input.ReadInt32(); + break; + } + case 64: { + HostedByGobjectId = input.ReadInt32(); + break; + } + case 74: { + templateChain_.AddEntriesFrom(input, _repeated_templateChain_codec); + break; + } + case 82: { + attributes_.AddEntriesFrom(input, _repeated_attributes_codec); + break; + } + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + case 8: { + GobjectId = input.ReadInt32(); + break; + } + case 18: { + TagName = input.ReadString(); + break; + } + case 26: { + ContainedName = input.ReadString(); + break; + } + case 34: { + BrowseName = input.ReadString(); + break; + } + case 40: { + ParentGobjectId = input.ReadInt32(); + break; + } + case 48: { + IsArea = input.ReadBool(); + break; + } + case 56: { + CategoryId = input.ReadInt32(); + break; + } + case 64: { + HostedByGobjectId = input.ReadInt32(); + break; + } + case 74: { + templateChain_.AddEntriesFrom(ref input, _repeated_templateChain_codec); + break; + } + case 82: { + attributes_.AddEntriesFrom(ref input, _repeated_attributes_codec); + break; + } + } + } + } + #endif + + } + + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class GalaxyAttribute : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new GalaxyAttribute()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[9]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public GalaxyAttribute() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public GalaxyAttribute(GalaxyAttribute other) : this() { + attributeName_ = other.attributeName_; + fullTagReference_ = other.fullTagReference_; + mxDataType_ = other.mxDataType_; + dataTypeName_ = other.dataTypeName_; + isArray_ = other.isArray_; + arrayDimension_ = other.arrayDimension_; + arrayDimensionPresent_ = other.arrayDimensionPresent_; + mxAttributeCategory_ = other.mxAttributeCategory_; + securityClassification_ = other.securityClassification_; + isHistorized_ = other.isHistorized_; + isAlarm_ = other.isAlarm_; + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public GalaxyAttribute Clone() { + return new GalaxyAttribute(this); + } + + /// Field number for the "attribute_name" field. + public const int AttributeNameFieldNumber = 1; + private string attributeName_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string AttributeName { + get { return attributeName_; } + set { + attributeName_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "full_tag_reference" field. + public const int FullTagReferenceFieldNumber = 2; + private string fullTagReference_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string FullTagReference { + get { return fullTagReference_; } + set { + fullTagReference_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "mx_data_type" field. + public const int MxDataTypeFieldNumber = 3; + private int mxDataType_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int MxDataType { + get { return mxDataType_; } + set { + mxDataType_ = value; + } + } + + /// Field number for the "data_type_name" field. + public const int DataTypeNameFieldNumber = 4; + private string dataTypeName_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string DataTypeName { + get { return dataTypeName_; } + set { + dataTypeName_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "is_array" field. + public const int IsArrayFieldNumber = 5; + private bool isArray_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool IsArray { + get { return isArray_; } + set { + isArray_ = value; + } + } + + /// Field number for the "array_dimension" field. + public const int ArrayDimensionFieldNumber = 6; + private int arrayDimension_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int ArrayDimension { + get { return arrayDimension_; } + set { + arrayDimension_ = value; + } + } + + /// Field number for the "array_dimension_present" field. + public const int ArrayDimensionPresentFieldNumber = 7; + private bool arrayDimensionPresent_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool ArrayDimensionPresent { + get { return arrayDimensionPresent_; } + set { + arrayDimensionPresent_ = value; + } + } + + /// Field number for the "mx_attribute_category" field. + public const int MxAttributeCategoryFieldNumber = 8; + private int mxAttributeCategory_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int MxAttributeCategory { + get { return mxAttributeCategory_; } + set { + mxAttributeCategory_ = value; + } + } + + /// Field number for the "security_classification" field. + public const int SecurityClassificationFieldNumber = 9; + private int securityClassification_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int SecurityClassification { + get { return securityClassification_; } + set { + securityClassification_ = value; + } + } + + /// Field number for the "is_historized" field. + public const int IsHistorizedFieldNumber = 10; + private bool isHistorized_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool IsHistorized { + get { return isHistorized_; } + set { + isHistorized_ = value; + } + } + + /// Field number for the "is_alarm" field. + public const int IsAlarmFieldNumber = 11; + private bool isAlarm_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool IsAlarm { + get { return isAlarm_; } + set { + isAlarm_ = value; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as GalaxyAttribute); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(GalaxyAttribute other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (AttributeName != other.AttributeName) return false; + if (FullTagReference != other.FullTagReference) return false; + if (MxDataType != other.MxDataType) return false; + if (DataTypeName != other.DataTypeName) return false; + if (IsArray != other.IsArray) return false; + if (ArrayDimension != other.ArrayDimension) return false; + if (ArrayDimensionPresent != other.ArrayDimensionPresent) return false; + if (MxAttributeCategory != other.MxAttributeCategory) return false; + if (SecurityClassification != other.SecurityClassification) return false; + if (IsHistorized != other.IsHistorized) return false; + if (IsAlarm != other.IsAlarm) return false; + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + if (AttributeName.Length != 0) hash ^= AttributeName.GetHashCode(); + if (FullTagReference.Length != 0) hash ^= FullTagReference.GetHashCode(); + if (MxDataType != 0) hash ^= MxDataType.GetHashCode(); + if (DataTypeName.Length != 0) hash ^= DataTypeName.GetHashCode(); + if (IsArray != false) hash ^= IsArray.GetHashCode(); + if (ArrayDimension != 0) hash ^= ArrayDimension.GetHashCode(); + if (ArrayDimensionPresent != false) hash ^= ArrayDimensionPresent.GetHashCode(); + if (MxAttributeCategory != 0) hash ^= MxAttributeCategory.GetHashCode(); + if (SecurityClassification != 0) hash ^= SecurityClassification.GetHashCode(); + if (IsHistorized != false) hash ^= IsHistorized.GetHashCode(); + if (IsAlarm != false) hash ^= IsAlarm.GetHashCode(); + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + if (AttributeName.Length != 0) { + output.WriteRawTag(10); + output.WriteString(AttributeName); + } + if (FullTagReference.Length != 0) { + output.WriteRawTag(18); + output.WriteString(FullTagReference); + } + if (MxDataType != 0) { + output.WriteRawTag(24); + output.WriteInt32(MxDataType); + } + if (DataTypeName.Length != 0) { + output.WriteRawTag(34); + output.WriteString(DataTypeName); + } + if (IsArray != false) { + output.WriteRawTag(40); + output.WriteBool(IsArray); + } + if (ArrayDimension != 0) { + output.WriteRawTag(48); + output.WriteInt32(ArrayDimension); + } + if (ArrayDimensionPresent != false) { + output.WriteRawTag(56); + output.WriteBool(ArrayDimensionPresent); + } + if (MxAttributeCategory != 0) { + output.WriteRawTag(64); + output.WriteInt32(MxAttributeCategory); + } + if (SecurityClassification != 0) { + output.WriteRawTag(72); + output.WriteInt32(SecurityClassification); + } + if (IsHistorized != false) { + output.WriteRawTag(80); + output.WriteBool(IsHistorized); + } + if (IsAlarm != false) { + output.WriteRawTag(88); + output.WriteBool(IsAlarm); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (AttributeName.Length != 0) { + output.WriteRawTag(10); + output.WriteString(AttributeName); + } + if (FullTagReference.Length != 0) { + output.WriteRawTag(18); + output.WriteString(FullTagReference); + } + if (MxDataType != 0) { + output.WriteRawTag(24); + output.WriteInt32(MxDataType); + } + if (DataTypeName.Length != 0) { + output.WriteRawTag(34); + output.WriteString(DataTypeName); + } + if (IsArray != false) { + output.WriteRawTag(40); + output.WriteBool(IsArray); + } + if (ArrayDimension != 0) { + output.WriteRawTag(48); + output.WriteInt32(ArrayDimension); + } + if (ArrayDimensionPresent != false) { + output.WriteRawTag(56); + output.WriteBool(ArrayDimensionPresent); + } + if (MxAttributeCategory != 0) { + output.WriteRawTag(64); + output.WriteInt32(MxAttributeCategory); + } + if (SecurityClassification != 0) { + output.WriteRawTag(72); + output.WriteInt32(SecurityClassification); + } + if (IsHistorized != false) { + output.WriteRawTag(80); + output.WriteBool(IsHistorized); + } + if (IsAlarm != false) { + output.WriteRawTag(88); + output.WriteBool(IsAlarm); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + if (AttributeName.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(AttributeName); + } + if (FullTagReference.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(FullTagReference); + } + if (MxDataType != 0) { + size += 1 + pb::CodedOutputStream.ComputeInt32Size(MxDataType); + } + if (DataTypeName.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(DataTypeName); + } + if (IsArray != false) { + size += 1 + 1; + } + if (ArrayDimension != 0) { + size += 1 + pb::CodedOutputStream.ComputeInt32Size(ArrayDimension); + } + if (ArrayDimensionPresent != false) { + size += 1 + 1; + } + if (MxAttributeCategory != 0) { + size += 1 + pb::CodedOutputStream.ComputeInt32Size(MxAttributeCategory); + } + if (SecurityClassification != 0) { + size += 1 + pb::CodedOutputStream.ComputeInt32Size(SecurityClassification); + } + if (IsHistorized != false) { + size += 1 + 1; + } + if (IsAlarm != false) { + size += 1 + 1; + } + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(GalaxyAttribute other) { + if (other == null) { + return; + } + if (other.AttributeName.Length != 0) { + AttributeName = other.AttributeName; + } + if (other.FullTagReference.Length != 0) { + FullTagReference = other.FullTagReference; + } + if (other.MxDataType != 0) { + MxDataType = other.MxDataType; + } + if (other.DataTypeName.Length != 0) { + DataTypeName = other.DataTypeName; + } + if (other.IsArray != false) { + IsArray = other.IsArray; + } + if (other.ArrayDimension != 0) { + ArrayDimension = other.ArrayDimension; + } + if (other.ArrayDimensionPresent != false) { + ArrayDimensionPresent = other.ArrayDimensionPresent; + } + if (other.MxAttributeCategory != 0) { + MxAttributeCategory = other.MxAttributeCategory; + } + if (other.SecurityClassification != 0) { + SecurityClassification = other.SecurityClassification; + } + if (other.IsHistorized != false) { + IsHistorized = other.IsHistorized; + } + if (other.IsAlarm != false) { + IsAlarm = other.IsAlarm; + } + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + case 10: { + AttributeName = input.ReadString(); + break; + } + case 18: { + FullTagReference = input.ReadString(); + break; + } + case 24: { + MxDataType = input.ReadInt32(); + break; + } + case 34: { + DataTypeName = input.ReadString(); + break; + } + case 40: { + IsArray = input.ReadBool(); + break; + } + case 48: { + ArrayDimension = input.ReadInt32(); + break; + } + case 56: { + ArrayDimensionPresent = input.ReadBool(); + break; + } + case 64: { + MxAttributeCategory = input.ReadInt32(); + break; + } + case 72: { + SecurityClassification = input.ReadInt32(); + break; + } + case 80: { + IsHistorized = input.ReadBool(); + break; + } + case 88: { + IsAlarm = input.ReadBool(); + break; + } + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + case 10: { + AttributeName = input.ReadString(); + break; + } + case 18: { + FullTagReference = input.ReadString(); + break; + } + case 24: { + MxDataType = input.ReadInt32(); + break; + } + case 34: { + DataTypeName = input.ReadString(); + break; + } + case 40: { + IsArray = input.ReadBool(); + break; + } + case 48: { + ArrayDimension = input.ReadInt32(); + break; + } + case 56: { + ArrayDimensionPresent = input.ReadBool(); + break; + } + case 64: { + MxAttributeCategory = input.ReadInt32(); + break; + } + case 72: { + SecurityClassification = input.ReadInt32(); + break; + } + case 80: { + IsHistorized = input.ReadBool(); + break; + } + case 88: { + IsAlarm = input.ReadBool(); + break; + } + } + } + } + #endif + + } + + #endregion + +} + +#endregion Designer generated code diff --git a/src/MxGateway.Contracts/Generated/GalaxyRepositoryGrpc.cs b/src/MxGateway.Contracts/Generated/GalaxyRepositoryGrpc.cs new file mode 100644 index 0000000..7f00f0a --- /dev/null +++ b/src/MxGateway.Contracts/Generated/GalaxyRepositoryGrpc.cs @@ -0,0 +1,307 @@ +// +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: galaxy_repository.proto +// +#pragma warning disable 0414, 1591, 8981, 0612 +#region Designer generated code + +using grpc = global::Grpc.Core; + +namespace MxGateway.Contracts.Proto.Galaxy { + /// + /// Read-only browse over the AVEVA System Platform Galaxy Repository (ZB SQL + /// database). Lets clients enumerate the deployed object hierarchy and each + /// object's dynamic attributes so they know what tag references to subscribe + /// to via the MxAccessGateway service. + /// + public static partial class GalaxyRepository + { + static readonly string __ServiceName = "galaxy_repository.v1.GalaxyRepository"; + + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + static void __Helper_SerializeMessage(global::Google.Protobuf.IMessage message, grpc::SerializationContext context) + { + #if !GRPC_DISABLE_PROTOBUF_BUFFER_SERIALIZATION + if (message is global::Google.Protobuf.IBufferMessage) + { + context.SetPayloadLength(message.CalculateSize()); + global::Google.Protobuf.MessageExtensions.WriteTo(message, context.GetBufferWriter()); + context.Complete(); + return; + } + #endif + context.Complete(global::Google.Protobuf.MessageExtensions.ToByteArray(message)); + } + + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + static class __Helper_MessageCache + { + public static readonly bool IsBufferMessage = global::System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(global::Google.Protobuf.IBufferMessage)).IsAssignableFrom(typeof(T)); + } + + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + static T __Helper_DeserializeMessage(grpc::DeserializationContext context, global::Google.Protobuf.MessageParser parser) where T : global::Google.Protobuf.IMessage + { + #if !GRPC_DISABLE_PROTOBUF_BUFFER_SERIALIZATION + if (__Helper_MessageCache.IsBufferMessage) + { + return parser.ParseFrom(context.PayloadAsReadOnlySequence()); + } + #endif + return parser.ParseFrom(context.PayloadAsNewBuffer()); + } + + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + static readonly grpc::Marshaller __Marshaller_galaxy_repository_v1_TestConnectionRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest.Parser)); + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + static readonly grpc::Marshaller __Marshaller_galaxy_repository_v1_TestConnectionReply = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.Galaxy.TestConnectionReply.Parser)); + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + static readonly grpc::Marshaller __Marshaller_galaxy_repository_v1_GetLastDeployTimeRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest.Parser)); + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + static readonly grpc::Marshaller __Marshaller_galaxy_repository_v1_GetLastDeployTimeReply = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply.Parser)); + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + static readonly grpc::Marshaller __Marshaller_galaxy_repository_v1_DiscoverHierarchyRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest.Parser)); + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + static readonly grpc::Marshaller __Marshaller_galaxy_repository_v1_DiscoverHierarchyReply = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply.Parser)); + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + static readonly grpc::Marshaller __Marshaller_galaxy_repository_v1_WatchDeployEventsRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest.Parser)); + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + static readonly grpc::Marshaller __Marshaller_galaxy_repository_v1_DeployEvent = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.Galaxy.DeployEvent.Parser)); + + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + static readonly grpc::Method __Method_TestConnection = new grpc::Method( + grpc::MethodType.Unary, + __ServiceName, + "TestConnection", + __Marshaller_galaxy_repository_v1_TestConnectionRequest, + __Marshaller_galaxy_repository_v1_TestConnectionReply); + + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + static readonly grpc::Method __Method_GetLastDeployTime = new grpc::Method( + grpc::MethodType.Unary, + __ServiceName, + "GetLastDeployTime", + __Marshaller_galaxy_repository_v1_GetLastDeployTimeRequest, + __Marshaller_galaxy_repository_v1_GetLastDeployTimeReply); + + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + static readonly grpc::Method __Method_DiscoverHierarchy = new grpc::Method( + grpc::MethodType.Unary, + __ServiceName, + "DiscoverHierarchy", + __Marshaller_galaxy_repository_v1_DiscoverHierarchyRequest, + __Marshaller_galaxy_repository_v1_DiscoverHierarchyReply); + + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + static readonly grpc::Method __Method_WatchDeployEvents = new grpc::Method( + grpc::MethodType.ServerStreaming, + __ServiceName, + "WatchDeployEvents", + __Marshaller_galaxy_repository_v1_WatchDeployEventsRequest, + __Marshaller_galaxy_repository_v1_DeployEvent); + + /// Service descriptor + public static global::Google.Protobuf.Reflection.ServiceDescriptor Descriptor + { + get { return global::MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.Services[0]; } + } + + /// Base class for server-side implementations of GalaxyRepository + [grpc::BindServiceMethod(typeof(GalaxyRepository), "BindService")] + public abstract partial class GalaxyRepositoryBase + { + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + public virtual global::System.Threading.Tasks.Task TestConnection(global::MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest request, grpc::ServerCallContext context) + { + throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, "")); + } + + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + public virtual global::System.Threading.Tasks.Task GetLastDeployTime(global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest request, grpc::ServerCallContext context) + { + throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, "")); + } + + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + public virtual global::System.Threading.Tasks.Task DiscoverHierarchy(global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest request, grpc::ServerCallContext context) + { + throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, "")); + } + + /// + /// Server-stream of deploy events. The server emits the current state immediately + /// on subscribe (so clients can bootstrap their cache without waiting for the next + /// deploy), then emits one event each time the gateway's hierarchy cache observes + /// a new galaxy.time_of_last_deploy. The sequence field is monotonically + /// increasing per server start; gaps indicate the per-subscriber buffer dropped + /// older events because the client was too slow. + /// + /// The request received from the client. + /// Used for sending responses back to the client. + /// The context of the server-side call handler being invoked. + /// A task indicating completion of the handler. + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + public virtual global::System.Threading.Tasks.Task WatchDeployEvents(global::MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest request, grpc::IServerStreamWriter responseStream, grpc::ServerCallContext context) + { + throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, "")); + } + + } + + /// Client for GalaxyRepository + public partial class GalaxyRepositoryClient : grpc::ClientBase + { + /// Creates a new client for GalaxyRepository + /// The channel to use to make remote calls. + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + public GalaxyRepositoryClient(grpc::ChannelBase channel) : base(channel) + { + } + /// Creates a new client for GalaxyRepository that uses a custom CallInvoker. + /// The callInvoker to use to make remote calls. + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + public GalaxyRepositoryClient(grpc::CallInvoker callInvoker) : base(callInvoker) + { + } + /// Protected parameterless constructor to allow creation of test doubles. + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + protected GalaxyRepositoryClient() : base() + { + } + /// Protected constructor to allow creation of configured clients. + /// The client configuration. + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + protected GalaxyRepositoryClient(ClientBaseConfiguration configuration) : base(configuration) + { + } + + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + public virtual global::MxGateway.Contracts.Proto.Galaxy.TestConnectionReply TestConnection(global::MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + return TestConnection(request, new grpc::CallOptions(headers, deadline, cancellationToken)); + } + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + public virtual global::MxGateway.Contracts.Proto.Galaxy.TestConnectionReply TestConnection(global::MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest request, grpc::CallOptions options) + { + return CallInvoker.BlockingUnaryCall(__Method_TestConnection, null, options, request); + } + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + public virtual grpc::AsyncUnaryCall TestConnectionAsync(global::MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + return TestConnectionAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken)); + } + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + public virtual grpc::AsyncUnaryCall TestConnectionAsync(global::MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest request, grpc::CallOptions options) + { + return CallInvoker.AsyncUnaryCall(__Method_TestConnection, null, options, request); + } + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + public virtual global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply GetLastDeployTime(global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + return GetLastDeployTime(request, new grpc::CallOptions(headers, deadline, cancellationToken)); + } + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + public virtual global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply GetLastDeployTime(global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest request, grpc::CallOptions options) + { + return CallInvoker.BlockingUnaryCall(__Method_GetLastDeployTime, null, options, request); + } + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + public virtual grpc::AsyncUnaryCall GetLastDeployTimeAsync(global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + return GetLastDeployTimeAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken)); + } + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + public virtual grpc::AsyncUnaryCall GetLastDeployTimeAsync(global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest request, grpc::CallOptions options) + { + return CallInvoker.AsyncUnaryCall(__Method_GetLastDeployTime, null, options, request); + } + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + public virtual global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply DiscoverHierarchy(global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + return DiscoverHierarchy(request, new grpc::CallOptions(headers, deadline, cancellationToken)); + } + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + public virtual global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply DiscoverHierarchy(global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest request, grpc::CallOptions options) + { + return CallInvoker.BlockingUnaryCall(__Method_DiscoverHierarchy, null, options, request); + } + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + public virtual grpc::AsyncUnaryCall DiscoverHierarchyAsync(global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + return DiscoverHierarchyAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken)); + } + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + public virtual grpc::AsyncUnaryCall DiscoverHierarchyAsync(global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest request, grpc::CallOptions options) + { + return CallInvoker.AsyncUnaryCall(__Method_DiscoverHierarchy, null, options, request); + } + /// + /// Server-stream of deploy events. The server emits the current state immediately + /// on subscribe (so clients can bootstrap their cache without waiting for the next + /// deploy), then emits one event each time the gateway's hierarchy cache observes + /// a new galaxy.time_of_last_deploy. The sequence field is monotonically + /// increasing per server start; gaps indicate the per-subscriber buffer dropped + /// older events because the client was too slow. + /// + /// The request to send to the server. + /// The initial metadata to send with the call. This parameter is optional. + /// An optional deadline for the call. The call will be cancelled if deadline is hit. + /// An optional token for canceling the call. + /// The call object. + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + public virtual grpc::AsyncServerStreamingCall WatchDeployEvents(global::MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + return WatchDeployEvents(request, new grpc::CallOptions(headers, deadline, cancellationToken)); + } + /// + /// Server-stream of deploy events. The server emits the current state immediately + /// on subscribe (so clients can bootstrap their cache without waiting for the next + /// deploy), then emits one event each time the gateway's hierarchy cache observes + /// a new galaxy.time_of_last_deploy. The sequence field is monotonically + /// increasing per server start; gaps indicate the per-subscriber buffer dropped + /// older events because the client was too slow. + /// + /// The request to send to the server. + /// The options for the call. + /// The call object. + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + public virtual grpc::AsyncServerStreamingCall WatchDeployEvents(global::MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest request, grpc::CallOptions options) + { + return CallInvoker.AsyncServerStreamingCall(__Method_WatchDeployEvents, null, options, request); + } + /// Creates a new instance of client from given ClientBaseConfiguration. + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + protected override GalaxyRepositoryClient NewInstance(ClientBaseConfiguration configuration) + { + return new GalaxyRepositoryClient(configuration); + } + } + + /// Creates service definition that can be registered with a server + /// An object implementing the server-side handling logic. + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + public static grpc::ServerServiceDefinition BindService(GalaxyRepositoryBase serviceImpl) + { + return grpc::ServerServiceDefinition.CreateBuilder() + .AddMethod(__Method_TestConnection, serviceImpl.TestConnection) + .AddMethod(__Method_GetLastDeployTime, serviceImpl.GetLastDeployTime) + .AddMethod(__Method_DiscoverHierarchy, serviceImpl.DiscoverHierarchy) + .AddMethod(__Method_WatchDeployEvents, serviceImpl.WatchDeployEvents).Build(); + } + + /// Register service method with a service binder with or without implementation. Useful when customizing the service binding logic. + /// Note: this method is part of an experimental API that can change or be removed without any prior notice. + /// Service methods will be bound by calling AddMethod on this object. + /// An object implementing the server-side handling logic. + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + public static void BindService(grpc::ServiceBinderBase serviceBinder, GalaxyRepositoryBase serviceImpl) + { + serviceBinder.AddMethod(__Method_TestConnection, serviceImpl == null ? null : new grpc::UnaryServerMethod(serviceImpl.TestConnection)); + serviceBinder.AddMethod(__Method_GetLastDeployTime, serviceImpl == null ? null : new grpc::UnaryServerMethod(serviceImpl.GetLastDeployTime)); + serviceBinder.AddMethod(__Method_DiscoverHierarchy, serviceImpl == null ? null : new grpc::UnaryServerMethod(serviceImpl.DiscoverHierarchy)); + serviceBinder.AddMethod(__Method_WatchDeployEvents, serviceImpl == null ? null : new grpc::ServerStreamingServerMethod(serviceImpl.WatchDeployEvents)); + } + + } +} +#endregion diff --git a/src/MxGateway.Contracts/MxGateway.Contracts.csproj b/src/MxGateway.Contracts/MxGateway.Contracts.csproj index 97f9535..f4c8dd8 100644 --- a/src/MxGateway.Contracts/MxGateway.Contracts.csproj +++ b/src/MxGateway.Contracts/MxGateway.Contracts.csproj @@ -8,6 +8,7 @@ + diff --git a/src/MxGateway.Contracts/Protos/galaxy_repository.proto b/src/MxGateway.Contracts/Protos/galaxy_repository.proto new file mode 100644 index 0000000..c4a87f5 --- /dev/null +++ b/src/MxGateway.Contracts/Protos/galaxy_repository.proto @@ -0,0 +1,89 @@ +syntax = "proto3"; + +package galaxy_repository.v1; + +option csharp_namespace = "MxGateway.Contracts.Proto.Galaxy"; + +import "google/protobuf/timestamp.proto"; + +// Read-only browse over the AVEVA System Platform Galaxy Repository (ZB SQL +// database). Lets clients enumerate the deployed object hierarchy and each +// object's dynamic attributes so they know what tag references to subscribe +// to via the MxAccessGateway service. +service GalaxyRepository { + rpc TestConnection(TestConnectionRequest) returns (TestConnectionReply); + rpc GetLastDeployTime(GetLastDeployTimeRequest) returns (GetLastDeployTimeReply); + rpc DiscoverHierarchy(DiscoverHierarchyRequest) returns (DiscoverHierarchyReply); + + // Server-stream of deploy events. The server emits the current state immediately + // on subscribe (so clients can bootstrap their cache without waiting for the next + // deploy), then emits one event each time the gateway's hierarchy cache observes + // a new galaxy.time_of_last_deploy. The sequence field is monotonically + // increasing per server start; gaps indicate the per-subscriber buffer dropped + // older events because the client was too slow. + rpc WatchDeployEvents(WatchDeployEventsRequest) returns (stream DeployEvent); +} + +message TestConnectionRequest {} + +message TestConnectionReply { + bool ok = 1; +} + +message GetLastDeployTimeRequest {} + +message GetLastDeployTimeReply { + bool present = 1; + google.protobuf.Timestamp time_of_last_deploy = 2; +} + +message DiscoverHierarchyRequest {} + +message DiscoverHierarchyReply { + repeated GalaxyObject objects = 1; +} + +message WatchDeployEventsRequest { + // Optional. When set, the bootstrap event is suppressed if the cached deploy + // time matches this value. Future events are still emitted normally. + google.protobuf.Timestamp last_seen_deploy_time = 1; +} + +message DeployEvent { + // Monotonically increasing per server start. Gaps indicate dropped events. + uint64 sequence = 1; + // Server wall-clock when the cache observed the deploy. + google.protobuf.Timestamp observed_at = 2; + // Galaxy.time_of_last_deploy. Absent only when the Galaxy table reports null. + google.protobuf.Timestamp time_of_last_deploy = 3; + bool time_of_last_deploy_present = 4; + int32 object_count = 5; + int32 attribute_count = 6; +} + +message GalaxyObject { + int32 gobject_id = 1; + string tag_name = 2; + string contained_name = 3; + string browse_name = 4; + int32 parent_gobject_id = 5; + bool is_area = 6; + int32 category_id = 7; + int32 hosted_by_gobject_id = 8; + repeated string template_chain = 9; + repeated GalaxyAttribute attributes = 10; +} + +message GalaxyAttribute { + string attribute_name = 1; + string full_tag_reference = 2; + int32 mx_data_type = 3; + string data_type_name = 4; + bool is_array = 5; + int32 array_dimension = 6; + bool array_dimension_present = 7; + int32 mx_attribute_category = 8; + int32 security_classification = 9; + bool is_historized = 10; + bool is_alarm = 11; +} diff --git a/src/MxGateway.IntegrationTests/Galaxy/GalaxyRepositoryLiveTests.cs b/src/MxGateway.IntegrationTests/Galaxy/GalaxyRepositoryLiveTests.cs new file mode 100644 index 0000000..34ae67d --- /dev/null +++ b/src/MxGateway.IntegrationTests/Galaxy/GalaxyRepositoryLiveTests.cs @@ -0,0 +1,68 @@ +using MxGateway.Server.Galaxy; + +namespace MxGateway.IntegrationTests.Galaxy; + +public sealed class GalaxyRepositoryLiveTests +{ + [LiveGalaxyRepositoryFact] + [Trait("Category", "LiveGalaxy")] + public async Task TestConnection_AgainstZb_Succeeds() + { + GalaxyRepository repository = CreateRepository(); + + bool ok = await repository.TestConnectionAsync(CancellationToken.None); + + Assert.True(ok, "TestConnectionAsync should return true against the ZB database."); + } + + [LiveGalaxyRepositoryFact] + [Trait("Category", "LiveGalaxy")] + public async Task GetLastDeployTime_AgainstZb_ReturnsTimestamp() + { + GalaxyRepository repository = CreateRepository(); + + DateTime? lastDeploy = await repository.GetLastDeployTimeAsync(CancellationToken.None); + + Assert.NotNull(lastDeploy); + } + + [LiveGalaxyRepositoryFact] + [Trait("Category", "LiveGalaxy")] + public async Task GetHierarchy_AgainstZb_ReturnsObjects() + { + GalaxyRepository repository = CreateRepository(); + + List rows = await repository.GetHierarchyAsync(CancellationToken.None); + + Assert.NotEmpty(rows); + Assert.All(rows, row => + { + Assert.True(row.GobjectId > 0); + Assert.False(string.IsNullOrEmpty(row.TagName)); + Assert.False(string.IsNullOrEmpty(row.BrowseName)); + }); + } + + [LiveGalaxyRepositoryFact] + [Trait("Category", "LiveGalaxy")] + public async Task GetAttributes_AgainstZb_ReturnsAtLeastOneAttribute() + { + GalaxyRepository repository = CreateRepository(); + + List rows = await repository.GetAttributesAsync(CancellationToken.None); + + Assert.NotEmpty(rows); + Assert.All(rows, row => + { + Assert.True(row.GobjectId > 0); + Assert.False(string.IsNullOrEmpty(row.AttributeName)); + Assert.False(string.IsNullOrEmpty(row.FullTagReference)); + }); + } + + private static GalaxyRepository CreateRepository() => new(new GalaxyRepositoryOptions + { + ConnectionString = LiveGalaxyRepositoryFactAttribute.ConnectionString, + CommandTimeoutSeconds = 30, + }); +} diff --git a/src/MxGateway.IntegrationTests/Galaxy/LiveGalaxyRepositoryFactAttribute.cs b/src/MxGateway.IntegrationTests/Galaxy/LiveGalaxyRepositoryFactAttribute.cs new file mode 100644 index 0000000..585b9d8 --- /dev/null +++ b/src/MxGateway.IntegrationTests/Galaxy/LiveGalaxyRepositoryFactAttribute.cs @@ -0,0 +1,25 @@ +namespace MxGateway.IntegrationTests.Galaxy; + +public sealed class LiveGalaxyRepositoryFactAttribute : FactAttribute +{ + public const string EnableVariableName = "MXGATEWAY_RUN_LIVE_GALAXY_TESTS"; + public const string ConnectionStringVariableName = "MXGATEWAY_LIVE_GALAXY_CONN"; + + public LiveGalaxyRepositoryFactAttribute() + { + if (!Enabled) + { + Skip = $"Set {EnableVariableName}=1 to run live Galaxy Repository tests."; + } + } + + public static bool Enabled => + string.Equals( + Environment.GetEnvironmentVariable(EnableVariableName), + "1", + StringComparison.Ordinal); + + public static string ConnectionString => + Environment.GetEnvironmentVariable(ConnectionStringVariableName) + ?? "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;"; +} diff --git a/src/MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs b/src/MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs index 44360a0..b773604 100644 --- a/src/MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs +++ b/src/MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs @@ -251,6 +251,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output) new MxAccessGrpcRequestValidator(), mapper, eventStreamService, + _metrics, _loggerFactory.CreateLogger()); } diff --git a/src/MxGateway.Server/Dashboard/Components/Layout/DashboardLayout.razor b/src/MxGateway.Server/Dashboard/Components/Layout/DashboardLayout.razor index cf5d467..1a9106e 100644 --- a/src/MxGateway.Server/Dashboard/Components/Layout/DashboardLayout.razor +++ b/src/MxGateway.Server/Dashboard/Components/Layout/DashboardLayout.razor @@ -23,6 +23,9 @@ + diff --git a/src/MxGateway.Server/Dashboard/Components/Pages/DashboardHome.razor b/src/MxGateway.Server/Dashboard/Components/Pages/DashboardHome.razor index 8471a05..0197588 100644 --- a/src/MxGateway.Server/Dashboard/Components/Pages/DashboardHome.razor +++ b/src/MxGateway.Server/Dashboard/Components/Pages/DashboardHome.razor @@ -29,6 +29,26 @@ else +
+
+

Galaxy Repository

+ + View browse details → +
+
+ + + + + + +
+ @if (!string.IsNullOrWhiteSpace(Snapshot.Galaxy.LastError)) + { +
@Snapshot.Galaxy.LastError
+ } +
+

Recent Faults

@@ -36,3 +56,23 @@ else
} + +@code { + private string? GalaxyRefreshDetail() + { + DashboardGalaxySummary galaxy = Snapshot!.Galaxy; + if (galaxy.LastQueriedAt is null) + { + return "never queried"; + } + + if (galaxy.LastSuccessAt is null) + { + return "no successful refresh yet"; + } + + return galaxy.LastQueriedAt > galaxy.LastSuccessAt + ? $"last attempt {DashboardDisplay.DateTime(galaxy.LastQueriedAt)}" + : null; + } +} diff --git a/src/MxGateway.Server/Dashboard/Components/Pages/GalaxyPage.razor b/src/MxGateway.Server/Dashboard/Components/Pages/GalaxyPage.razor new file mode 100644 index 0000000..a858e77 --- /dev/null +++ b/src/MxGateway.Server/Dashboard/Components/Pages/GalaxyPage.razor @@ -0,0 +1,195 @@ +@page "/galaxy" +@page "/dashboard/galaxy" +@inherits DashboardPageBase + +Dashboard Galaxy + +@if (Snapshot is null) +{ +
Loading Galaxy summary.
+} +else +{ +
+
+

Galaxy Repository

+
@RefreshHeading()
+
+ +
+ +
+ + + + + + +
+ + @if (Snapshot.Galaxy.Status == DashboardGalaxyStatus.Unknown) + { +
+
+ Galaxy summary has not been collected yet. The dashboard refreshes the + summary every @RefreshIntervalSeconds() seconds via the + GalaxyRepository service. +
+
+ } + + @if (!string.IsNullOrWhiteSpace(Snapshot.Galaxy.LastError)) + { +
+
+

Last Error

+
+
@Snapshot.Galaxy.LastError
+
+ } + +
+
+

Object Categories

+
+ @if (Snapshot.Galaxy.ObjectCategories.Count == 0) + { +
No deployed objects observed.
+ } + else + { +
+ + + + + + + + + + @foreach (DashboardGalaxyCategoryCount row in Snapshot.Galaxy.ObjectCategories) + { + + + + + + } + +
CategoryCategory IDObjects
@row.CategoryName@row.CategoryId@DashboardDisplay.Count(row.ObjectCount)
+
+ } +
+ +
+
+

Top Templates

+
+ @if (Snapshot.Galaxy.TopTemplates.Count == 0) + { +
No template usage observed.
+ } + else + { +
+ + + + + + + + + @foreach (DashboardGalaxyTemplateUsage row in Snapshot.Galaxy.TopTemplates) + { + + + + + } + +
TemplateInstances
@row.TemplateName@DashboardDisplay.Count(row.InstanceCount)
+
+ } +
+ +
+
+

Sync Info

+
+
+ + + + + + + + + + +
Status
Last successful refresh@DashboardDisplay.DateTime(Snapshot.Galaxy.LastSuccessAt)
Last attempt@DashboardDisplay.DateTime(Snapshot.Galaxy.LastQueriedAt)
Galaxy time_of_last_deploy@DashboardDisplay.DateTime(Snapshot.Galaxy.LastDeployTime)
Refresh interval@RefreshIntervalSeconds() seconds
Connection string@DashboardDisplay.Text(GalaxyConnectionStringDisplay())
Command timeout@CommandTimeoutSeconds() seconds
+
+
+ Browse data is served by the galaxy_repository.v1.GalaxyRepository gRPC + service. Clients call DiscoverHierarchy for the full tree and + GetLastDeployTime to detect redeployments. +
+
+} + +@code { + [Inject] + private IOptions GalaxyOptions { get; set; } = null!; + + private string RefreshHeading() + { + DashboardGalaxySummary galaxy = Snapshot!.Galaxy; + return galaxy.LastSuccessAt is null + ? "Awaiting first successful refresh" + : $"Refreshed {DashboardDisplay.DateTime(galaxy.LastSuccessAt)}"; + } + + private string? DeployAge() + { + DashboardGalaxySummary galaxy = Snapshot!.Galaxy; + if (galaxy.LastDeployTime is null || galaxy.LastSuccessAt is null) + { + return null; + } + + TimeSpan age = galaxy.LastSuccessAt.Value - galaxy.LastDeployTime.Value; + if (age < TimeSpan.Zero) + { + return null; + } + + return $"{DashboardDisplay.Duration(age)} ago"; + } + + private string? LastAttemptDetail() + { + DashboardGalaxySummary galaxy = Snapshot!.Galaxy; + if (galaxy.LastQueriedAt is null) + { + return "never queried"; + } + + if (galaxy.LastSuccessAt is null) + { + return "no successful refresh yet"; + } + + return galaxy.LastQueriedAt > galaxy.LastSuccessAt + ? $"last attempt {DashboardDisplay.DateTime(galaxy.LastQueriedAt)}" + : null; + } + + private int RefreshIntervalSeconds() => Math.Max(1, GalaxyOptions.Value.DashboardRefreshIntervalSeconds); + + private int CommandTimeoutSeconds() => GalaxyOptions.Value.CommandTimeoutSeconds; + + private string? GalaxyConnectionStringDisplay() => + DashboardRedactor.Redact(GalaxyOptions.Value.ConnectionString); +} diff --git a/src/MxGateway.Server/Dashboard/Components/Shared/StatusBadge.razor b/src/MxGateway.Server/Dashboard/Components/Shared/StatusBadge.razor index be31d3c..791227c 100644 --- a/src/MxGateway.Server/Dashboard/Components/Shared/StatusBadge.razor +++ b/src/MxGateway.Server/Dashboard/Components/Shared/StatusBadge.razor @@ -9,7 +9,9 @@ "Ready" or "Healthy" => "text-bg-success", "Creating" or "StartingWorker" or "WaitingForPipe" or "InitializingWorker" or "Closing" => "text-bg-info", "Closed" => "text-bg-secondary", - "Faulted" => "text-bg-danger", + "Stale" => "text-bg-warning", + "Faulted" or "Unavailable" => "text-bg-danger", + "Unknown" => "text-bg-light text-dark border", _ => "text-bg-light text-dark border" }; } diff --git a/src/MxGateway.Server/Dashboard/DashboardGalaxyProjector.cs b/src/MxGateway.Server/Dashboard/DashboardGalaxyProjector.cs new file mode 100644 index 0000000..03b6bfe --- /dev/null +++ b/src/MxGateway.Server/Dashboard/DashboardGalaxyProjector.cs @@ -0,0 +1,98 @@ +using MxGateway.Server.Galaxy; + +namespace MxGateway.Server.Dashboard; + +/// +/// Projects a into a +/// for the Blazor pages. Top-templates and +/// per-category breakdowns are computed here rather than stored on the cache so the +/// Galaxy namespace stays free of dashboard-presentation concepts. +/// +internal static class DashboardGalaxyProjector +{ + private const int TopTemplatesLimit = 10; + + private static readonly IReadOnlyDictionary CategoryNamesById = new Dictionary + { + [1] = "WinPlatform", + [3] = "AppEngine", + [4] = "InTouchViewApp", + [10] = "UserDefined", + [11] = "FieldReference", + [13] = "Area", + [17] = "DIObject", + [24] = "DDESuiteLinkClient", + [26] = "OPCClient", + }; + + public static DashboardGalaxySummary Project(GalaxyHierarchyCacheEntry entry) + { + DashboardGalaxyStatus status = entry.Status switch + { + GalaxyCacheStatus.Healthy => DashboardGalaxyStatus.Healthy, + GalaxyCacheStatus.Stale => DashboardGalaxyStatus.Stale, + GalaxyCacheStatus.Unavailable => DashboardGalaxyStatus.Unavailable, + _ => DashboardGalaxyStatus.Unknown, + }; + + IReadOnlyList topTemplates; + IReadOnlyList objectCategories; + + if (entry.Hierarchy.Count == 0) + { + topTemplates = Array.Empty(); + objectCategories = Array.Empty(); + } + else + { + Dictionary objectsByCategory = new(); + Dictionary templateUsage = new(StringComparer.OrdinalIgnoreCase); + + foreach (GalaxyHierarchyRow row in entry.Hierarchy) + { + objectsByCategory.TryGetValue(row.CategoryId, out int categoryCount); + objectsByCategory[row.CategoryId] = categoryCount + 1; + + if (row.TemplateChain.Count > 0) + { + string immediate = row.TemplateChain[0]; + if (!string.IsNullOrWhiteSpace(immediate)) + { + templateUsage.TryGetValue(immediate, out int templateCount); + templateUsage[immediate] = templateCount + 1; + } + } + } + + topTemplates = templateUsage + .OrderByDescending(entry => entry.Value) + .ThenBy(entry => entry.Key, StringComparer.OrdinalIgnoreCase) + .Take(TopTemplatesLimit) + .Select(entry => new DashboardGalaxyTemplateUsage(entry.Key, entry.Value)) + .ToArray(); + + objectCategories = objectsByCategory + .OrderByDescending(entry => entry.Value) + .ThenBy(entry => entry.Key) + .Select(entry => new DashboardGalaxyCategoryCount( + entry.Key, + CategoryNamesById.TryGetValue(entry.Key, out string? name) ? name : $"Category {entry.Key}", + entry.Value)) + .ToArray(); + } + + return new DashboardGalaxySummary( + Status: status, + LastQueriedAt: entry.LastQueriedAt, + LastSuccessAt: entry.LastSuccessAt, + LastDeployTime: entry.LastDeployTime, + LastError: entry.LastError, + ObjectCount: entry.ObjectCount, + AreaCount: entry.AreaCount, + AttributeCount: entry.AttributeCount, + HistorizedAttributeCount: entry.HistorizedAttributeCount, + AlarmAttributeCount: entry.AlarmAttributeCount, + TopTemplates: topTemplates, + ObjectCategories: objectCategories); + } +} diff --git a/src/MxGateway.Server/Dashboard/DashboardGalaxySummary.cs b/src/MxGateway.Server/Dashboard/DashboardGalaxySummary.cs new file mode 100644 index 0000000..769a76b --- /dev/null +++ b/src/MxGateway.Server/Dashboard/DashboardGalaxySummary.cs @@ -0,0 +1,47 @@ +namespace MxGateway.Server.Dashboard; + +/// +/// Snapshot of the Galaxy Repository (ZB) browse state surfaced on the dashboard. +/// Populated by on a background refresh cadence so +/// the dashboard never blocks on SQL. +/// +public sealed record DashboardGalaxySummary( + DashboardGalaxyStatus Status, + DateTimeOffset? LastQueriedAt, + DateTimeOffset? LastSuccessAt, + DateTimeOffset? LastDeployTime, + string? LastError, + int ObjectCount, + int AreaCount, + int AttributeCount, + int HistorizedAttributeCount, + int AlarmAttributeCount, + IReadOnlyList TopTemplates, + IReadOnlyList ObjectCategories) +{ + public static DashboardGalaxySummary Unknown { get; } = new( + DashboardGalaxyStatus.Unknown, + LastQueriedAt: null, + LastSuccessAt: null, + LastDeployTime: null, + LastError: null, + ObjectCount: 0, + AreaCount: 0, + AttributeCount: 0, + HistorizedAttributeCount: 0, + AlarmAttributeCount: 0, + TopTemplates: Array.Empty(), + ObjectCategories: Array.Empty()); +} + +public enum DashboardGalaxyStatus +{ + Unknown = 0, + Healthy = 1, + Stale = 2, + Unavailable = 3, +} + +public sealed record DashboardGalaxyTemplateUsage(string TemplateName, int InstanceCount); + +public sealed record DashboardGalaxyCategoryCount(int CategoryId, string CategoryName, int ObjectCount); diff --git a/src/MxGateway.Server/Dashboard/DashboardSnapshot.cs b/src/MxGateway.Server/Dashboard/DashboardSnapshot.cs index 00aa0e0..8f30de7 100644 --- a/src/MxGateway.Server/Dashboard/DashboardSnapshot.cs +++ b/src/MxGateway.Server/Dashboard/DashboardSnapshot.cs @@ -12,4 +12,5 @@ public sealed record DashboardSnapshot( IReadOnlyList Workers, IReadOnlyList Metrics, IReadOnlyList Faults, - EffectiveGatewayConfiguration Configuration); + EffectiveGatewayConfiguration Configuration, + DashboardGalaxySummary Galaxy); diff --git a/src/MxGateway.Server/Dashboard/DashboardSnapshotService.cs b/src/MxGateway.Server/Dashboard/DashboardSnapshotService.cs index baddf52..f996503 100644 --- a/src/MxGateway.Server/Dashboard/DashboardSnapshotService.cs +++ b/src/MxGateway.Server/Dashboard/DashboardSnapshotService.cs @@ -1,6 +1,7 @@ using System.Runtime.CompilerServices; using Microsoft.Extensions.Options; using MxGateway.Server.Configuration; +using MxGateway.Server.Galaxy; using MxGateway.Server.Metrics; using MxGateway.Server.Sessions; using MxGateway.Server.Workers; @@ -14,6 +15,7 @@ public sealed class DashboardSnapshotService : IDashboardSnapshotService private readonly ISessionRegistry _sessionRegistry; private readonly GatewayMetrics _metrics; private readonly IGatewayConfigurationProvider _configurationProvider; + private readonly IGalaxyHierarchyCache _galaxyHierarchyCache; private readonly TimeProvider _timeProvider; private readonly DateTimeOffset _gatewayStartedAt; private readonly TimeSpan _snapshotInterval; @@ -24,12 +26,14 @@ public sealed class DashboardSnapshotService : IDashboardSnapshotService ISessionRegistry sessionRegistry, GatewayMetrics metrics, IGatewayConfigurationProvider configurationProvider, + IGalaxyHierarchyCache galaxyHierarchyCache, IOptions options, TimeProvider? timeProvider = null) { _sessionRegistry = sessionRegistry ?? throw new ArgumentNullException(nameof(sessionRegistry)); _metrics = metrics ?? throw new ArgumentNullException(nameof(metrics)); _configurationProvider = configurationProvider ?? throw new ArgumentNullException(nameof(configurationProvider)); + _galaxyHierarchyCache = galaxyHierarchyCache ?? throw new ArgumentNullException(nameof(galaxyHierarchyCache)); ArgumentNullException.ThrowIfNull(options); _timeProvider = timeProvider ?? TimeProvider.System; @@ -65,7 +69,8 @@ public sealed class DashboardSnapshotService : IDashboardSnapshotService Workers: workerSummaries, Metrics: CreateMetricSummaries(metricsSnapshot), Faults: CreateFaultSummaries(sessions, generatedAt), - Configuration: _configurationProvider.GetEffectiveConfiguration()); + Configuration: _configurationProvider.GetEffectiveConfiguration(), + Galaxy: DashboardGalaxyProjector.Project(_galaxyHierarchyCache.Current)); } public async IAsyncEnumerable WatchSnapshotsAsync( diff --git a/src/MxGateway.Server/Galaxy/GalaxyCacheStatus.cs b/src/MxGateway.Server/Galaxy/GalaxyCacheStatus.cs new file mode 100644 index 0000000..53572b6 --- /dev/null +++ b/src/MxGateway.Server/Galaxy/GalaxyCacheStatus.cs @@ -0,0 +1,17 @@ +namespace MxGateway.Server.Galaxy; + +public enum GalaxyCacheStatus +{ + /// Cache has never completed a refresh. + Unknown = 0, + + /// Cache holds data from a recent successful refresh. + Healthy = 1, + + /// Cache holds data, but the most recent refresh attempt failed + /// or no successful refresh has happened within the staleness threshold. + Stale = 2, + + /// Latest refresh failed and no prior data is available. + Unavailable = 3, +} diff --git a/src/MxGateway.Server/Galaxy/GalaxyDeployEventInfo.cs b/src/MxGateway.Server/Galaxy/GalaxyDeployEventInfo.cs new file mode 100644 index 0000000..0afd24f --- /dev/null +++ b/src/MxGateway.Server/Galaxy/GalaxyDeployEventInfo.cs @@ -0,0 +1,14 @@ +namespace MxGateway.Server.Galaxy; + +/// +/// A single Galaxy deploy notification. Published by +/// whenever a refresh detects that galaxy.time_of_last_deploy has changed (or on +/// the first successful refresh). Consumed by +/// subscribers (the streaming gRPC RPC). +/// +public sealed record GalaxyDeployEventInfo( + long Sequence, + DateTimeOffset ObservedAt, + DateTimeOffset? TimeOfLastDeploy, + int ObjectCount, + int AttributeCount); diff --git a/src/MxGateway.Server/Galaxy/GalaxyDeployNotifier.cs b/src/MxGateway.Server/Galaxy/GalaxyDeployNotifier.cs new file mode 100644 index 0000000..3793d18 --- /dev/null +++ b/src/MxGateway.Server/Galaxy/GalaxyDeployNotifier.cs @@ -0,0 +1,74 @@ +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; +using System.Threading.Channels; + +namespace MxGateway.Server.Galaxy; + +/// +/// Channel-based fan-out of Galaxy deploy events to streaming gRPC subscribers. Each +/// subscriber gets a private bounded channel so a slow client cannot back-pressure +/// other subscribers or the publisher. When a subscriber's channel is full the oldest +/// event is dropped — clients use the sequence field to detect gaps. +/// +public sealed class GalaxyDeployNotifier : IGalaxyDeployNotifier +{ + private const int SubscriberQueueCapacity = 16; + + private readonly ConcurrentDictionary> _subscribers = new(); + private GalaxyDeployEventInfo? _latest; + + public GalaxyDeployEventInfo? Latest => Volatile.Read(ref _latest); + + public void Publish(GalaxyDeployEventInfo info) + { + ArgumentNullException.ThrowIfNull(info); + + Volatile.Write(ref _latest, info); + + foreach (Channel channel in _subscribers.Values) + { + // BoundedChannelFullMode.DropOldest -> writes never wait; we only fail if the + // channel was completed by the subscriber side, which we ignore. + channel.Writer.TryWrite(info); + } + } + + public async IAsyncEnumerable SubscribeAsync( + [EnumeratorCancellation] CancellationToken cancellationToken) + { + Guid subscriberId = Guid.NewGuid(); + Channel channel = Channel.CreateBounded( + new BoundedChannelOptions(SubscriberQueueCapacity) + { + FullMode = BoundedChannelFullMode.DropOldest, + SingleReader = true, + SingleWriter = false, + }); + + _subscribers[subscriberId] = channel; + + // Bootstrap: emit the latest known event so subscribers don't need to wait for + // the next deploy to know current state. + GalaxyDeployEventInfo? bootstrap = Volatile.Read(ref _latest); + if (bootstrap is not null) + { + channel.Writer.TryWrite(bootstrap); + } + + try + { + while (await channel.Reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) + { + while (channel.Reader.TryRead(out GalaxyDeployEventInfo? next)) + { + yield return next; + } + } + } + finally + { + _subscribers.TryRemove(subscriberId, out _); + channel.Writer.TryComplete(); + } + } +} diff --git a/src/MxGateway.Server/Galaxy/GalaxyHierarchyCache.cs b/src/MxGateway.Server/Galaxy/GalaxyHierarchyCache.cs new file mode 100644 index 0000000..3f0b7f1 --- /dev/null +++ b/src/MxGateway.Server/Galaxy/GalaxyHierarchyCache.cs @@ -0,0 +1,186 @@ +using Google.Protobuf.WellKnownTypes; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Logging; +using MxGateway.Contracts.Proto.Galaxy; +using MxGateway.Server.Grpc; + +namespace MxGateway.Server.Galaxy; + +/// +/// Server-side cache of Galaxy Repository browse data. All gRPC clients share the same +/// entry — the materialized is produced once per +/// refresh and reused across requests. Refreshes are deploy-time gated: every tick +/// queries galaxy.time_of_last_deploy (cheap), and the heavy hierarchy + +/// attributes rowsets are pulled only when that timestamp has advanced. +/// +public sealed class GalaxyHierarchyCache : IGalaxyHierarchyCache +{ + private static readonly TimeSpan StaleThreshold = TimeSpan.FromMinutes(5); + + private readonly GalaxyRepository _repository; + private readonly IGalaxyDeployNotifier _notifier; + private readonly TimeProvider _timeProvider; + private readonly ILogger? _logger; + private readonly TaskCompletionSource _firstLoad = new(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly SemaphoreSlim _refreshGate = new(1, 1); + private GalaxyHierarchyCacheEntry _current = GalaxyHierarchyCacheEntry.Empty; + + public GalaxyHierarchyCache( + GalaxyRepository repository, + IGalaxyDeployNotifier notifier, + TimeProvider? timeProvider = null, + ILogger? logger = null) + { + _repository = repository; + _notifier = notifier; + _timeProvider = timeProvider ?? TimeProvider.System; + _logger = logger; + } + + public GalaxyHierarchyCacheEntry Current + { + get + { + GalaxyHierarchyCacheEntry snapshot = Volatile.Read(ref _current); + GalaxyCacheStatus projected = ProjectStatus(snapshot); + return projected == snapshot.Status ? snapshot : snapshot with { Status = projected }; + } + } + + public async Task RefreshAsync(CancellationToken cancellationToken) + { + await _refreshGate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + await RefreshCoreAsync(cancellationToken).ConfigureAwait(false); + } + finally + { + _refreshGate.Release(); + } + } + + public Task WaitForFirstLoadAsync(CancellationToken cancellationToken) + { + return _firstLoad.Task.WaitAsync(cancellationToken); + } + + private async Task RefreshCoreAsync(CancellationToken cancellationToken) + { + GalaxyHierarchyCacheEntry previous = Volatile.Read(ref _current); + DateTimeOffset queriedAt = _timeProvider.GetUtcNow(); + + try + { + DateTime? deployRaw = await _repository.GetLastDeployTimeAsync(cancellationToken).ConfigureAwait(false); + DateTimeOffset? deployTime = deployRaw.HasValue + ? new DateTimeOffset(DateTime.SpecifyKind(deployRaw.Value, DateTimeKind.Utc)) + : null; + + bool hasPriorData = previous.HasData; + bool deployChanged = !hasPriorData || deployTime != previous.LastDeployTime; + + if (!deployChanged) + { + // No deploy change — skip heavy queries; just bump LastSuccessAt. + GalaxyHierarchyCacheEntry refreshed = previous with + { + Status = GalaxyCacheStatus.Healthy, + LastQueriedAt = queriedAt, + LastSuccessAt = queriedAt, + LastError = null, + }; + Volatile.Write(ref _current, refreshed); + _firstLoad.TrySetResult(); + return; + } + + Task> hierarchyTask = _repository.GetHierarchyAsync(cancellationToken); + Task> attributesTask = _repository.GetAttributesAsync(cancellationToken); + await Task.WhenAll(hierarchyTask, attributesTask).ConfigureAwait(false); + + List hierarchy = hierarchyTask.Result; + List attributes = attributesTask.Result; + DiscoverHierarchyReply reply = BuildReply(hierarchy, attributes); + + int areaCount = hierarchy.Count(row => row.IsArea); + int historized = attributes.Count(row => row.IsHistorized); + int alarms = attributes.Count(row => row.IsAlarm); + + long nextSequence = previous.Sequence + 1; + GalaxyHierarchyCacheEntry next = new( + Status: GalaxyCacheStatus.Healthy, + Sequence: nextSequence, + LastQueriedAt: queriedAt, + LastSuccessAt: queriedAt, + LastDeployTime: deployTime, + LastError: null, + Hierarchy: hierarchy, + Attributes: attributes, + Reply: reply, + ObjectCount: hierarchy.Count, + AreaCount: areaCount, + AttributeCount: attributes.Count, + HistorizedAttributeCount: historized, + AlarmAttributeCount: alarms); + + Volatile.Write(ref _current, next); + _firstLoad.TrySetResult(); + + _notifier.Publish(new GalaxyDeployEventInfo( + Sequence: nextSequence, + ObservedAt: queriedAt, + TimeOfLastDeploy: deployTime, + ObjectCount: hierarchy.Count, + AttributeCount: attributes.Count)); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception exception) when (exception is SqlException or InvalidOperationException) + { + _logger?.LogWarning(exception, "Galaxy hierarchy cache refresh failed."); + GalaxyHierarchyCacheEntry failed = previous with + { + Status = previous.HasData ? GalaxyCacheStatus.Stale : GalaxyCacheStatus.Unavailable, + LastQueriedAt = queriedAt, + LastError = exception.Message, + }; + Volatile.Write(ref _current, failed); + _firstLoad.TrySetResult(); + } + } + + private static DiscoverHierarchyReply BuildReply( + IReadOnlyList hierarchy, + IReadOnlyList attributes) + { + Dictionary> attributesByGobjectId = attributes + .GroupBy(a => a.GobjectId) + .ToDictionary(g => g.Key, g => g.ToList()); + + DiscoverHierarchyReply reply = new(); + foreach (GalaxyHierarchyRow row in hierarchy) + { + reply.Objects.Add(GalaxyProtoMapper.MapObject(row, attributesByGobjectId)); + } + return reply; + } + + private GalaxyCacheStatus ProjectStatus(GalaxyHierarchyCacheEntry snapshot) + { + if (snapshot.Status is GalaxyCacheStatus.Unknown or GalaxyCacheStatus.Unavailable) + { + return snapshot.Status; + } + + if (snapshot.LastSuccessAt is { } success + && _timeProvider.GetUtcNow() - success > StaleThreshold) + { + return GalaxyCacheStatus.Stale; + } + + return snapshot.Status; + } +} diff --git a/src/MxGateway.Server/Galaxy/GalaxyHierarchyCacheEntry.cs b/src/MxGateway.Server/Galaxy/GalaxyHierarchyCacheEntry.cs new file mode 100644 index 0000000..3b7fcf2 --- /dev/null +++ b/src/MxGateway.Server/Galaxy/GalaxyHierarchyCacheEntry.cs @@ -0,0 +1,43 @@ +using MxGateway.Contracts.Proto.Galaxy; + +namespace MxGateway.Server.Galaxy; + +/// +/// Immutable snapshot of the Galaxy Repository browse data held by +/// . Multiple gRPC clients share the same instance — +/// the materialized is produced once per refresh and reused. +/// +public sealed record GalaxyHierarchyCacheEntry( + GalaxyCacheStatus Status, + long Sequence, + DateTimeOffset? LastQueriedAt, + DateTimeOffset? LastSuccessAt, + DateTimeOffset? LastDeployTime, + string? LastError, + IReadOnlyList Hierarchy, + IReadOnlyList Attributes, + DiscoverHierarchyReply? Reply, + int ObjectCount, + int AreaCount, + int AttributeCount, + int HistorizedAttributeCount, + int AlarmAttributeCount) +{ + public static GalaxyHierarchyCacheEntry Empty { get; } = new( + Status: GalaxyCacheStatus.Unknown, + Sequence: 0, + LastQueriedAt: null, + LastSuccessAt: null, + LastDeployTime: null, + LastError: null, + Hierarchy: Array.Empty(), + Attributes: Array.Empty(), + Reply: null, + ObjectCount: 0, + AreaCount: 0, + AttributeCount: 0, + HistorizedAttributeCount: 0, + AlarmAttributeCount: 0); + + public bool HasData => Status is GalaxyCacheStatus.Healthy or GalaxyCacheStatus.Stale; +} diff --git a/src/MxGateway.Server/Galaxy/GalaxyHierarchyRefreshService.cs b/src/MxGateway.Server/Galaxy/GalaxyHierarchyRefreshService.cs new file mode 100644 index 0000000..063039e --- /dev/null +++ b/src/MxGateway.Server/Galaxy/GalaxyHierarchyRefreshService.cs @@ -0,0 +1,56 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace MxGateway.Server.Galaxy; + +/// +/// Periodically refreshes off the request path. The +/// interval comes from ; +/// each tick is cheap when the deploy timestamp is unchanged. +/// +public sealed class GalaxyHierarchyRefreshService( + IGalaxyHierarchyCache cache, + IOptions options, + ILogger logger, + TimeProvider? timeProvider = null) : BackgroundService +{ + private readonly TimeProvider _timeProvider = timeProvider ?? TimeProvider.System; + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + TimeSpan interval = TimeSpan.FromSeconds(Math.Max(1, options.Value.DashboardRefreshIntervalSeconds)); + + try + { + await cache.RefreshAsync(stoppingToken).ConfigureAwait(false); + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + return; + } + + using PeriodicTimer timer = new(interval, _timeProvider); + try + { + while (await timer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false)) + { + try + { + await cache.RefreshAsync(stoppingToken).ConfigureAwait(false); + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + return; + } + catch (Exception exception) + { + logger.LogWarning(exception, "Galaxy hierarchy cache refresh tick failed."); + } + } + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + } + } +} diff --git a/src/MxGateway.Server/Galaxy/GalaxyHierarchyRow.cs b/src/MxGateway.Server/Galaxy/GalaxyHierarchyRow.cs new file mode 100644 index 0000000..8f0a10b --- /dev/null +++ b/src/MxGateway.Server/Galaxy/GalaxyHierarchyRow.cs @@ -0,0 +1,35 @@ +namespace MxGateway.Server.Galaxy; + +/// +/// One row from : a deployed Galaxy +/// gobject with its hierarchy parent and template-derivation chain. +/// +public sealed class GalaxyHierarchyRow +{ + public int GobjectId { get; init; } + public string TagName { get; init; } = string.Empty; + public string ContainedName { get; init; } = string.Empty; + public string BrowseName { get; init; } = string.Empty; + public int ParentGobjectId { get; init; } + public bool IsArea { get; init; } + public int CategoryId { get; init; } + public int HostedByGobjectId { get; init; } + public IReadOnlyList TemplateChain { get; init; } = Array.Empty(); +} + +/// One row from . +public sealed class GalaxyAttributeRow +{ + public int GobjectId { get; init; } + public string TagName { get; init; } = string.Empty; + public string AttributeName { get; init; } = string.Empty; + public string FullTagReference { get; init; } = string.Empty; + public int MxDataType { get; init; } + public string? DataTypeName { get; init; } + public bool IsArray { get; init; } + public int? ArrayDimension { get; init; } + public int MxAttributeCategory { get; init; } + public int SecurityClassification { get; init; } + public bool IsHistorized { get; init; } + public bool IsAlarm { get; init; } +} diff --git a/src/MxGateway.Server/Galaxy/GalaxyRepository.cs b/src/MxGateway.Server/Galaxy/GalaxyRepository.cs new file mode 100644 index 0000000..07a3a92 --- /dev/null +++ b/src/MxGateway.Server/Galaxy/GalaxyRepository.cs @@ -0,0 +1,218 @@ +using Microsoft.Data.SqlClient; + +namespace MxGateway.Server.Galaxy; + +/// +/// SQL access to the AVEVA System Platform Galaxy Repository (ZB) database. Ported from +/// the OtOpcUa project so the row sets stay byte-for-byte identical between the two +/// consumers — the same SQL drives the OPC UA server's address space and this gateway's +/// gRPC browse surface. +/// +public sealed class GalaxyRepository(GalaxyRepositoryOptions options) +{ + public async Task TestConnectionAsync(CancellationToken ct = default) + { + try + { + using SqlConnection conn = new(options.ConnectionString); + await conn.OpenAsync(ct).ConfigureAwait(false); + using SqlCommand cmd = new("SELECT 1", conn) { CommandTimeout = options.CommandTimeoutSeconds }; + object? result = await cmd.ExecuteScalarAsync(ct).ConfigureAwait(false); + return result is int i && i == 1; + } + catch (SqlException) { return false; } + catch (InvalidOperationException) { return false; } + } + + public async Task GetLastDeployTimeAsync(CancellationToken ct = default) + { + using SqlConnection conn = new(options.ConnectionString); + await conn.OpenAsync(ct).ConfigureAwait(false); + using SqlCommand cmd = new("SELECT time_of_last_deploy FROM galaxy", conn) + { CommandTimeout = options.CommandTimeoutSeconds }; + object? result = await cmd.ExecuteScalarAsync(ct).ConfigureAwait(false); + return result is DateTime dt ? dt : null; + } + + public async Task> GetHierarchyAsync(CancellationToken ct = default) + { + List rows = new(); + + using SqlConnection conn = new(options.ConnectionString); + await conn.OpenAsync(ct).ConfigureAwait(false); + + using SqlCommand cmd = new(HierarchySql, conn) { CommandTimeout = options.CommandTimeoutSeconds }; + using SqlDataReader reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false); + + while (await reader.ReadAsync(ct).ConfigureAwait(false)) + { + string templateChainRaw = reader.IsDBNull(8) ? string.Empty : reader.GetString(8); + string[] templateChain = templateChainRaw.Length == 0 + ? Array.Empty() + : templateChainRaw.Split(['|'], StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()) + .Where(s => s.Length > 0) + .ToArray(); + + rows.Add(new GalaxyHierarchyRow + { + GobjectId = Convert.ToInt32(reader.GetValue(0)), + TagName = reader.GetString(1), + ContainedName = reader.IsDBNull(2) ? string.Empty : reader.GetString(2), + BrowseName = reader.GetString(3), + ParentGobjectId = Convert.ToInt32(reader.GetValue(4)), + IsArea = Convert.ToInt32(reader.GetValue(5)) == 1, + CategoryId = Convert.ToInt32(reader.GetValue(6)), + HostedByGobjectId = Convert.ToInt32(reader.GetValue(7)), + TemplateChain = templateChain, + }); + } + return rows; + } + + public async Task> GetAttributesAsync(CancellationToken ct = default) + { + List rows = new(); + + using SqlConnection conn = new(options.ConnectionString); + await conn.OpenAsync(ct).ConfigureAwait(false); + + using SqlCommand cmd = new(AttributesSql, conn) { CommandTimeout = options.CommandTimeoutSeconds }; + using SqlDataReader reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false); + + while (await reader.ReadAsync(ct).ConfigureAwait(false)) + { + rows.Add(new GalaxyAttributeRow + { + GobjectId = Convert.ToInt32(reader.GetValue(0)), + TagName = reader.GetString(1), + AttributeName = reader.GetString(2), + FullTagReference = reader.GetString(3), + MxDataType = Convert.ToInt32(reader.GetValue(4)), + DataTypeName = reader.IsDBNull(5) ? null : reader.GetString(5), + IsArray = Convert.ToInt32(reader.GetValue(6)) == 1, + ArrayDimension = reader.IsDBNull(7) ? null : Convert.ToInt32(reader.GetValue(7)), + MxAttributeCategory = Convert.ToInt32(reader.GetValue(8)), + SecurityClassification = Convert.ToInt32(reader.GetValue(9)), + IsHistorized = Convert.ToInt32(reader.GetValue(10)) == 1, + IsAlarm = Convert.ToInt32(reader.GetValue(11)) == 1, + }); + } + return rows; + } + + private const string HierarchySql = @" +;WITH template_chain AS ( + SELECT g.gobject_id AS instance_gobject_id, t.gobject_id AS template_gobject_id, + t.tag_name AS template_tag_name, t.derived_from_gobject_id, 0 AS depth + FROM gobject g + INNER JOIN gobject t ON t.gobject_id = g.derived_from_gobject_id + WHERE g.is_template = 0 AND g.deployed_package_id <> 0 AND g.derived_from_gobject_id <> 0 + UNION ALL + SELECT tc.instance_gobject_id, t.gobject_id, t.tag_name, t.derived_from_gobject_id, tc.depth + 1 + FROM template_chain tc + INNER JOIN gobject t ON t.gobject_id = tc.derived_from_gobject_id + WHERE tc.derived_from_gobject_id <> 0 AND tc.depth < 10 +) +SELECT DISTINCT + g.gobject_id, + g.tag_name, + g.contained_name, + CASE WHEN g.contained_name IS NULL OR g.contained_name = '' + THEN g.tag_name + ELSE g.contained_name + END AS browse_name, + CASE WHEN g.contained_by_gobject_id = 0 + THEN g.area_gobject_id + ELSE g.contained_by_gobject_id + END AS parent_gobject_id, + CASE WHEN td.category_id = 13 + THEN 1 + ELSE 0 + END AS is_area, + td.category_id AS category_id, + g.hosted_by_gobject_id AS hosted_by_gobject_id, + ISNULL( + STUFF(( + SELECT '|' + tc.template_tag_name + FROM template_chain tc + WHERE tc.instance_gobject_id = g.gobject_id + ORDER BY tc.depth + FOR XML PATH('') + ), 1, 1, ''), + '' + ) AS template_chain +FROM gobject g +INNER JOIN template_definition td + ON g.template_definition_id = td.template_definition_id +WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26) + AND g.is_template = 0 + AND g.deployed_package_id <> 0 +ORDER BY parent_gobject_id, g.tag_name"; + + private const string AttributesSql = @" +;WITH deployed_package_chain AS ( + SELECT g.gobject_id, p.package_id, p.derived_from_package_id, 0 AS depth + FROM gobject g + INNER JOIN package p ON p.package_id = g.deployed_package_id + WHERE g.is_template = 0 AND g.deployed_package_id <> 0 + UNION ALL + SELECT dpc.gobject_id, p.package_id, p.derived_from_package_id, dpc.depth + 1 + FROM deployed_package_chain dpc + INNER JOIN package p ON p.package_id = dpc.derived_from_package_id + WHERE dpc.derived_from_package_id <> 0 AND dpc.depth < 10 +) +SELECT gobject_id, tag_name, attribute_name, full_tag_reference, + mx_data_type, data_type_name, is_array, array_dimension, + mx_attribute_category, security_classification, is_historized, is_alarm +FROM ( + SELECT + dpc.gobject_id, + g.tag_name, + da.attribute_name, + g.tag_name + '.' + da.attribute_name + + CASE WHEN da.is_array = 1 THEN '[]' ELSE '' END + AS full_tag_reference, + da.mx_data_type, + dt.description AS data_type_name, + da.is_array, + CASE WHEN da.is_array = 1 + THEN CONVERT(int, CONVERT(varbinary(2), + SUBSTRING(da.mx_value, 15, 2) + SUBSTRING(da.mx_value, 13, 2), 2)) + ELSE NULL + END AS array_dimension, + da.mx_attribute_category, + da.security_classification, + CASE WHEN EXISTS ( + SELECT 1 FROM deployed_package_chain dpc2 + INNER JOIN primitive_instance pi ON pi.package_id = dpc2.package_id AND pi.primitive_name = da.attribute_name + INNER JOIN primitive_definition pd ON pd.primitive_definition_id = pi.primitive_definition_id AND pd.primitive_name = 'HistoryExtension' + WHERE dpc2.gobject_id = dpc.gobject_id + ) THEN 1 ELSE 0 END AS is_historized, + CASE WHEN EXISTS ( + SELECT 1 FROM deployed_package_chain dpc2 + INNER JOIN primitive_instance pi ON pi.package_id = dpc2.package_id AND pi.primitive_name = da.attribute_name + INNER JOIN primitive_definition pd ON pd.primitive_definition_id = pi.primitive_definition_id AND pd.primitive_name = 'AlarmExtension' + WHERE dpc2.gobject_id = dpc.gobject_id + ) THEN 1 ELSE 0 END AS is_alarm, + ROW_NUMBER() OVER ( + PARTITION BY dpc.gobject_id, da.attribute_name + ORDER BY dpc.depth + ) AS rn + FROM deployed_package_chain dpc + INNER JOIN dynamic_attribute da + ON da.package_id = dpc.package_id + INNER JOIN gobject g + ON g.gobject_id = dpc.gobject_id + INNER JOIN template_definition td + ON td.template_definition_id = g.template_definition_id + LEFT JOIN data_type dt + ON dt.mx_data_type = da.mx_data_type + WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26) + AND da.attribute_name NOT LIKE '[_]%' + AND da.attribute_name NOT LIKE '%.Description' + AND da.mx_attribute_category IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 24) +) ranked +WHERE rn = 1 +ORDER BY tag_name, attribute_name"; +} diff --git a/src/MxGateway.Server/Galaxy/GalaxyRepositoryOptions.cs b/src/MxGateway.Server/Galaxy/GalaxyRepositoryOptions.cs new file mode 100644 index 0000000..e2e12c9 --- /dev/null +++ b/src/MxGateway.Server/Galaxy/GalaxyRepositoryOptions.cs @@ -0,0 +1,21 @@ +namespace MxGateway.Server.Galaxy; + +/// +/// Connection settings for the AVEVA System Platform Galaxy Repository (ZB) database. +/// Bound to the MxGateway:Galaxy configuration section. +/// +public sealed class GalaxyRepositoryOptions +{ + public const string SectionName = "MxGateway:Galaxy"; + + public string ConnectionString { get; init; } = + "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;"; + + public int CommandTimeoutSeconds { get; init; } = 60; + + /// + /// Interval (seconds) between background refreshes of the dashboard Galaxy summary + /// cache. SQL is hit at most once per interval regardless of dashboard render rate. + /// + public int DashboardRefreshIntervalSeconds { get; init; } = 30; +} diff --git a/src/MxGateway.Server/Galaxy/GalaxyRepositoryServiceCollectionExtensions.cs b/src/MxGateway.Server/Galaxy/GalaxyRepositoryServiceCollectionExtensions.cs new file mode 100644 index 0000000..dcff412 --- /dev/null +++ b/src/MxGateway.Server/Galaxy/GalaxyRepositoryServiceCollectionExtensions.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Options; + +namespace MxGateway.Server.Galaxy; + +public static class GalaxyRepositoryServiceCollectionExtensions +{ + public static IServiceCollection AddGalaxyRepository(this IServiceCollection services) + { + services + .AddOptions() + .BindConfiguration(GalaxyRepositoryOptions.SectionName) + .ValidateOnStart(); + + services.AddSingleton(sp => + new GalaxyRepository(sp.GetRequiredService>().Value)); + + services.AddSingleton(); + services.AddSingleton(); + services.AddHostedService(); + + return services; + } +} diff --git a/src/MxGateway.Server/Galaxy/IGalaxyDeployNotifier.cs b/src/MxGateway.Server/Galaxy/IGalaxyDeployNotifier.cs new file mode 100644 index 0000000..85593f9 --- /dev/null +++ b/src/MxGateway.Server/Galaxy/IGalaxyDeployNotifier.cs @@ -0,0 +1,18 @@ +namespace MxGateway.Server.Galaxy; + +public interface IGalaxyDeployNotifier +{ + /// The most recently published event, or null if no event has fired yet. + GalaxyDeployEventInfo? Latest { get; } + + /// Publishes a deploy event to all current subscribers and stores it as . + void Publish(GalaxyDeployEventInfo info); + + /// + /// Subscribe to deploy events. The async sequence yields events as they fire. If + /// is set, it is yielded first so subscribers can bootstrap their + /// local cache without waiting for the next deploy. Pass a cancellation token to + /// unsubscribe. + /// + IAsyncEnumerable SubscribeAsync(CancellationToken cancellationToken); +} diff --git a/src/MxGateway.Server/Galaxy/IGalaxyHierarchyCache.cs b/src/MxGateway.Server/Galaxy/IGalaxyHierarchyCache.cs new file mode 100644 index 0000000..301edee --- /dev/null +++ b/src/MxGateway.Server/Galaxy/IGalaxyHierarchyCache.cs @@ -0,0 +1,22 @@ +namespace MxGateway.Server.Galaxy; + +public interface IGalaxyHierarchyCache +{ + /// The latest cache entry. Status freshness is recomputed against the clock. + GalaxyHierarchyCacheEntry Current { get; } + + /// + /// Forces a refresh against the Galaxy Repository. Performs a cheap + /// time_of_last_deploy probe first and only re-queries the heavy hierarchy + + /// attributes rowsets when the deploy time has changed since the last successful + /// refresh. + /// + Task RefreshAsync(CancellationToken cancellationToken); + + /// + /// Awaits the first completed refresh attempt (success or failure). Useful for + /// gRPC handlers that want to serve from cache without returning Unavailable on the + /// very first request after gateway start. + /// + Task WaitForFirstLoadAsync(CancellationToken cancellationToken); +} diff --git a/src/MxGateway.Server/GatewayApplication.cs b/src/MxGateway.Server/GatewayApplication.cs index 546a003..53b8c0d 100644 --- a/src/MxGateway.Server/GatewayApplication.cs +++ b/src/MxGateway.Server/GatewayApplication.cs @@ -3,6 +3,7 @@ using MxGateway.Contracts; using MxGateway.Server.Configuration; using MxGateway.Server.Dashboard; using MxGateway.Server.Diagnostics; +using MxGateway.Server.Galaxy; using MxGateway.Server.Grpc; using MxGateway.Server.Metrics; using MxGateway.Server.Security.Authentication; @@ -51,6 +52,7 @@ public static class GatewayApplication builder.Services.AddWorkerProcessLauncher(); builder.Services.AddGatewaySessions(); builder.Services.AddGatewayDashboard(); + builder.Services.AddGalaxyRepository(); return builder; } @@ -125,6 +127,7 @@ public static class GatewayApplication .WithName("LiveHealth"); endpoints.MapGrpcService(); + endpoints.MapGrpcService(); endpoints.MapGatewayDashboard(); return endpoints; diff --git a/src/MxGateway.Server/Grpc/GalaxyProtoMapper.cs b/src/MxGateway.Server/Grpc/GalaxyProtoMapper.cs new file mode 100644 index 0000000..3ca660d --- /dev/null +++ b/src/MxGateway.Server/Grpc/GalaxyProtoMapper.cs @@ -0,0 +1,69 @@ +using MxGateway.Contracts.Proto.Galaxy; +using MxGateway.Server.Galaxy; + +namespace MxGateway.Server.Grpc; + +/// +/// Maps + rows produced +/// by into galaxy_repository.v1 proto messages. +/// Pure function, separated so it can be unit-tested without a SQL connection. +/// +public static class GalaxyProtoMapper +{ + public static IEnumerable MapHierarchy( + IReadOnlyList hierarchy, + IReadOnlyList attributes) + { + Dictionary> attributesByGobjectId = attributes + .GroupBy(a => a.GobjectId) + .ToDictionary(g => g.Key, g => g.ToList()); + + foreach (GalaxyHierarchyRow row in hierarchy) + { + yield return MapObject(row, attributesByGobjectId); + } + } + + public static GalaxyObject MapObject( + GalaxyHierarchyRow row, + IReadOnlyDictionary> attributesByGobjectId) + { + GalaxyObject obj = new() + { + GobjectId = row.GobjectId, + TagName = row.TagName, + ContainedName = row.ContainedName, + BrowseName = row.BrowseName, + ParentGobjectId = row.ParentGobjectId, + IsArea = row.IsArea, + CategoryId = row.CategoryId, + HostedByGobjectId = row.HostedByGobjectId, + }; + obj.TemplateChain.AddRange(row.TemplateChain); + + if (attributesByGobjectId.TryGetValue(row.GobjectId, out List? attrs)) + { + foreach (GalaxyAttributeRow attr in attrs) + { + obj.Attributes.Add(MapAttribute(attr)); + } + } + + return obj; + } + + public static GalaxyAttribute MapAttribute(GalaxyAttributeRow row) => new() + { + AttributeName = row.AttributeName, + FullTagReference = row.FullTagReference, + MxDataType = row.MxDataType, + DataTypeName = row.DataTypeName ?? string.Empty, + IsArray = row.IsArray, + ArrayDimension = row.ArrayDimension ?? 0, + ArrayDimensionPresent = row.ArrayDimension.HasValue, + MxAttributeCategory = row.MxAttributeCategory, + SecurityClassification = row.SecurityClassification, + IsHistorized = row.IsHistorized, + IsAlarm = row.IsAlarm, + }; +} diff --git a/src/MxGateway.Server/Grpc/GalaxyRepositoryGrpcService.cs b/src/MxGateway.Server/Grpc/GalaxyRepositoryGrpcService.cs new file mode 100644 index 0000000..68ed9f1 --- /dev/null +++ b/src/MxGateway.Server/Grpc/GalaxyRepositoryGrpcService.cs @@ -0,0 +1,158 @@ +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; +using Microsoft.Data.SqlClient; +using MxGateway.Contracts.Proto.Galaxy; +using GalaxyDb = MxGateway.Server.Galaxy; +using ProtoGalaxyRepository = MxGateway.Contracts.Proto.Galaxy.GalaxyRepository; + +namespace MxGateway.Server.Grpc; + +/// +/// gRPC surface that exposes the Galaxy Repository to clients. DiscoverHierarchy +/// and GetLastDeployTime serve from +/// so many clients share a single SQL pull. WatchDeployEvents streams events +/// from . TestConnection remains a +/// direct SQL probe since callers use it as a health check. +/// +public sealed class GalaxyRepositoryGrpcService( + GalaxyDb.GalaxyRepository repository, + GalaxyDb.IGalaxyHierarchyCache cache, + GalaxyDb.IGalaxyDeployNotifier notifier, + ILogger logger) : ProtoGalaxyRepository.GalaxyRepositoryBase +{ + private static readonly TimeSpan FirstLoadWaitBudget = TimeSpan.FromSeconds(5); + + public override async Task TestConnection( + TestConnectionRequest request, + ServerCallContext context) + { + bool ok = await repository.TestConnectionAsync(context.CancellationToken).ConfigureAwait(false); + return new TestConnectionReply { Ok = ok }; + } + + public override async Task GetLastDeployTime( + GetLastDeployTimeRequest request, + ServerCallContext context) + { + await WaitForCacheBootstrap(context.CancellationToken).ConfigureAwait(false); + GalaxyDb.GalaxyHierarchyCacheEntry entry = cache.Current; + + if (!entry.HasData) + { + throw new RpcException(new Status( + StatusCode.Unavailable, + ResolveUnavailableMessage(entry))); + } + + GetLastDeployTimeReply reply = new() { Present = entry.LastDeployTime.HasValue }; + if (entry.LastDeployTime.HasValue) + { + reply.TimeOfLastDeploy = Timestamp.FromDateTimeOffset(entry.LastDeployTime.Value); + } + return reply; + } + + public override async Task DiscoverHierarchy( + DiscoverHierarchyRequest request, + ServerCallContext context) + { + await WaitForCacheBootstrap(context.CancellationToken).ConfigureAwait(false); + GalaxyDb.GalaxyHierarchyCacheEntry entry = cache.Current; + + if (!entry.HasData || entry.Reply is null) + { + throw new RpcException(new Status( + StatusCode.Unavailable, + ResolveUnavailableMessage(entry))); + } + + // Same materialized reply is shared across all clients — gRPC serialization is + // read-only and the entry is replaced atomically on the next refresh. + return entry.Reply; + } + + public override async Task WatchDeployEvents( + WatchDeployEventsRequest request, + IServerStreamWriter responseStream, + ServerCallContext context) + { + DateTimeOffset? lastSeen = request.LastSeenDeployTime?.ToDateTimeOffset(); + + await foreach (GalaxyDb.GalaxyDeployEventInfo info in notifier + .SubscribeAsync(context.CancellationToken) + .ConfigureAwait(false)) + { + // Suppress the initial bootstrap event when the client already knows about + // this deploy time. We only suppress the first one — subsequent events fire + // on actual changes, so they always pass. + if (lastSeen is { } seen && info.TimeOfLastDeploy == seen) + { + lastSeen = null; + continue; + } + lastSeen = null; + + await responseStream.WriteAsync(MapDeployEvent(info), context.CancellationToken).ConfigureAwait(false); + } + } + + private async Task WaitForCacheBootstrap(CancellationToken cancellationToken) + { + if (cache.Current.HasData || cache.Current.Status == GalaxyDb.GalaxyCacheStatus.Unavailable) + { + return; + } + + using CancellationTokenSource budget = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + budget.CancelAfter(FirstLoadWaitBudget); + try + { + await cache.WaitForFirstLoadAsync(budget.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (OperationCanceledException) + { + // Budget elapsed; fall through and let the caller see the current + // (possibly Unknown/Unavailable) entry. + } + } + + private static DeployEvent MapDeployEvent(GalaxyDb.GalaxyDeployEventInfo info) + { + DeployEvent ev = new() + { + Sequence = (ulong)info.Sequence, + ObservedAt = Timestamp.FromDateTimeOffset(info.ObservedAt), + ObjectCount = info.ObjectCount, + AttributeCount = info.AttributeCount, + TimeOfLastDeployPresent = info.TimeOfLastDeploy.HasValue, + }; + if (info.TimeOfLastDeploy.HasValue) + { + ev.TimeOfLastDeploy = Timestamp.FromDateTimeOffset(info.TimeOfLastDeploy.Value); + } + return ev; + } + + private static string ResolveUnavailableMessage(GalaxyDb.GalaxyHierarchyCacheEntry entry) => entry.Status switch + { + GalaxyDb.GalaxyCacheStatus.Unknown => "Galaxy cache has not completed its initial load yet.", + GalaxyDb.GalaxyCacheStatus.Unavailable => "Galaxy repository is unavailable.", + _ => "Galaxy cache has no data available.", + }; + + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Style", + "IDE0051:Remove unused private members", + Justification = "Kept for parity with prior SQL exception mapping; future direct-SQL paths reuse it.")] + private RpcException MapSqlException(SqlException exception) + { + logger.LogWarning(exception, "Galaxy repository query failed."); + return new RpcException(new Status( + StatusCode.Unavailable, + "Galaxy repository is unavailable.")); + } +} diff --git a/src/MxGateway.Server/Grpc/MxAccessGatewayService.cs b/src/MxGateway.Server/Grpc/MxAccessGatewayService.cs index 75cc9c3..53f7f0f 100644 --- a/src/MxGateway.Server/Grpc/MxAccessGatewayService.cs +++ b/src/MxGateway.Server/Grpc/MxAccessGatewayService.cs @@ -1,6 +1,8 @@ +using System.Diagnostics; using Grpc.Core; using MxGateway.Contracts; using MxGateway.Contracts.Proto; +using MxGateway.Server.Metrics; using MxGateway.Server.Security.Authorization; using MxGateway.Server.Sessions; using MxGateway.Server.Workers; @@ -13,6 +15,7 @@ public sealed class MxAccessGatewayService( MxAccessGrpcRequestValidator requestValidator, MxAccessGrpcMapper mapper, IEventStreamService eventStreamService, + GatewayMetrics metrics, ILogger logger) : MxAccessGateway.MxAccessGatewayBase { public override async Task OpenSession( @@ -110,7 +113,9 @@ public sealed class MxAccessGatewayService( .WithCancellation(context.CancellationToken) .ConfigureAwait(false)) { + Stopwatch stopwatch = Stopwatch.StartNew(); await responseStream.WriteAsync(publicEvent).ConfigureAwait(false); + metrics.RecordEventStreamSend(publicEvent.Family.ToString(), stopwatch.Elapsed); } } catch (Exception exception) when (exception is not RpcException) diff --git a/src/MxGateway.Server/Metrics/GatewayMetrics.cs b/src/MxGateway.Server/Metrics/GatewayMetrics.cs index ab78cae..af9c0c0 100644 --- a/src/MxGateway.Server/Metrics/GatewayMetrics.cs +++ b/src/MxGateway.Server/Metrics/GatewayMetrics.cs @@ -219,19 +219,6 @@ public sealed class GatewayMetrics : IDisposable } } - public void SetGrpcEventStreamQueueDepth(int depth) - { - if (depth < 0) - { - throw new ArgumentOutOfRangeException(nameof(depth), depth, "Queue depth cannot be negative."); - } - - lock (_syncRoot) - { - _grpcEventStreamQueueDepth = depth; - } - } - public void AdjustGrpcEventStreamQueueDepth(int delta) { lock (_syncRoot) diff --git a/src/MxGateway.Server/MxGateway.Server.csproj b/src/MxGateway.Server/MxGateway.Server.csproj index bcb3bf5..23afd1b 100644 --- a/src/MxGateway.Server/MxGateway.Server.csproj +++ b/src/MxGateway.Server/MxGateway.Server.csproj @@ -7,6 +7,7 @@ + diff --git a/src/MxGateway.Server/Security/Authorization/GatewayGrpcScopeResolver.cs b/src/MxGateway.Server/Security/Authorization/GatewayGrpcScopeResolver.cs index c315ab4..f5488f6 100644 --- a/src/MxGateway.Server/Security/Authorization/GatewayGrpcScopeResolver.cs +++ b/src/MxGateway.Server/Security/Authorization/GatewayGrpcScopeResolver.cs @@ -1,4 +1,5 @@ using MxGateway.Contracts.Proto; +using MxGateway.Contracts.Proto.Galaxy; namespace MxGateway.Server.Security.Authorization; @@ -12,6 +13,10 @@ public sealed class GatewayGrpcScopeResolver CloseSessionRequest => GatewayScopes.SessionClose, StreamEventsRequest => GatewayScopes.EventsRead, MxCommandRequest commandRequest => ResolveCommandScope(commandRequest.Command?.Kind ?? MxCommandKind.Unspecified), + TestConnectionRequest or + GetLastDeployTimeRequest or + DiscoverHierarchyRequest or + WatchDeployEventsRequest => GatewayScopes.MetadataRead, _ => GatewayScopes.Admin }; } diff --git a/src/MxGateway.Server/appsettings.json b/src/MxGateway.Server/appsettings.json index 5998298..f231d1a 100644 --- a/src/MxGateway.Server/appsettings.json +++ b/src/MxGateway.Server/appsettings.json @@ -43,6 +43,11 @@ }, "Protocol": { "WorkerProtocolVersion": 1 + }, + "Galaxy": { + "ConnectionString": "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;", + "CommandTimeoutSeconds": 60, + "DashboardRefreshIntervalSeconds": 30 } } } diff --git a/src/MxGateway.Tests/Galaxy/GalaxyDeployNotifierTests.cs b/src/MxGateway.Tests/Galaxy/GalaxyDeployNotifierTests.cs new file mode 100644 index 0000000..ca34766 --- /dev/null +++ b/src/MxGateway.Tests/Galaxy/GalaxyDeployNotifierTests.cs @@ -0,0 +1,90 @@ +using MxGateway.Server.Galaxy; + +namespace MxGateway.Tests.Galaxy; + +public sealed class GalaxyDeployNotifierTests +{ + [Fact] + public async Task SubscribeAsync_NoLatestEvent_BlocksUntilPublish() + { + GalaxyDeployNotifier notifier = new(); + using CancellationTokenSource cts = new(); + + IAsyncEnumerator enumerator = notifier + .SubscribeAsync(cts.Token) + .GetAsyncEnumerator(cts.Token); + + ValueTask moveNext = enumerator.MoveNextAsync(); + Assert.False(moveNext.IsCompleted); + + GalaxyDeployEventInfo published = new( + Sequence: 1, + ObservedAt: DateTimeOffset.UtcNow, + TimeOfLastDeploy: DateTimeOffset.UtcNow, + ObjectCount: 5, + AttributeCount: 25); + notifier.Publish(published); + + Assert.True(await moveNext.AsTask().WaitAsync(TimeSpan.FromSeconds(1))); + Assert.Same(published, enumerator.Current); + + await cts.CancelAsync(); + await enumerator.DisposeAsync(); + } + + [Fact] + public async Task SubscribeAsync_WithLatestEvent_BootstrapsImmediately() + { + GalaxyDeployNotifier notifier = new(); + GalaxyDeployEventInfo first = new(1, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, 3, 9); + notifier.Publish(first); + + using CancellationTokenSource cts = new(); + await using IAsyncEnumerator enumerator = notifier + .SubscribeAsync(cts.Token) + .GetAsyncEnumerator(cts.Token); + + Assert.True(await enumerator.MoveNextAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(1))); + Assert.Same(first, enumerator.Current); + + await cts.CancelAsync(); + } + + [Fact] + public async Task Publish_FansOutToAllSubscribers() + { + GalaxyDeployNotifier notifier = new(); + using CancellationTokenSource cts = new(); + + await using IAsyncEnumerator a = notifier + .SubscribeAsync(cts.Token) + .GetAsyncEnumerator(cts.Token); + await using IAsyncEnumerator b = notifier + .SubscribeAsync(cts.Token) + .GetAsyncEnumerator(cts.Token); + + GalaxyDeployEventInfo info = new(1, DateTimeOffset.UtcNow, null, 0, 0); + notifier.Publish(info); + + Assert.True(await a.MoveNextAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(1))); + Assert.True(await b.MoveNextAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(1))); + Assert.Same(info, a.Current); + Assert.Same(info, b.Current); + + await cts.CancelAsync(); + } + + [Fact] + public void Latest_TracksMostRecentPublish() + { + GalaxyDeployNotifier notifier = new(); + Assert.Null(notifier.Latest); + + GalaxyDeployEventInfo first = new(1, DateTimeOffset.UtcNow, null, 0, 0); + GalaxyDeployEventInfo second = new(2, DateTimeOffset.UtcNow, null, 0, 0); + notifier.Publish(first); + notifier.Publish(second); + + Assert.Same(second, notifier.Latest); + } +} diff --git a/src/MxGateway.Tests/Galaxy/GalaxyHierarchyCacheTests.cs b/src/MxGateway.Tests/Galaxy/GalaxyHierarchyCacheTests.cs new file mode 100644 index 0000000..5e3558d --- /dev/null +++ b/src/MxGateway.Tests/Galaxy/GalaxyHierarchyCacheTests.cs @@ -0,0 +1,74 @@ +using MxGateway.Server.Galaxy; + +namespace MxGateway.Tests.Galaxy; + +public sealed class GalaxyHierarchyCacheTests +{ + [Fact] + public void Current_BeforeAnyRefresh_ReturnsEmpty() + { + GalaxyDeployNotifier notifier = new(); + GalaxyHierarchyCache cache = CreateCache(notifier, new ManualTimeProvider()); + + GalaxyHierarchyCacheEntry entry = cache.Current; + + Assert.Equal(GalaxyCacheStatus.Unknown, entry.Status); + Assert.False(entry.HasData); + Assert.Equal(0, entry.ObjectCount); + Assert.Null(entry.Reply); + } + + [Fact] + public async Task RefreshAsync_WhenSqlIsUnreachable_MarksUnavailableAndDoesNotPublish() + { + GalaxyDeployNotifier notifier = new(); + ManualTimeProvider clock = new(DateTimeOffset.Parse("2026-04-28T12:00:00Z")); + GalaxyHierarchyCache cache = CreateCache(notifier, clock); + + await cache.RefreshAsync(CancellationToken.None); + + Assert.Equal(GalaxyCacheStatus.Unavailable, cache.Current.Status); + Assert.False(string.IsNullOrWhiteSpace(cache.Current.LastError)); + Assert.Null(notifier.Latest); + Assert.True(cache.WaitForFirstLoadAsync(CancellationToken.None).IsCompletedSuccessfully); + } + + [Fact] + public void HasData_OnHealthyEntry_IsTrue() + { + GalaxyHierarchyCacheEntry entry = GalaxyHierarchyCacheEntry.Empty with + { + Status = GalaxyCacheStatus.Healthy, + LastSuccessAt = DateTimeOffset.UtcNow, + ObjectCount = 1, + }; + + Assert.True(entry.HasData); + } + + [Fact] + public void HasData_OnUnknownEntry_IsFalse() + { + Assert.False(GalaxyHierarchyCacheEntry.Empty.HasData); + } + + private static GalaxyHierarchyCache CreateCache(GalaxyDeployNotifier notifier, TimeProvider clock) + { + GalaxyRepositoryOptions options = new() + { + ConnectionString = "Server=127.0.0.1,65500;Database=ZB;Connection Timeout=1;Encrypt=False;", + CommandTimeoutSeconds = 1, + }; + GalaxyRepository repository = new(options); + return new GalaxyHierarchyCache(repository, notifier, clock); + } + + private sealed class ManualTimeProvider(DateTimeOffset start = default) : TimeProvider + { + private DateTimeOffset _now = start == default ? DateTimeOffset.UtcNow : start; + + public override DateTimeOffset GetUtcNow() => _now; + + public void Advance(TimeSpan duration) => _now += duration; + } +} diff --git a/src/MxGateway.Tests/Galaxy/GalaxyProtoMapperTests.cs b/src/MxGateway.Tests/Galaxy/GalaxyProtoMapperTests.cs new file mode 100644 index 0000000..2a6ecc6 --- /dev/null +++ b/src/MxGateway.Tests/Galaxy/GalaxyProtoMapperTests.cs @@ -0,0 +1,109 @@ +using MxGateway.Contracts.Proto.Galaxy; +using MxGateway.Server.Galaxy; +using MxGateway.Server.Grpc; + +namespace MxGateway.Tests.Galaxy; + +public sealed class GalaxyProtoMapperTests +{ + [Fact] + public void MapAttribute_PreservesAllScalarFields() + { + GalaxyAttributeRow row = new() + { + GobjectId = 42, + TagName = "Pump_001", + AttributeName = "Speed", + FullTagReference = "Pump_001.Speed", + MxDataType = 3, + DataTypeName = "Float", + IsArray = false, + ArrayDimension = null, + MxAttributeCategory = 5, + SecurityClassification = 2, + IsHistorized = true, + IsAlarm = false, + }; + + GalaxyAttribute proto = GalaxyProtoMapper.MapAttribute(row); + + Assert.Equal("Speed", proto.AttributeName); + Assert.Equal("Pump_001.Speed", proto.FullTagReference); + Assert.Equal(3, proto.MxDataType); + Assert.Equal("Float", proto.DataTypeName); + Assert.False(proto.IsArray); + Assert.Equal(0, proto.ArrayDimension); + Assert.False(proto.ArrayDimensionPresent); + Assert.Equal(5, proto.MxAttributeCategory); + Assert.Equal(2, proto.SecurityClassification); + Assert.True(proto.IsHistorized); + Assert.False(proto.IsAlarm); + } + + [Fact] + public void MapAttribute_ArrayDimensionPresentFlag_DistinguishesNullFromZero() + { + GalaxyAttributeRow withDim = new() { ArrayDimension = 0, IsArray = true }; + GalaxyAttributeRow withoutDim = new() { ArrayDimension = null, IsArray = false }; + + Assert.True(GalaxyProtoMapper.MapAttribute(withDim).ArrayDimensionPresent); + Assert.Equal(0, GalaxyProtoMapper.MapAttribute(withDim).ArrayDimension); + + Assert.False(GalaxyProtoMapper.MapAttribute(withoutDim).ArrayDimensionPresent); + Assert.Equal(0, GalaxyProtoMapper.MapAttribute(withoutDim).ArrayDimension); + } + + [Fact] + public void MapAttribute_NullDataTypeName_BecomesEmptyString() + { + GalaxyAttributeRow row = new() { DataTypeName = null }; + + GalaxyAttribute proto = GalaxyProtoMapper.MapAttribute(row); + + Assert.Equal(string.Empty, proto.DataTypeName); + } + + [Fact] + public void MapHierarchy_GroupsAttributesByGobjectId() + { + List hierarchy = + [ + new() { GobjectId = 1, TagName = "A", BrowseName = "A", TemplateChain = ["RootTpl"] }, + new() { GobjectId = 2, TagName = "B", BrowseName = "B", ParentGobjectId = 1 }, + new() { GobjectId = 3, TagName = "C", BrowseName = "C", ParentGobjectId = 1 }, + ]; + List attributes = + [ + new() { GobjectId = 1, AttributeName = "X", FullTagReference = "A.X" }, + new() { GobjectId = 2, AttributeName = "Y1", FullTagReference = "B.Y1" }, + new() { GobjectId = 2, AttributeName = "Y2", FullTagReference = "B.Y2" }, + ]; + + List result = GalaxyProtoMapper.MapHierarchy(hierarchy, attributes).ToList(); + + Assert.Equal(3, result.Count); + Assert.Single(result[0].Attributes); + Assert.Equal("X", result[0].Attributes[0].AttributeName); + Assert.Equal(2, result[1].Attributes.Count); + Assert.Empty(result[2].Attributes); + } + + [Fact] + public void MapObject_CopiesTemplateChain() + { + GalaxyHierarchyRow row = new() + { + GobjectId = 5, + TagName = "Engine_001", + ContainedName = "Engine", + BrowseName = "Engine", + TemplateChain = ["EngineTpl", "AppEngineBase"], + }; + + GalaxyObject proto = GalaxyProtoMapper.MapObject( + row, + new Dictionary>()); + + Assert.Equal(new[] { "EngineTpl", "AppEngineBase" }, proto.TemplateChain); + } +} diff --git a/src/MxGateway.Tests/Gateway/Dashboard/DashboardSnapshotServiceTests.cs b/src/MxGateway.Tests/Gateway/Dashboard/DashboardSnapshotServiceTests.cs index 2e838de..3117109 100644 --- a/src/MxGateway.Tests/Gateway/Dashboard/DashboardSnapshotServiceTests.cs +++ b/src/MxGateway.Tests/Gateway/Dashboard/DashboardSnapshotServiceTests.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Options; using MxGateway.Contracts.Proto; using MxGateway.Server.Configuration; using MxGateway.Server.Dashboard; +using MxGateway.Server.Galaxy; using MxGateway.Server.Metrics; using MxGateway.Server.Sessions; using MxGateway.Server.Workers; @@ -171,6 +172,51 @@ public sealed class DashboardSnapshotServiceTests Assert.Equal("session-newer", Assert.Single(snapshot.Faults).SessionId); } + [Fact] + public void GetSnapshot_ProjectsGalaxySummaryFromHierarchyCache() + { + GalaxyHierarchyCacheEntry entry = GalaxyHierarchyCacheEntry.Empty with + { + Status = GalaxyCacheStatus.Healthy, + Sequence = 7, + LastQueriedAt = DateTimeOffset.Parse("2026-04-28T11:30:00Z"), + LastSuccessAt = DateTimeOffset.Parse("2026-04-28T11:30:00Z"), + LastDeployTime = DateTimeOffset.Parse("2026-04-28T09:00:00Z"), + Hierarchy = + [ + new GalaxyHierarchyRow { GobjectId = 1, TagName = "Pump_001", BrowseName = "Pump_001", CategoryId = 10, IsArea = false, TemplateChain = ["$Pump"] }, + new GalaxyHierarchyRow { GobjectId = 2, TagName = "Pump_002", BrowseName = "Pump_002", CategoryId = 10, IsArea = false, TemplateChain = ["$Pump"] }, + new GalaxyHierarchyRow { GobjectId = 3, TagName = "Area_A", BrowseName = "Area_A", CategoryId = 13, IsArea = true, TemplateChain = ["$Area"] }, + ], + Attributes = + [ + new GalaxyAttributeRow { GobjectId = 1, AttributeName = "Speed", IsHistorized = true }, + new GalaxyAttributeRow { GobjectId = 1, AttributeName = "Status", IsAlarm = true }, + ], + ObjectCount = 3, + AreaCount = 1, + AttributeCount = 2, + HistorizedAttributeCount = 1, + AlarmAttributeCount = 1, + }; + using GatewayMetrics metrics = new(); + DashboardSnapshotService service = CreateService( + new SessionRegistry(), + metrics, + galaxyHierarchyCache: new StubGalaxyHierarchyCache(entry)); + + DashboardSnapshot snapshot = service.GetSnapshot(); + + Assert.Equal(DashboardGalaxyStatus.Healthy, snapshot.Galaxy.Status); + Assert.Equal(3, snapshot.Galaxy.ObjectCount); + Assert.Equal(1, snapshot.Galaxy.AreaCount); + Assert.Equal(2, snapshot.Galaxy.AttributeCount); + Assert.Equal("$Pump", Assert.Single(snapshot.Galaxy.TopTemplates, t => t.TemplateName == "$Pump").TemplateName); + Assert.Equal(2, snapshot.Galaxy.TopTemplates.First(t => t.TemplateName == "$Pump").InstanceCount); + Assert.Contains(snapshot.Galaxy.ObjectCategories, c => c.CategoryName == "UserDefined" && c.ObjectCount == 2); + Assert.Contains(snapshot.Galaxy.ObjectCategories, c => c.CategoryName == "Area" && c.ObjectCount == 1); + } + [Fact] public async Task WatchSnapshotsAsync_WhenSubscriberCancels_DisposesCleanly() { @@ -200,7 +246,8 @@ public sealed class DashboardSnapshotServiceTests private static DashboardSnapshotService CreateService( SessionRegistry registry, GatewayMetrics metrics, - GatewayOptions? options = null) + GatewayOptions? options = null, + IGalaxyHierarchyCache? galaxyHierarchyCache = null) { GatewayOptions resolvedOptions = options ?? new GatewayOptions { @@ -215,9 +262,19 @@ public sealed class DashboardSnapshotServiceTests registry, metrics, configurationProvider, + galaxyHierarchyCache ?? new StubGalaxyHierarchyCache(GalaxyHierarchyCacheEntry.Empty), Options.Create(resolvedOptions)); } + private sealed class StubGalaxyHierarchyCache(GalaxyHierarchyCacheEntry current) : IGalaxyHierarchyCache + { + public GalaxyHierarchyCacheEntry Current { get; } = current; + + public Task RefreshAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task WaitForFirstLoadAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } + private static GatewaySession CreateSession( string sessionId, string? clientIdentity, diff --git a/src/MxGateway.Tests/Gateway/GatewayEndToEndFakeWorkerSmokeTests.cs b/src/MxGateway.Tests/Gateway/GatewayEndToEndFakeWorkerSmokeTests.cs index 7374035..274ac67 100644 --- a/src/MxGateway.Tests/Gateway/GatewayEndToEndFakeWorkerSmokeTests.cs +++ b/src/MxGateway.Tests/Gateway/GatewayEndToEndFakeWorkerSmokeTests.cs @@ -174,6 +174,7 @@ public sealed class GatewayEndToEndFakeWorkerSmokeTests new MxAccessGrpcRequestValidator(), mapper, eventStreamService, + _metrics, NullLogger.Instance); } diff --git a/src/MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceTests.cs b/src/MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceTests.cs index f6d5806..5739084 100644 --- a/src/MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceTests.cs +++ b/src/MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceTests.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.Metrics; using System.Runtime.CompilerServices; using Google.Protobuf.WellKnownTypes; using Grpc.Core; @@ -5,6 +6,7 @@ using Microsoft.Extensions.Logging.Abstractions; using MxGateway.Contracts; using MxGateway.Contracts.Proto; using MxGateway.Server.Grpc; +using MxGateway.Server.Metrics; using MxGateway.Server.Security.Authentication; using MxGateway.Server.Security.Authorization; using MxGateway.Server.Sessions; @@ -163,6 +165,50 @@ public sealed class MxAccessGatewayServiceTests Assert.Equal("session-1", sessionManager.LastReadEventsSessionId); } + [Fact] + public async Task StreamEvents_WhenEventIsWritten_RecordsSendDuration() + { + using GatewayMetrics metrics = new(); + using MeterListener listener = new(); + List families = []; + listener.InstrumentPublished = (instrument, meterListener) => + { + if (instrument.Meter.Name == GatewayMetrics.MeterName + && instrument.Name == "mxgateway.events.stream_send.duration") + { + meterListener.EnableMeasurementEvents(instrument); + } + }; + listener.SetMeasurementEventCallback( + (instrument, measurement, tags, _) => + { + if (instrument.Name != "mxgateway.events.stream_send.duration") + { + return; + } + + foreach (KeyValuePair tag in tags) + { + if (tag.Key == "family" && tag.Value is string family) + { + families.Add(family); + } + } + }); + listener.Start(); + FakeSessionManager sessionManager = new(); + sessionManager.Events.Add(CreateWorkerEvent("session-1", workerSequence: 2)); + MxAccessGatewayService service = CreateService(sessionManager, metrics: metrics); + TestServerStreamWriter writer = new(); + + await service.StreamEvents( + new StreamEventsRequest { SessionId = "session-1" }, + writer, + new TestServerCallContext()); + + Assert.Equal([MxEventFamily.OnDataChange.ToString()], families); + } + [Fact] public async Task CloseSession_WithBlankSessionId_ThrowsInvalidArgument() { @@ -178,7 +224,8 @@ public sealed class MxAccessGatewayServiceTests private static MxAccessGatewayService CreateService( FakeSessionManager sessionManager, - IGatewayRequestIdentityAccessor? identityAccessor = null) + IGatewayRequestIdentityAccessor? identityAccessor = null, + GatewayMetrics? metrics = null) { return new MxAccessGatewayService( sessionManager, @@ -186,6 +233,7 @@ public sealed class MxAccessGatewayServiceTests new MxAccessGrpcRequestValidator(), new MxAccessGrpcMapper(), new FakeEventStreamService(sessionManager), + metrics ?? new GatewayMetrics(), NullLogger.Instance); } diff --git a/src/MxGateway.Tests/Metrics/GatewayMetricsTests.cs b/src/MxGateway.Tests/Metrics/GatewayMetricsTests.cs index cea1f2f..fc7e221 100644 --- a/src/MxGateway.Tests/Metrics/GatewayMetricsTests.cs +++ b/src/MxGateway.Tests/Metrics/GatewayMetricsTests.cs @@ -18,7 +18,7 @@ public sealed class GatewayMetricsTests metrics.EventReceived("session-1", "OnDataChange"); metrics.EventReceived("session-1", "OnDataChange"); metrics.SetWorkerEventQueueDepth(7); - metrics.SetGrpcEventStreamQueueDepth(3); + metrics.AdjustGrpcEventStreamQueueDepth(3); metrics.QueueOverflow("session-events"); metrics.Fault("CommandTimeout"); metrics.WorkerKilled("CommandTimeout"); diff --git a/src/MxGateway.Tests/Security/Authorization/GatewayGrpcScopeResolverTests.cs b/src/MxGateway.Tests/Security/Authorization/GatewayGrpcScopeResolverTests.cs index 6db403c..efadef9 100644 --- a/src/MxGateway.Tests/Security/Authorization/GatewayGrpcScopeResolverTests.cs +++ b/src/MxGateway.Tests/Security/Authorization/GatewayGrpcScopeResolverTests.cs @@ -1,4 +1,5 @@ using MxGateway.Contracts.Proto; +using MxGateway.Contracts.Proto.Galaxy; using MxGateway.Server.Security.Authorization; namespace MxGateway.Tests.Security.Authorization; @@ -9,6 +10,9 @@ public sealed class GatewayGrpcScopeResolverTests [InlineData(typeof(OpenSessionRequest), GatewayScopes.SessionOpen)] [InlineData(typeof(CloseSessionRequest), GatewayScopes.SessionClose)] [InlineData(typeof(StreamEventsRequest), GatewayScopes.EventsRead)] + [InlineData(typeof(TestConnectionRequest), GatewayScopes.MetadataRead)] + [InlineData(typeof(GetLastDeployTimeRequest), GatewayScopes.MetadataRead)] + [InlineData(typeof(DiscoverHierarchyRequest), GatewayScopes.MetadataRead)] public void ResolveRequiredScope_KnownRpcRequest_ReturnsExpectedScope( Type requestType, string expectedScope)