From cf429c1a6d27cd6198be872f1a81d053670d6c31 Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Sun, 5 Oct 2025 22:30:50 +0200
Subject: [PATCH 01/31] wip
---
.gitignore | 4 +-
Directory.Packages.props | 8 ++
Drift.sln | 63 +++++++++
README_dev.md | 4 +
containerlab/blog.txt | 3 +
containerlab/topo1.clab.yaml | 23 ++++
src/Agent.Hosting/Agent.Hosting.csproj | 14 ++
src/Agent.Hosting/AgentHost.cs | 68 ++++++++++
.../Adopt/AdoptRequestHandler.cs | 16 +++
.../Adopt/AdoptRequestPayload.cs | 17 +++
.../Adopt/AdoptResponsePayload.cs | 18 +++
.../Adopt/IAdoptRequestHandler.cs | 7 +
.../Agent.PeerProtocol.csproj | 14 ++
.../PeerProtocolAssemblyMarker.cs | 7 +
.../ServiceCollectionExtensions.cs | 13 ++
.../Subnets/SubnetsRequest.cs | 7 +
.../Subnets/SubnetsRequestHandler.cs | 28 ++++
.../Subnets/SubnetsResponse.cs | 13 ++
src/ArchTests/SanityTests.cs | 4 +-
src/Cli.Tests/Commands/AgentCommandTests.cs | 21 +++
.../ScanCommandTests.RemoteScan.verified.txt | 8 ++
src/Cli.Tests/Commands/ScanCommandTests.cs | 71 ++++++++++
...emCommandLineDefaultErrorTest.verified.txt | 1 +
...eptionReturnsUnknownErrorTest.verified.txt | 6 +-
src/Cli.Tests/ExitCodeTests.cs | 28 ++--
src/Cli.Tests/SpecFilePathResolverTests.cs | 2 +-
src/Cli/Cli.csproj | 11 +-
src/Cli/Commands/Agent/AgentCommand.cs | 24 ++++
.../Subcommands/Start/AgentStartCommand.cs | 74 +++++++++++
.../Subcommands/Start/AgentStartParameters.cs | 53 ++++++++
.../Common/{ => Commands}/CommandBase.cs | 6 +-
.../Common/{ => Commands}/ICommandHandler.cs | 6 +-
.../Common/Commands/SpecCommandBase.cs | 8 ++
src/Cli/Commands/Common/DefaultParameters.cs | 19 ---
.../Common/Parameters/BaseParameters.cs | 14 ++
.../Common/Parameters/SpecParameters.cs | 13 ++
src/Cli/Commands/Common/ParseResultHolder.cs | 2 +-
src/Cli/Commands/Init/InitCommand.cs | 5 +-
src/Cli/Commands/Init/InitParameters.cs | 4 +-
src/Cli/Commands/Lint/LintCommand.cs | 6 +-
src/Cli/Commands/Lint/LintParameters.cs | 4 +-
.../Lint/Presentation/LogLintRenderer.cs | 1 -
src/Cli/Commands/Preview/AgentCommand.cs | 36 ------
src/Cli/Commands/Scan/AgentSubnetProvider.cs | 39 ++++++
src/Cli/Commands/Scan/ClusterExtensions.cs | 19 +++
.../Scan/NonInteractive/NonInteractiveUi.cs | 1 -
.../Scan/Rendering/LogScanRenderer.cs | 1 -
.../SubnetScanResultProcessor.cs | 2 +-
src/Cli/Commands/Scan/ScanCommand.cs | 40 ++++--
src/Cli/Commands/Scan/ScanParameters.cs | 3 +-
src/Cli/DriftCli.cs | 1 -
src/Cli/ExecutionEnvironment.cs | 2 +-
src/Cli/Infrastructure/RootCommandFactory.cs | 26 +++-
.../Logging/NormalOutputLoggerAdapter.cs | 21 ++-
.../Logging/OutputManagerExtensions.cs | 1 -
.../Managers/Abstractions/ILogOutput.cs | 2 -
.../Console/Managers/ConsoleOutputManager.cs | 1 -
.../Console/Managers/NullOutputManager.cs | 1 -
.../Console/Managers/Outputs/LogOutput.cs | 1 -
.../Console/Managers/Outputs/NormalOutput.cs | 18 ++-
.../Console/OutputManagerFactory.cs | 1 -
src/Cli/Properties/launchSettings.json | 14 +-
src/Cli/SpecFile/FileSystemSpecProvider.cs | 1 -
src/Cli/SpecFile/SpecFilePathResolver.cs | 1 -
src/Diff.Tests/DiffTest.cs | 20 +--
src/Domain/AgentId.cs | 37 ++++++
src/Domain/Environment.cs | 65 ++++++++++
src/Domain/Inventory.cs | 5 +
src/Domain/RequestId.cs | 19 +++
.../Converters/CidrBlockConverter.cs | 25 ++++
.../Converters/IpAddressConverter.cs | 21 +++
src/Networking.Clustering/Cluster.cs | 77 +++++++++++
src/Networking.Clustering/Enrollment.cs | 10 ++
src/Networking.Clustering/ICluster.cs | 19 +++
.../Networking.Clustering.csproj | 7 +
.../ServiceCollectionExtensions.cs | 9 ++
.../DefaultPeerClientFactory.cs | 13 ++
.../Networking.PeerStreaming.Client.csproj | 12 ++
.../ServiceCollectionExtensions.cs | 10 ++
.../ConnectionSide.cs | 6 +
.../IPeerClientFactory.cs | 8 ++
.../IPeerMessage.cs | 7 +
.../IPeerMessageEnvelopeConverter.cs | 9 ++
.../IPeerMessageHandler.cs | 28 ++++
.../IPeerStream.cs | 20 +++
.../IPeerStreamManager.cs | 15 +++
...ing.PeerStreaming.Core.Abstractions.csproj | 12 ++
.../Common/GrpcMetadataExtensions.cs | 16 +++
.../Messages/PeerMessageDispatcher.cs | 55 ++++++++
.../Messages/PeerMessageEnvelopeConverter.cs | 48 +++++++
.../Messages/PeerMessageTypesProvider.cs | 23 ++++
.../Messages/PeerResponseCorrelator.cs | 43 +++++++
.../Networking.PeerStreaming.Core.csproj | 18 +++
.../PeerStream.cs | 115 +++++++++++++++++
.../PeerStreamManager.cs | 71 ++++++++++
.../PeerStreamingOptions.cs | 15 +++
.../ServiceCollectionExtensions.cs | 22 ++++
.../Networking.PeerStreaming.Grpc.csproj | 15 +++
.../Protos/peer.proto | 17 +++
.../InboundPeerService.cs | 31 +++++
.../Networking.PeerStreaming.Server.csproj | 15 +++
.../PeerStreamingServerMarker.cs | 8 ++
.../ServiceCollectionExtensions.cs | 47 +++++++
.../AssemblyInfo.cs | 1 +
.../Helpers/InMemoryDuplexStreamPair.cs | 121 ++++++++++++++++++
.../Helpers/TestServerCallContext.cs | 77 +++++++++++
.../TestServerCallContextExtensions.cs | 23 ++++
.../InboundTests.cs | 71 ++++++++++
.../Networking.PeerStreaming.Tests.csproj | 21 +++
.../PeerStreamManagerTests.cs | 64 +++++++++
.../Subnets/CompositeSubnetProvider.cs | 5 +-
src/Scanning/Subnets/ISubnetProvider.cs | 2 +-
.../Interface/InterfaceSubnetProviderBase.cs | 4 +-
.../Subnets/PredefinedSubnetProvider.cs | 12 +-
src/Spec.Tests/ValidationTests.cs | 5 +-
src/Spec/Dtos/V1_preview/DriftSpec.cs | 13 ++
.../V1_preview/Mappers/Mapper.ToDomain.cs | 50 ++++++--
.../Dtos/V1_preview/Mappers/Mapper.ToDto.cs | 28 ++--
src/Spec/Schema/SchemaGenerator.cs | 3 +-
src/Spec/Serialization/YamlStaticContext.cs | 16 ++-
src/Spec/Validation/SpecValidator.cs | 18 ++-
.../schemas/drift-spec-v1-preview.schema.json | 15 +++
src/TestUtilities/StringLogger.cs | 4 +-
123 files changed, 2316 insertions(+), 200 deletions(-)
create mode 100644 containerlab/blog.txt
create mode 100644 containerlab/topo1.clab.yaml
create mode 100644 src/Agent.Hosting/Agent.Hosting.csproj
create mode 100644 src/Agent.Hosting/AgentHost.cs
create mode 100644 src/Agent.PeerProtocol/Adopt/AdoptRequestHandler.cs
create mode 100644 src/Agent.PeerProtocol/Adopt/AdoptRequestPayload.cs
create mode 100644 src/Agent.PeerProtocol/Adopt/AdoptResponsePayload.cs
create mode 100644 src/Agent.PeerProtocol/Adopt/IAdoptRequestHandler.cs
create mode 100644 src/Agent.PeerProtocol/Agent.PeerProtocol.csproj
create mode 100644 src/Agent.PeerProtocol/PeerProtocolAssemblyMarker.cs
create mode 100644 src/Agent.PeerProtocol/ServiceCollectionExtensions.cs
create mode 100644 src/Agent.PeerProtocol/Subnets/SubnetsRequest.cs
create mode 100644 src/Agent.PeerProtocol/Subnets/SubnetsRequestHandler.cs
create mode 100644 src/Agent.PeerProtocol/Subnets/SubnetsResponse.cs
create mode 100644 src/Cli.Tests/Commands/AgentCommandTests.cs
create mode 100644 src/Cli.Tests/Commands/ScanCommandTests.RemoteScan.verified.txt
create mode 100644 src/Cli/Commands/Agent/AgentCommand.cs
create mode 100644 src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs
create mode 100644 src/Cli/Commands/Agent/Subcommands/Start/AgentStartParameters.cs
rename src/Cli/Commands/Common/{ => Commands}/CommandBase.cs (88%)
rename src/Cli/Commands/Common/{ => Commands}/ICommandHandler.cs (56%)
create mode 100644 src/Cli/Commands/Common/Commands/SpecCommandBase.cs
delete mode 100644 src/Cli/Commands/Common/DefaultParameters.cs
create mode 100644 src/Cli/Commands/Common/Parameters/BaseParameters.cs
create mode 100644 src/Cli/Commands/Common/Parameters/SpecParameters.cs
delete mode 100644 src/Cli/Commands/Preview/AgentCommand.cs
create mode 100644 src/Cli/Commands/Scan/AgentSubnetProvider.cs
create mode 100644 src/Cli/Commands/Scan/ClusterExtensions.cs
create mode 100644 src/Domain/AgentId.cs
create mode 100644 src/Domain/Environment.cs
create mode 100644 src/Domain/RequestId.cs
create mode 100644 src/EnvironmentConfig/Converters/CidrBlockConverter.cs
create mode 100644 src/EnvironmentConfig/Converters/IpAddressConverter.cs
create mode 100644 src/Networking.Clustering/Cluster.cs
create mode 100644 src/Networking.Clustering/Enrollment.cs
create mode 100644 src/Networking.Clustering/ICluster.cs
create mode 100644 src/Networking.Clustering/Networking.Clustering.csproj
create mode 100644 src/Networking.Clustering/ServiceCollectionExtensions.cs
create mode 100644 src/Networking.PeerStreaming.Client/DefaultPeerClientFactory.cs
create mode 100644 src/Networking.PeerStreaming.Client/Networking.PeerStreaming.Client.csproj
create mode 100644 src/Networking.PeerStreaming.Client/ServiceCollectionExtensions.cs
create mode 100644 src/Networking.PeerStreaming.Core.Abstractions/ConnectionSide.cs
create mode 100644 src/Networking.PeerStreaming.Core.Abstractions/IPeerClientFactory.cs
create mode 100644 src/Networking.PeerStreaming.Core.Abstractions/IPeerMessage.cs
create mode 100644 src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageEnvelopeConverter.cs
create mode 100644 src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageHandler.cs
create mode 100644 src/Networking.PeerStreaming.Core.Abstractions/IPeerStream.cs
create mode 100644 src/Networking.PeerStreaming.Core.Abstractions/IPeerStreamManager.cs
create mode 100644 src/Networking.PeerStreaming.Core.Abstractions/Networking.PeerStreaming.Core.Abstractions.csproj
create mode 100644 src/Networking.PeerStreaming.Core/Common/GrpcMetadataExtensions.cs
create mode 100644 src/Networking.PeerStreaming.Core/Messages/PeerMessageDispatcher.cs
create mode 100644 src/Networking.PeerStreaming.Core/Messages/PeerMessageEnvelopeConverter.cs
create mode 100644 src/Networking.PeerStreaming.Core/Messages/PeerMessageTypesProvider.cs
create mode 100644 src/Networking.PeerStreaming.Core/Messages/PeerResponseCorrelator.cs
create mode 100644 src/Networking.PeerStreaming.Core/Networking.PeerStreaming.Core.csproj
create mode 100644 src/Networking.PeerStreaming.Core/PeerStream.cs
create mode 100644 src/Networking.PeerStreaming.Core/PeerStreamManager.cs
create mode 100644 src/Networking.PeerStreaming.Core/PeerStreamingOptions.cs
create mode 100644 src/Networking.PeerStreaming.Core/ServiceCollectionExtensions.cs
create mode 100644 src/Networking.PeerStreaming.Grpc/Networking.PeerStreaming.Grpc.csproj
create mode 100644 src/Networking.PeerStreaming.Grpc/Protos/peer.proto
create mode 100644 src/Networking.PeerStreaming.Server/InboundPeerService.cs
create mode 100644 src/Networking.PeerStreaming.Server/Networking.PeerStreaming.Server.csproj
create mode 100644 src/Networking.PeerStreaming.Server/PeerStreamingServerMarker.cs
create mode 100644 src/Networking.PeerStreaming.Server/ServiceCollectionExtensions.cs
create mode 100644 src/Networking.PeerStreaming.Tests/AssemblyInfo.cs
create mode 100644 src/Networking.PeerStreaming.Tests/Helpers/InMemoryDuplexStreamPair.cs
create mode 100644 src/Networking.PeerStreaming.Tests/Helpers/TestServerCallContext.cs
create mode 100644 src/Networking.PeerStreaming.Tests/Helpers/TestServerCallContextExtensions.cs
create mode 100644 src/Networking.PeerStreaming.Tests/InboundTests.cs
create mode 100644 src/Networking.PeerStreaming.Tests/Networking.PeerStreaming.Tests.csproj
create mode 100644 src/Networking.PeerStreaming.Tests/PeerStreamManagerTests.cs
diff --git a/.gitignore b/.gitignore
index d3ed220a..80a95396 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@ obj/
#*.idea
.idea/.idea.Drift/.idea/watcherTasks.xml
.idea/.idea.Drift/.idea/encodings.xml
+.idea/.idea.Drift.Build/.idea/encodings.xml
*.DotSettings
*.received.*
artifacts/
@@ -14,4 +15,5 @@ TestResults/
build.binlog
build.binlog-warnings-only.log
publish.binlog
-publish.binlog-warnings-only.log
\ No newline at end of file
+publish.binlog-warnings-only.log
+containerlab/*/
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 7bb4a047..7c57ddb1 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -5,6 +5,12 @@
true
+
+
+
+
+
+
@@ -13,6 +19,7 @@
+
@@ -35,6 +42,7 @@
+
diff --git a/Drift.sln b/Drift.sln
index d29c2422..859849e0 100644
--- a/Drift.sln
+++ b/Drift.sln
@@ -73,6 +73,26 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cli.Settings.SchemaGenerato
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Schemas", "src\Common.Schemas\Common.Schemas.csproj", "{BAC0F9AF-CAE2-43FB-AF47-B9AC7B62544B}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Networking.PeerStreaming.Grpc", "src\Networking.PeerStreaming.Grpc\Networking.PeerStreaming.Grpc.csproj", "{8ED3FF22-90D2-4F08-A079-55FE7127D1C7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Networking.Clustering", "src\Networking.Clustering\Networking.Clustering.csproj", "{091D3DCE-F062-4D40-A8F6-5B6F123ED713}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Networking.PeerStreaming.Core", "src\Networking.PeerStreaming.Core\Networking.PeerStreaming.Core.csproj", "{80445644-7342-4C6D-88E5-BF27126FE9A2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agent.Hosting", "src\Agent.Hosting\Agent.Hosting.csproj", "{655124DB-312F-4135-B104-20518CAFDA82}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Networking.PeerStreaming.Server", "src\Networking.PeerStreaming.Server\Networking.PeerStreaming.Server.csproj", "{A26B4527-6EBF-4A20-8E75-945CCD59016B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Networking.PeerStreaming.Client", "src\Networking.PeerStreaming.Client\Networking.PeerStreaming.Client.csproj", "{E69772D3-8A07-414F-8F9A-30370D81A972}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Networking", "Networking", "{75F8AA01-64B6-4EF6-A1B2-CC6E8745A2CC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Networking.PeerStreaming.Core.Abstractions", "src\Networking.PeerStreaming.Core.Abstractions\Networking.PeerStreaming.Core.Abstractions.csproj", "{ED4522C5-C32B-4FDB-B1BA-82D40D1EC403}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Networking.PeerStreaming.Tests", "src\Networking.PeerStreaming.Tests\Networking.PeerStreaming.Tests.csproj", "{9DFDD692-22F8-4F9A-8808-94E318863D23}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agent.PeerProtocol", "src\Agent.PeerProtocol\Agent.PeerProtocol.csproj", "{7C72C2AE-2888-47A0-AAA4-61CC66B9F941}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -177,11 +197,54 @@ Global
{BAC0F9AF-CAE2-43FB-AF47-B9AC7B62544B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BAC0F9AF-CAE2-43FB-AF47-B9AC7B62544B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BAC0F9AF-CAE2-43FB-AF47-B9AC7B62544B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8ED3FF22-90D2-4F08-A079-55FE7127D1C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8ED3FF22-90D2-4F08-A079-55FE7127D1C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8ED3FF22-90D2-4F08-A079-55FE7127D1C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8ED3FF22-90D2-4F08-A079-55FE7127D1C7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {091D3DCE-F062-4D40-A8F6-5B6F123ED713}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {091D3DCE-F062-4D40-A8F6-5B6F123ED713}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {091D3DCE-F062-4D40-A8F6-5B6F123ED713}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {091D3DCE-F062-4D40-A8F6-5B6F123ED713}.Release|Any CPU.Build.0 = Release|Any CPU
+ {80445644-7342-4C6D-88E5-BF27126FE9A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {80445644-7342-4C6D-88E5-BF27126FE9A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {80445644-7342-4C6D-88E5-BF27126FE9A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {80445644-7342-4C6D-88E5-BF27126FE9A2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {655124DB-312F-4135-B104-20518CAFDA82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {655124DB-312F-4135-B104-20518CAFDA82}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {655124DB-312F-4135-B104-20518CAFDA82}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {655124DB-312F-4135-B104-20518CAFDA82}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A26B4527-6EBF-4A20-8E75-945CCD59016B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A26B4527-6EBF-4A20-8E75-945CCD59016B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A26B4527-6EBF-4A20-8E75-945CCD59016B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A26B4527-6EBF-4A20-8E75-945CCD59016B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E69772D3-8A07-414F-8F9A-30370D81A972}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E69772D3-8A07-414F-8F9A-30370D81A972}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E69772D3-8A07-414F-8F9A-30370D81A972}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E69772D3-8A07-414F-8F9A-30370D81A972}.Release|Any CPU.Build.0 = Release|Any CPU
+ {ED4522C5-C32B-4FDB-B1BA-82D40D1EC403}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {ED4522C5-C32B-4FDB-B1BA-82D40D1EC403}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {ED4522C5-C32B-4FDB-B1BA-82D40D1EC403}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {ED4522C5-C32B-4FDB-B1BA-82D40D1EC403}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9DFDD692-22F8-4F9A-8808-94E318863D23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9DFDD692-22F8-4F9A-8808-94E318863D23}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9DFDD692-22F8-4F9A-8808-94E318863D23}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9DFDD692-22F8-4F9A-8808-94E318863D23}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7C72C2AE-2888-47A0-AAA4-61CC66B9F941}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7C72C2AE-2888-47A0-AAA4-61CC66B9F941}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7C72C2AE-2888-47A0-AAA4-61CC66B9F941}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7C72C2AE-2888-47A0-AAA4-61CC66B9F941}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{8523E9E0-F412-41B7-B361-ADE639FFAF24} = {C0698EF0-61C8-403E-8E93-1F1D34C5B910}
{FEA2FBBE-785F-4187-8242-FD348F9E78AF} = {C0698EF0-61C8-403E-8E93-1F1D34C5B910}
{272166CF-E425-45F8-984F-FAFD3CE953C9} = {C0698EF0-61C8-403E-8E93-1F1D34C5B910}
{DD70FBC7-8367-45B3-8D3D-757F1CDF6531} = {C0698EF0-61C8-403E-8E93-1F1D34C5B910}
+ {091D3DCE-F062-4D40-A8F6-5B6F123ED713} = {75F8AA01-64B6-4EF6-A1B2-CC6E8745A2CC}
+ {8ED3FF22-90D2-4F08-A079-55FE7127D1C7} = {75F8AA01-64B6-4EF6-A1B2-CC6E8745A2CC}
+ {80445644-7342-4C6D-88E5-BF27126FE9A2} = {75F8AA01-64B6-4EF6-A1B2-CC6E8745A2CC}
+ {A26B4527-6EBF-4A20-8E75-945CCD59016B} = {75F8AA01-64B6-4EF6-A1B2-CC6E8745A2CC}
+ {E69772D3-8A07-414F-8F9A-30370D81A972} = {75F8AA01-64B6-4EF6-A1B2-CC6E8745A2CC}
+ {ED4522C5-C32B-4FDB-B1BA-82D40D1EC403} = {75F8AA01-64B6-4EF6-A1B2-CC6E8745A2CC}
+ {9DFDD692-22F8-4F9A-8808-94E318863D23} = {75F8AA01-64B6-4EF6-A1B2-CC6E8745A2CC}
EndGlobalSection
EndGlobal
diff --git a/README_dev.md b/README_dev.md
index 7a89c5ed..f58d70a3 100644
--- a/README_dev.md
+++ b/README_dev.md
@@ -35,6 +35,10 @@
One or more addresses (MAC, IPv4, IPv6, and/or hostname) that together serve as a unique identifier for a network
device.
+- **Agent**
+ A running instance of Drift in agent mode that reports network state to other Drift peers.
+ Agents help ensure full network visibility by uncovering state that's only observable when scanning from specific subnets.
+
## Concepts
### Device ID
diff --git a/containerlab/blog.txt b/containerlab/blog.txt
new file mode 100644
index 00000000..6f33cbe4
--- /dev/null
+++ b/containerlab/blog.txt
@@ -0,0 +1,3 @@
+install
+
+create topology
\ No newline at end of file
diff --git a/containerlab/topo1.clab.yaml b/containerlab/topo1.clab.yaml
new file mode 100644
index 00000000..a5140b1b
--- /dev/null
+++ b/containerlab/topo1.clab.yaml
@@ -0,0 +1,23 @@
+name: drift-topo1
+topology:
+ nodes:
+ switch|bp1:
+ kind: bridge
+ network-mode: container:bp1
+ bp1:
+ kind: linux
+ image: alpine:latest
+ pc1:
+ kind: linux
+ image: hojmark/drift
+ cmd: scan -i
+ pc2:
+ kind: linux
+ image: agent start --adoptable
+ pc3:
+ kind: linux
+ image: agent start --adoptable
+ links:
+ - endpoints: [ "pc1:eth1", "switch|bp1:eth1" ]
+ - endpoints: [ "pc2:eth1", "switch|bp1:eth2" ]
+ - endpoints: [ "pc3:eth1", "switch|bp1:eth3" ]
\ No newline at end of file
diff --git a/src/Agent.Hosting/Agent.Hosting.csproj b/src/Agent.Hosting/Agent.Hosting.csproj
new file mode 100644
index 00000000..5d558217
--- /dev/null
+++ b/src/Agent.Hosting/Agent.Hosting.csproj
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Agent.Hosting/AgentHost.cs b/src/Agent.Hosting/AgentHost.cs
new file mode 100644
index 00000000..7a9eced8
--- /dev/null
+++ b/src/Agent.Hosting/AgentHost.cs
@@ -0,0 +1,68 @@
+using Drift.Agent.PeerProtocol.Subnets;
+using Drift.Networking.PeerStreaming.Client;
+using Drift.Networking.PeerStreaming.Core;
+using Drift.Networking.PeerStreaming.Server;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Server.Kestrel.Core;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace Drift.Agent.Hosting;
+
+public static class AgentHost {
+ public static Task Run(
+ ushort port,
+ ILogger logger,
+ Action? configureServices,
+ CancellationToken cancellationToken
+ ) {
+ var app = Build( port, logger, configureServices );
+ return app.RunAsync( cancellationToken );
+ }
+
+ private static WebApplication Build(
+ ushort port,
+ ILogger logger,
+ Action? configureServices = null
+ ) {
+ var builder = WebApplication.CreateSlimBuilder();
+
+ builder.Logging.ClearProviders();
+ builder.Services.AddSingleton( logger );
+ builder.Services.AddPeerStreamingServer( options => {
+ options.EnableDetailedErrors = true;
+ } );
+ builder.Services.AddPeerStreamingClient();
+ var peerStreamingOptions = new PeerStreamingOptions { MessageAssembly = typeof(SubnetsRequest).Assembly };
+ builder.Services.AddPeerStreamingCore( peerStreamingOptions );
+ configureServices?.Invoke( builder.Services );
+
+ builder.WebHost.ConfigureKestrel( options => {
+ options.ListenLocalhost( port, o => {
+ o.Protocols = HttpProtocols.Http2; // Allow HTTP/2 over plain HTTP i.e., non-HTTPS
+ } );
+ } );
+
+ var app = builder.Build();
+
+ peerStreamingOptions.StoppingToken = app.Lifetime.ApplicationStopping;
+
+ app.MapPeerStreamingServerEndpoints();
+ app.MapGet( "/", () => "Nothing to see here" );
+
+ app.Lifetime.ApplicationStarted.Register( () => {
+ logger.LogInformation( "Listening for incoming connections on port {Port}", port );
+ logger.LogInformation( "Agent started" );
+ } );
+ app.Lifetime.ApplicationStopping.Register( () => {
+ logger.LogInformation( "Agent stopping..." );
+ } );
+ app.Lifetime.ApplicationStopped.Register( () => {
+ logger.LogInformation( "Agent stopped" );
+ } );
+
+ return app;
+ }
+}
\ No newline at end of file
diff --git a/src/Agent.PeerProtocol/Adopt/AdoptRequestHandler.cs b/src/Agent.PeerProtocol/Adopt/AdoptRequestHandler.cs
new file mode 100644
index 00000000..c586a04a
--- /dev/null
+++ b/src/Agent.PeerProtocol/Adopt/AdoptRequestHandler.cs
@@ -0,0 +1,16 @@
+using Drift.Agent.PeerProtocol.Subnets;
+using Drift.Networking.PeerStreaming.Core.Abstractions;
+using Microsoft.Extensions.Logging;
+
+namespace Drift.Agent.PeerProtocol.Adopt;
+
+internal sealed class AdoptRequestHandler : IPeerMessageHandler {
+ private readonly ILogger _logger; // Example: inject what you need
+
+ public string MessageType => "adopt-request";
+
+ public async Task HandleAsync( AdoptRequestPayload message, CancellationToken cancellationToken = default ) {
+ _logger.LogInformation( $"[AdoptRequest] Controller: {message.ControllerId}" );
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/Agent.PeerProtocol/Adopt/AdoptRequestPayload.cs b/src/Agent.PeerProtocol/Adopt/AdoptRequestPayload.cs
new file mode 100644
index 00000000..17c39167
--- /dev/null
+++ b/src/Agent.PeerProtocol/Adopt/AdoptRequestPayload.cs
@@ -0,0 +1,17 @@
+using Drift.Networking.PeerStreaming.Core.Abstractions;
+
+namespace Drift.Agent.PeerProtocol.Adopt;
+
+internal sealed class AdoptRequestPayload : IPeerMessage {
+ public string MessageType => "adopt-request";
+
+ public string Jwt {
+ get;
+ set;
+ }
+
+ public string ControllerId {
+ get;
+ set;
+ }
+}
\ No newline at end of file
diff --git a/src/Agent.PeerProtocol/Adopt/AdoptResponsePayload.cs b/src/Agent.PeerProtocol/Adopt/AdoptResponsePayload.cs
new file mode 100644
index 00000000..6441cfcf
--- /dev/null
+++ b/src/Agent.PeerProtocol/Adopt/AdoptResponsePayload.cs
@@ -0,0 +1,18 @@
+namespace Drift.Agent.PeerProtocol.Adopt;
+
+internal sealed class AdoptResponsePayload {
+ public string Status {
+ get;
+ set;
+ } // "accepted" or "rejected"
+
+ public string AgentId {
+ get;
+ set;
+ } // Only with "accepted"
+
+ public string Reason {
+ get;
+ set;
+ } // Only with "rejected"
+}
\ No newline at end of file
diff --git a/src/Agent.PeerProtocol/Adopt/IAdoptRequestHandler.cs b/src/Agent.PeerProtocol/Adopt/IAdoptRequestHandler.cs
new file mode 100644
index 00000000..0a83dfa3
--- /dev/null
+++ b/src/Agent.PeerProtocol/Adopt/IAdoptRequestHandler.cs
@@ -0,0 +1,7 @@
+namespace Drift.Agent.PeerProtocol.Adopt;
+
+internal interface IAdoptRequestHandler {
+ public string MessageType => "adopt-request";
+
+ Task HandleAsync( AdoptRequestPayload message, CancellationToken cancellationToken = default );
+}
\ No newline at end of file
diff --git a/src/Agent.PeerProtocol/Agent.PeerProtocol.csproj b/src/Agent.PeerProtocol/Agent.PeerProtocol.csproj
new file mode 100644
index 00000000..13dacae1
--- /dev/null
+++ b/src/Agent.PeerProtocol/Agent.PeerProtocol.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/src/Agent.PeerProtocol/PeerProtocolAssemblyMarker.cs b/src/Agent.PeerProtocol/PeerProtocolAssemblyMarker.cs
new file mode 100644
index 00000000..1907e215
--- /dev/null
+++ b/src/Agent.PeerProtocol/PeerProtocolAssemblyMarker.cs
@@ -0,0 +1,7 @@
+namespace Drift.Agent.PeerProtocol;
+
+// Justification: marker class
+#pragma warning disable S2094
+public class PeerProtocolAssemblyMarker {
+}
+#pragma warning restore S2094
\ No newline at end of file
diff --git a/src/Agent.PeerProtocol/ServiceCollectionExtensions.cs b/src/Agent.PeerProtocol/ServiceCollectionExtensions.cs
new file mode 100644
index 00000000..06b4412f
--- /dev/null
+++ b/src/Agent.PeerProtocol/ServiceCollectionExtensions.cs
@@ -0,0 +1,13 @@
+using Drift.Agent.PeerProtocol.Subnets;
+using Drift.Networking.PeerStreaming.Core.Abstractions;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Drift.Agent.PeerProtocol;
+
+public static class ServiceCollectionExtensions {
+ public static void AddPeerProtocol( this IServiceCollection services ) {
+ //TODO need both?
+ services.AddScoped, SubnetsRequestHandler>();
+ services.AddScoped();
+ }
+}
\ No newline at end of file
diff --git a/src/Agent.PeerProtocol/Subnets/SubnetsRequest.cs b/src/Agent.PeerProtocol/Subnets/SubnetsRequest.cs
new file mode 100644
index 00000000..2cb6044d
--- /dev/null
+++ b/src/Agent.PeerProtocol/Subnets/SubnetsRequest.cs
@@ -0,0 +1,7 @@
+using Drift.Networking.PeerStreaming.Core.Abstractions;
+
+namespace Drift.Agent.PeerProtocol.Subnets;
+
+public sealed class SubnetsRequest : IPeerMessage {
+ public string MessageType => "subnetsrequest";
+}
\ No newline at end of file
diff --git a/src/Agent.PeerProtocol/Subnets/SubnetsRequestHandler.cs b/src/Agent.PeerProtocol/Subnets/SubnetsRequestHandler.cs
new file mode 100644
index 00000000..48654152
--- /dev/null
+++ b/src/Agent.PeerProtocol/Subnets/SubnetsRequestHandler.cs
@@ -0,0 +1,28 @@
+using System.Collections;
+using Drift.Networking.Grpc.Generated;
+using Drift.Networking.PeerStreaming.Core.Abstractions;
+using Drift.Scanning.Subnets.Interface;
+using Microsoft.Extensions.Logging;
+
+namespace Drift.Agent.PeerProtocol.Subnets;
+
+internal sealed class SubnetsRequestHandler(
+ IInterfaceSubnetProvider interfaceSubnetProvider,
+ ILogger logger
+) : IPeerMessageHandler {
+ public string MessageType => "subnetsrequest";
+
+ public async Task HandleAsync(
+ SubnetsRequest message,
+ CancellationToken cancellationToken = default
+ ) {
+ logger.LogInformation( "Handling subnet request" );
+
+ var subnets = await interfaceSubnetProvider.GetAsync();
+ var response = new SubnetsResponse { Subnets = subnets };
+
+ logger.LogInformation( "Sending subnets: {Subnets}", string.Join( ", ", subnets ) );
+
+ return response;
+ }
+}
\ No newline at end of file
diff --git a/src/Agent.PeerProtocol/Subnets/SubnetsResponse.cs b/src/Agent.PeerProtocol/Subnets/SubnetsResponse.cs
new file mode 100644
index 00000000..25b7c549
--- /dev/null
+++ b/src/Agent.PeerProtocol/Subnets/SubnetsResponse.cs
@@ -0,0 +1,13 @@
+using Drift.Domain;
+using Drift.Networking.PeerStreaming.Core.Abstractions;
+
+namespace Drift.Agent.PeerProtocol.Subnets;
+
+public sealed class SubnetsResponse : IPeerMessage {
+ public string MessageType => "subnetsresponse";
+
+ public required IReadOnlyList Subnets {
+ get;
+ init;
+ }
+}
\ No newline at end of file
diff --git a/src/ArchTests/SanityTests.cs b/src/ArchTests/SanityTests.cs
index 3bac161e..8639eace 100644
--- a/src/ArchTests/SanityTests.cs
+++ b/src/ArchTests/SanityTests.cs
@@ -3,8 +3,8 @@
namespace Drift.ArchTests;
internal sealed class SanityTests : DriftArchitectureFixture {
- private const uint ExpectedAssemblyCount = 20;
- private const uint ExpectedAssemblyCountTolerance = 3;
+ private const uint ExpectedAssemblyCount = 25;
+ private const uint ExpectedAssemblyCountTolerance = 5;
[Test]
public void FindManyAssemblies() {
diff --git a/src/Cli.Tests/Commands/AgentCommandTests.cs b/src/Cli.Tests/Commands/AgentCommandTests.cs
new file mode 100644
index 00000000..c96eef4c
--- /dev/null
+++ b/src/Cli.Tests/Commands/AgentCommandTests.cs
@@ -0,0 +1,21 @@
+using Drift.Cli.Abstractions;
+using Drift.Cli.Tests.Utils;
+
+namespace Drift.Cli.Tests.Commands;
+
+internal class AgentCommandTests {
+ [CancelAfter( 10000 )]
+ [Test]
+ public async Task RespectsCancellationToken() {
+ using var tcs = new CancellationTokenSource( TimeSpan.FromSeconds( 5 ) );
+
+ var (exitCode, output, _) = await DriftTestCli.InvokeFromTestAsync(
+ "agent start --adoptable",
+ cancellationToken: tcs.Token
+ );
+
+ Console.WriteLine( output );
+
+ Assert.That( exitCode, Is.EqualTo( ExitCodes.Success ) );
+ }
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Commands/ScanCommandTests.RemoteScan.verified.txt b/src/Cli.Tests/Commands/ScanCommandTests.RemoteScan.verified.txt
new file mode 100644
index 00000000..68cf64c2
--- /dev/null
+++ b/src/Cli.Tests/Commands/ScanCommandTests.RemoteScan.verified.txt
@@ -0,0 +1,8 @@
+Requesting subnets from agent agentid_local1 (http://localhost:51515)
+Received subnet(s) from agent agentid_local1 (http://localhost:51515): 192.168.0.0/24
+
+Scanning 1 subnet
+ 192.168.0.0/24 (254 addresses, estimated scan time is 00:00:05.0800000) via agentid_local1, local
+
+192.168.0.0/24 (1 devices)
+└── 192.168.0.100 11-11-11-11-11-11 Online (unknown device)
\ No newline at end of file
diff --git a/src/Cli.Tests/Commands/ScanCommandTests.cs b/src/Cli.Tests/Commands/ScanCommandTests.cs
index 5e77b568..e3edbd71 100644
--- a/src/Cli.Tests/Commands/ScanCommandTests.cs
+++ b/src/Cli.Tests/Commands/ScanCommandTests.cs
@@ -213,6 +213,77 @@ await Verify( output.ToString() + error )
}
}
+ [Test]
+ public async Task RemoteScan() {
+ // Arrange
+ var serviceConfigScan = ConfigureServices(
+ [
+ new NetworkInterface {
+ Description = "eth1",
+ OperationalStatus = OperationalStatus.Up,
+ UnicastAddress = new CidrBlock( "192.168.0.0/24" )
+ }
+ ],
+ [
+ new DiscoveredDevice { Addresses = [new IpV4Address( "192.168.0.100" ), new MacAddress( "11:11:11:11:11:11" )] }
+ ],
+ new Inventory { Network = new Network(), Agents = [new Domain.Agent { Address = "http://localhost:51515" }] }
+ );
+
+ var serviceConfigAgent = ConfigureServices(
+ [
+ new NetworkInterface {
+ Description = "eth1",
+ OperationalStatus = OperationalStatus.Up,
+ UnicastAddress = new CidrBlock( "192.168.100.0/24" )
+ }
+ ],
+ [
+ new DiscoveredDevice {
+ Addresses = [new IpV4Address( "192.168.100.200" ), new MacAddress( "22:22:22:22:22:22" )]
+ }
+ ]
+ );
+
+ var cts = new CancellationTokenSource( TimeSpan.FromSeconds( 800 ) );
+
+ // Act
+ Console.WriteLine( "Invoking agent start" );
+ var agentTask = DriftTestCli.InvokeFromTestAsync(
+ "agent start --adoptable -v",
+ serviceConfigAgent,
+ cancellationToken: cts.Token
+ );
+ await Task.Delay( 3000, cts.Token );
+ Console.WriteLine( "Invoking scan" );
+ var (scanExitCode, scanOutput, scanError) = await DriftTestCli.InvokeFromTestAsync(
+ "scan unittest",
+ serviceConfigScan,
+ cancellationToken: cts.Token
+ );
+ Console.WriteLine( "Scan finished" );
+ Console.WriteLine( "----------------" );
+ Console.WriteLine( scanOutput.ToString() + scanError );
+ Console.WriteLine( "----------------" );
+
+ Console.WriteLine( "Cancelling token" );
+ await cts.CancelAsync();
+ cts.Dispose();
+ Console.WriteLine( "Waiting for agent to shut down" );
+
+ var (agentExitCode, agentOutput, agentError) = await agentTask;
+
+ Console.WriteLine( "Agent finished" );
+ Console.WriteLine( "----------------" );
+ Console.WriteLine( agentOutput.ToString() + agentError );
+ Console.WriteLine( "----------------" );
+
+ // Assert
+ Assert.That( agentExitCode, Is.EqualTo( ExitCodes.Success ) );
+ Assert.That( scanExitCode, Is.EqualTo( ExitCodes.Success ) );
+ await Verify( scanOutput.ToString() + scanError );
+ }
+
[Test]
public async Task NonExistingSpecOption() {
// Arrange / Act
diff --git a/src/Cli.Tests/ExitCodeTests.NonExistingCommand_ReturnsSystemCommandLineDefaultErrorTest.verified.txt b/src/Cli.Tests/ExitCodeTests.NonExistingCommand_ReturnsSystemCommandLineDefaultErrorTest.verified.txt
index 1756febb..bf9563ca 100644
--- a/src/Cli.Tests/ExitCodeTests.NonExistingCommand_ReturnsSystemCommandLineDefaultErrorTest.verified.txt
+++ b/src/Cli.Tests/ExitCodeTests.NonExistingCommand_ReturnsSystemCommandLineDefaultErrorTest.verified.txt
@@ -18,6 +18,7 @@ Commands:
init Create a network spec
scan Scan the network and detect drift
lint Validate a network spec
+ agent Manage the local Drift agent
Required command was not provided.
Unrecognized command or argument 'nonexisting'.
diff --git a/src/Cli.Tests/ExitCodeTests.UnhandledExceptionReturnsUnknownErrorTest.verified.txt b/src/Cli.Tests/ExitCodeTests.UnhandledExceptionReturnsUnknownErrorTest.verified.txt
index f1d8cd7a..c5c74bf4 100644
--- a/src/Cli.Tests/ExitCodeTests.UnhandledExceptionReturnsUnknownErrorTest.verified.txt
+++ b/src/Cli.Tests/ExitCodeTests.UnhandledExceptionReturnsUnknownErrorTest.verified.txt
@@ -1,6 +1,6 @@
✗ This exception was thrown from ExceptionCommandHandler
- at Drift.Cli.Tests.ExitCodeTests.ExceptionCommandHandler.Invoke(DefaultParameters parameters, CancellationToken cancellationToken) in {ProjectDirectory}ExitCodeTests.cs:line 104
- at Drift.Cli.Commands.Common.CommandBase`2.<>c__DisplayClass0_0.<.ctor>b__0(ParseResult parseResult, CancellationToken cancellationToken) in {SolutionDirectory}src/Cli/Commands/Common/CommandBase.cs:line 25
+ at Drift.Cli.Tests.ExitCodeTests.ExceptionCommandHandler.Invoke(DummyParameters parameters, CancellationToken cancellationToken) in {ProjectDirectory}ExitCodeTests.cs:line 105
+ at Drift.Cli.Commands.Common.Commands.CommandBase`2.<>c__DisplayClass0_0.<.ctor>b__0(ParseResult parseResult, CancellationToken cancellationToken) in {SolutionDirectory}src/Cli/Commands/Common/Commands/CommandBase.cs:line 25
at System.CommandLine.Invocation.AnonymousAsynchronousCommandLineAction.InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken)
at System.CommandLine.Invocation.InvocationPipeline.InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken)
- at Drift.Cli.DriftCli.InvokeAsync(String[] args, Boolean toConsole, Boolean plainConsole, Action`1 configureServices, CommandRegistration[] customCommands, Action`1 configureInvocation, CancellationToken cancellationToken) in {SolutionDirectory}src/Cli/DriftCli.cs:line 41
+ at Drift.Cli.DriftCli.InvokeAsync(String[] args, Boolean toConsole, Boolean plainConsole, Action`1 configureServices, CommandRegistration[] customCommands, Action`1 configureInvocation, CancellationToken cancellationToken) in {SolutionDirectory}src/Cli/DriftCli.cs:line 40
diff --git a/src/Cli.Tests/ExitCodeTests.cs b/src/Cli.Tests/ExitCodeTests.cs
index 3d588492..28c4d2e1 100644
--- a/src/Cli.Tests/ExitCodeTests.cs
+++ b/src/Cli.Tests/ExitCodeTests.cs
@@ -1,7 +1,8 @@
using System.CommandLine;
using System.Text.RegularExpressions;
using Drift.Cli.Abstractions;
-using Drift.Cli.Commands.Common;
+using Drift.Cli.Commands.Common.Commands;
+using Drift.Cli.Commands.Common.Parameters;
using Drift.Cli.Infrastructure;
using Drift.Cli.Presentation.Console.Managers.Abstractions;
using Drift.Cli.Tests.Utils;
@@ -71,37 +72,42 @@ public async Task UnhandledExceptionReturnsUnknownErrorTest() {
}
private sealed class ExitCodeTestCommand( IServiceProvider provider )
- : CommandBase(
+ : CommandBase(
ExitCodeCommand,
"Command that returns a specific exit code",
provider
) {
- protected override DefaultParameters CreateParameters( ParseResult result ) {
- return new DefaultParameters( result );
+ protected override DummyParameters CreateParameters( ParseResult result ) {
+ return new DummyParameters( result );
}
}
- private sealed class ExitCodeCommandHandler( IOutputManager output ) : ICommandHandler {
- public Task Invoke( DefaultParameters parameters, CancellationToken cancellationToken ) {
+ private sealed class ExitCodeCommandHandler( IOutputManager output ) : ICommandHandler {
+ public Task Invoke( DummyParameters parameters, CancellationToken cancellationToken ) {
output.Normal.Write( $"Output from command '{ExitCodeCommand}'" );
return Task.FromResult( ExitCodeCommandExitCode );
}
}
private sealed class ExceptionTestCommand( IServiceProvider provider )
- : CommandBase(
+ : CommandBase(
ExceptionThrowingCommand,
"Command that throws an exception",
provider
) {
- protected override DefaultParameters CreateParameters( ParseResult result ) {
- return new DefaultParameters( result );
+ protected override DummyParameters CreateParameters( ParseResult result ) {
+ return new DummyParameters( result );
}
}
- private sealed class ExceptionCommandHandler : ICommandHandler {
- public Task Invoke( DefaultParameters parameters, CancellationToken cancellationToken ) {
+ private sealed class ExceptionCommandHandler : ICommandHandler {
+ public Task Invoke( DummyParameters parameters, CancellationToken cancellationToken ) {
throw new Exception( $"This exception was thrown from {nameof(ExceptionCommandHandler)}" );
}
}
+
+ private sealed record DummyParameters : BaseParameters {
+ public DummyParameters( ParseResult parseResult ) : base( parseResult ) {
+ }
+ }
}
\ No newline at end of file
diff --git a/src/Cli.Tests/SpecFilePathResolverTests.cs b/src/Cli.Tests/SpecFilePathResolverTests.cs
index 6f3aebc6..0d23bb41 100644
--- a/src/Cli.Tests/SpecFilePathResolverTests.cs
+++ b/src/Cli.Tests/SpecFilePathResolverTests.cs
@@ -18,7 +18,7 @@ internal sealed class SpecFilePathResolverTests {
[OneTimeSetUp]
public void SetupHomeDir() {
- _tempHome = Path.Combine( Path.GetTempPath(), "fake-home-" + Guid.NewGuid().ToString() );
+ _tempHome = Path.Combine( Path.GetTempPath(), "fake-home-" + Guid.NewGuid() );
Directory.CreateDirectory( _tempHome );
_originalHome = Environment.GetEnvironmentVariable( HomeEnvVar );
diff --git a/src/Cli/Cli.csproj b/src/Cli/Cli.csproj
index 4c2454ff..cb2a0d64 100644
--- a/src/Cli/Cli.csproj
+++ b/src/Cli/Cli.csproj
@@ -1,4 +1,4 @@
-
+
linux-x64;linux-musl-x64;linux-arm64;linux-arm
@@ -14,14 +14,19 @@
+
+
+
+
+
@@ -47,4 +52,8 @@
+
+
+
+
diff --git a/src/Cli/Commands/Agent/AgentCommand.cs b/src/Cli/Commands/Agent/AgentCommand.cs
new file mode 100644
index 00000000..09eb43d3
--- /dev/null
+++ b/src/Cli/Commands/Agent/AgentCommand.cs
@@ -0,0 +1,24 @@
+using Drift.Cli.Commands.Agent.Subcommands.Start;
+using Drift.Cli.Commands.Common.Commands;
+
+namespace Drift.Cli.Commands.Agent;
+
+internal class AgentCommand : ContainerCommandBase {
+ internal AgentCommand( IServiceProvider provider ) : base( "agent", "Manage the local Drift agent (PREVIEW)" ) {
+ Subcommands.Add( new AgentStartCommand( provider ) );
+ // Subcommands.Add( new AgentServiceCommand( provider ) );
+
+ /*// Support other init systems in the future
+ var installCmd = new Command( "install", "Create agent systemd service file" );
+ installCmd.Options.Add( new Option( "--join" ) {
+ Description = "Join the distributed agent network using a JWT"
+ } );
+ Subcommands.Add( installCmd );
+
+ var uninstallCmd = new Command( "uninstall", "Remove agent systemd service file" );
+ Subcommands.Add( uninstallCmd );
+
+ var statusCmd = new Command( "status", "Show agent status" );
+ Subcommands.Add( statusCmd );*/
+ }
+}
\ No newline at end of file
diff --git a/src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs b/src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs
new file mode 100644
index 00000000..9ae31289
--- /dev/null
+++ b/src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs
@@ -0,0 +1,74 @@
+using System.CommandLine;
+using Drift.Agent.Hosting;
+using Drift.Agent.PeerProtocol;
+using Drift.Cli.Abstractions;
+using Drift.Cli.Commands.Common.Commands;
+using Drift.Cli.Infrastructure;
+using Drift.Cli.Presentation.Console.Logging;
+using Drift.Cli.Presentation.Console.Managers.Abstractions;
+using Drift.Domain;
+using Drift.Networking.Clustering;
+
+namespace Drift.Cli.Commands.Agent.Subcommands.Start;
+
+internal class AgentStartCommand : CommandBase {
+ internal AgentStartCommand( IServiceProvider provider ) : base( "start", "Start a local Drift agent", provider ) {
+ Options.Add( AgentStartParameters.Options.Port );
+ Options.Add( AgentStartParameters.Options.Adoptable );
+ Options.Add( AgentStartParameters.Options.Join );
+ }
+
+ protected override AgentStartParameters CreateParameters( ParseResult result ) {
+ return new AgentStartParameters( result );
+ }
+}
+
+internal class AgentStartCommandHandler( IOutputManager output ) : ICommandHandler {
+ public async Task Invoke( AgentStartParameters parameters, CancellationToken cancellationToken ) {
+ output.Log.LogDebug( "Running 'agent start' command" );
+ var logger = output.GetLogger();
+
+ var identity = LoadAgentIdentity();
+
+ if ( identity == null ) {
+ logger.LogDebug( "Agent is not enrolled" );
+
+ var enrollmentRequest = new EnrollmentRequest( parameters.Adoptable, parameters.Join );
+ logger.LogInformation( "Agent cluster enrollment method is {EnrollmentMethod}", enrollmentRequest.Method );
+ }
+ else {
+ logger.LogDebug( "Agent is enrolled into cluster 'milkyway'" );
+ logger.LogInformation( "Attempting to re-join cluster 'milkyway'..." );
+ }
+
+ /*Inventory? inventory;
+
+ try {
+ inventory = await specProvider.GetDeserializedAsync( parameters.SpecFile );
+ }
+ catch ( FileNotFoundException ) {
+ return ExitCodes.GeneralError;
+ }*/
+
+ output.Log.LogDebug( "Starting agent..." );
+
+ await AgentHost.Run( parameters.Port, logger, ConfigureServices, cancellationToken );
+
+ output.Log.LogDebug( "Completed 'agent start' command" );
+
+ return ExitCodes.Success;
+
+ void ConfigureServices( IServiceCollection services ) {
+ RootCommandFactory.ConfigureSubnetProvider( services );
+ services.AddPeerProtocol();
+ }
+ }
+
+ private AgentId? LoadAgentIdentity() {
+ if ( false ) {
+ return AgentId.New(); // TODO load from file
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/Cli/Commands/Agent/Subcommands/Start/AgentStartParameters.cs b/src/Cli/Commands/Agent/Subcommands/Start/AgentStartParameters.cs
new file mode 100644
index 00000000..dffd01fb
--- /dev/null
+++ b/src/Cli/Commands/Agent/Subcommands/Start/AgentStartParameters.cs
@@ -0,0 +1,53 @@
+using System.CommandLine;
+using Drift.Cli.Commands.Common.Parameters;
+
+namespace Drift.Cli.Commands.Agent.Subcommands.Start;
+
+internal record AgentStartParameters : BaseParameters {
+ internal static class Options {
+ internal static readonly Option Adoptable = new("--adoptable") {
+ DefaultValueFactory = _ => false,
+ Description = "Allow this agent to be adopted by another peer in the agent cluster"
+ };
+
+ // support @ for supplying local file
+ internal static readonly Option Join = new("--join") { Description = "Join the agent cluster using a JWT" };
+
+ internal static readonly Option Daemon = new("--daemon", "-d") {
+ Description = "Run the agent as a background daemon"
+ };
+
+ internal static readonly Option Port = new("--port", "-p") {
+ DefaultValueFactory = _ => 51515, Description = "Set the port used for both adoption and communication"
+ };
+ }
+
+ internal AgentStartParameters( ParseResult parseResult ) : base( parseResult ) {
+ Port = parseResult.GetValue( Options.Port );
+ Adoptable = parseResult.GetValue( Options.Adoptable );
+ Join = parseResult.GetValue( Options.Join );
+
+ if ( !Adoptable && string.IsNullOrWhiteSpace( Join ) ) {
+ throw new ArgumentException( "Either --adoptable or --join must be specified." );
+ }
+
+ if ( Adoptable && !string.IsNullOrWhiteSpace( Join ) ) {
+ throw new ArgumentException( "Cannot specify both --adoptable and --join." );
+ }
+ }
+
+ public string? Join {
+ get;
+ set;
+ }
+
+ public bool Adoptable {
+ get;
+ set;
+ }
+
+ public ushort Port {
+ get;
+ set;
+ }
+}
\ No newline at end of file
diff --git a/src/Cli/Commands/Common/CommandBase.cs b/src/Cli/Commands/Common/Commands/CommandBase.cs
similarity index 88%
rename from src/Cli/Commands/Common/CommandBase.cs
rename to src/Cli/Commands/Common/Commands/CommandBase.cs
index 111412df..cdfd1c50 100644
--- a/src/Cli/Commands/Common/CommandBase.cs
+++ b/src/Cli/Commands/Common/Commands/CommandBase.cs
@@ -1,10 +1,10 @@
using System.CommandLine;
-using Microsoft.Extensions.DependencyInjection;
+using Drift.Cli.Commands.Common.Parameters;
-namespace Drift.Cli.Commands.Common;
+namespace Drift.Cli.Commands.Common.Commands;
internal abstract class CommandBase : Command
- where TParameters : DefaultParameters
+ where TParameters : BaseParameters
where THandler : ICommandHandler {
protected CommandBase( string name, string description, IServiceProvider provider ) : base( name, description ) {
Add( CommonParameters.Options.Verbose );
diff --git a/src/Cli/Commands/Common/ICommandHandler.cs b/src/Cli/Commands/Common/Commands/ICommandHandler.cs
similarity index 56%
rename from src/Cli/Commands/Common/ICommandHandler.cs
rename to src/Cli/Commands/Common/Commands/ICommandHandler.cs
index e303b028..07ea9ef0 100644
--- a/src/Cli/Commands/Common/ICommandHandler.cs
+++ b/src/Cli/Commands/Common/Commands/ICommandHandler.cs
@@ -1,5 +1,7 @@
-namespace Drift.Cli.Commands.Common;
+using Drift.Cli.Commands.Common.Parameters;
-internal interface ICommandHandler where TParameters : DefaultParameters {
+namespace Drift.Cli.Commands.Common.Commands;
+
+internal interface ICommandHandler where TParameters : BaseParameters {
Task Invoke( TParameters parameters, CancellationToken cancellationToken );
}
\ No newline at end of file
diff --git a/src/Cli/Commands/Common/Commands/SpecCommandBase.cs b/src/Cli/Commands/Common/Commands/SpecCommandBase.cs
new file mode 100644
index 00000000..146b4c3c
--- /dev/null
+++ b/src/Cli/Commands/Common/Commands/SpecCommandBase.cs
@@ -0,0 +1,8 @@
+using System.CommandLine;
+
+namespace Drift.Cli.Commands.Common.Commands;
+
+internal class ContainerCommandBase : Command {
+ public ContainerCommandBase( string name, string description ) : base( name, description ) {
+ }
+}
\ No newline at end of file
diff --git a/src/Cli/Commands/Common/DefaultParameters.cs b/src/Cli/Commands/Common/DefaultParameters.cs
deleted file mode 100644
index 8e8eb2db..00000000
--- a/src/Cli/Commands/Common/DefaultParameters.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.CommandLine;
-using Drift.Cli.Presentation.Console;
-
-namespace Drift.Cli.Commands.Common;
-
-internal record DefaultParameters {
- internal DefaultParameters( ParseResult parseResult ) {
- OutputFormat = parseResult.GetValue( CommonParameters.Options.OutputFormat );
- SpecFile = parseResult.GetValue( CommonParameters.Arguments.Spec );
- }
-
- internal OutputFormat OutputFormat {
- get;
- }
-
- internal FileInfo? SpecFile {
- get;
- }
-}
\ No newline at end of file
diff --git a/src/Cli/Commands/Common/Parameters/BaseParameters.cs b/src/Cli/Commands/Common/Parameters/BaseParameters.cs
new file mode 100644
index 00000000..07e4338e
--- /dev/null
+++ b/src/Cli/Commands/Common/Parameters/BaseParameters.cs
@@ -0,0 +1,14 @@
+using System.CommandLine;
+using Drift.Cli.Presentation.Console;
+
+namespace Drift.Cli.Commands.Common.Parameters;
+
+internal abstract record BaseParameters {
+ protected BaseParameters( ParseResult parseResult ) {
+ OutputFormat = parseResult.GetValue( CommonParameters.Options.OutputFormat );
+ }
+
+ internal OutputFormat OutputFormat {
+ get;
+ }
+}
\ No newline at end of file
diff --git a/src/Cli/Commands/Common/Parameters/SpecParameters.cs b/src/Cli/Commands/Common/Parameters/SpecParameters.cs
new file mode 100644
index 00000000..3d55c247
--- /dev/null
+++ b/src/Cli/Commands/Common/Parameters/SpecParameters.cs
@@ -0,0 +1,13 @@
+using System.CommandLine;
+
+namespace Drift.Cli.Commands.Common.Parameters;
+
+internal abstract record SpecParameters : BaseParameters {
+ protected SpecParameters( ParseResult parseResult ) : base( parseResult ) {
+ SpecFile = parseResult.GetValue( CommonParameters.Arguments.Spec );
+ }
+
+ internal FileInfo? SpecFile {
+ get;
+ }
+}
\ No newline at end of file
diff --git a/src/Cli/Commands/Common/ParseResultHolder.cs b/src/Cli/Commands/Common/ParseResultHolder.cs
index a05afe98..ee3b7751 100644
--- a/src/Cli/Commands/Common/ParseResultHolder.cs
+++ b/src/Cli/Commands/Common/ParseResultHolder.cs
@@ -8,7 +8,7 @@ internal class ParseResultHolder {
public ParseResult ParseResult {
get => _parseResult ??
throw new InvalidOperationException(
- $"{nameof(ParseResult)} is null. This should have been set via dependency injection."
+ $"{nameof(ParseResult)} is null. This should have been set during dependency injection."
);
set => _parseResult = value;
}
diff --git a/src/Cli/Commands/Init/InitCommand.cs b/src/Cli/Commands/Init/InitCommand.cs
index a49ceb7a..581ab266 100644
--- a/src/Cli/Commands/Init/InitCommand.cs
+++ b/src/Cli/Commands/Init/InitCommand.cs
@@ -1,6 +1,6 @@
using System.CommandLine;
using Drift.Cli.Abstractions;
-using Drift.Cli.Commands.Common;
+using Drift.Cli.Commands.Common.Commands;
using Drift.Cli.Commands.Init.Helpers;
using Drift.Cli.Commands.Scan.NonInteractive;
using Drift.Cli.Presentation.Console;
@@ -11,7 +11,6 @@
using Drift.Common.Network;
using Drift.Domain.Scan;
using Drift.Scanning.Subnets.Interface;
-using Microsoft.Extensions.Logging;
using Spectre.Console;
namespace Drift.Cli.Commands.Init;
@@ -179,7 +178,7 @@ private async Task Initialize( InitOptions options ) {
return false;
}
- var scanOptions = new NetworkScanOptions { Cidrs = interfaceSubnetProvider.Get().ToList() };
+ var scanOptions = new NetworkScanOptions { Cidrs = ( await interfaceSubnetProvider.GetAsync() ).ToList() };
LogSubnetDetails( scanOptions );
diff --git a/src/Cli/Commands/Init/InitParameters.cs b/src/Cli/Commands/Init/InitParameters.cs
index 8115012a..0eac12a6 100644
--- a/src/Cli/Commands/Init/InitParameters.cs
+++ b/src/Cli/Commands/Init/InitParameters.cs
@@ -1,9 +1,9 @@
using System.CommandLine;
-using Drift.Cli.Commands.Common;
+using Drift.Cli.Commands.Common.Parameters;
namespace Drift.Cli.Commands.Init;
-internal record InitParameters : DefaultParameters {
+internal record InitParameters : SpecParameters {
internal bool? Discover {
get;
}
diff --git a/src/Cli/Commands/Lint/LintCommand.cs b/src/Cli/Commands/Lint/LintCommand.cs
index 28f99de0..ba9ab8e4 100644
--- a/src/Cli/Commands/Lint/LintCommand.cs
+++ b/src/Cli/Commands/Lint/LintCommand.cs
@@ -1,13 +1,13 @@
using System.CommandLine;
using Drift.Cli.Abstractions;
-using Drift.Cli.Commands.Common;
+using Drift.Cli.Commands.Common.Commands;
using Drift.Cli.Commands.Lint.Presentation;
using Drift.Cli.Presentation.Console;
using Drift.Cli.Presentation.Console.Managers.Abstractions;
using Drift.Cli.Presentation.Rendering;
using Drift.Cli.SpecFile;
+using Drift.Spec.Schema;
using Drift.Spec.Validation;
-using Microsoft.Extensions.Logging;
namespace Drift.Cli.Commands.Lint;
@@ -48,7 +48,7 @@ public async Task Invoke( LintParameters parameters, CancellationToken canc
var yamlContent = await File.ReadAllTextAsync( filePath!.FullName, cancellationToken );
- var result = SpecValidator.Validate( yamlContent, Spec.Schema.SpecVersion.V1_preview );
+ var result = SpecValidator.Validate( yamlContent, SpecVersion.V1_preview );
IRenderer renderer =
parameters.OutputFormat switch {
diff --git a/src/Cli/Commands/Lint/LintParameters.cs b/src/Cli/Commands/Lint/LintParameters.cs
index b3e533bf..ef92c71d 100644
--- a/src/Cli/Commands/Lint/LintParameters.cs
+++ b/src/Cli/Commands/Lint/LintParameters.cs
@@ -1,9 +1,9 @@
using System.CommandLine;
-using Drift.Cli.Commands.Common;
+using Drift.Cli.Commands.Common.Parameters;
namespace Drift.Cli.Commands.Lint;
-internal record LintParameters : DefaultParameters {
+internal record LintParameters : SpecParameters {
internal LintParameters( ParseResult parseResult ) : base( parseResult ) {
}
}
\ No newline at end of file
diff --git a/src/Cli/Commands/Lint/Presentation/LogLintRenderer.cs b/src/Cli/Commands/Lint/Presentation/LogLintRenderer.cs
index e4cebbd5..776c3ebb 100644
--- a/src/Cli/Commands/Lint/Presentation/LogLintRenderer.cs
+++ b/src/Cli/Commands/Lint/Presentation/LogLintRenderer.cs
@@ -1,7 +1,6 @@
using Drift.Cli.Presentation.Console.Managers.Abstractions;
using Drift.Cli.Presentation.Rendering;
using Drift.Spec.Validation;
-using Microsoft.Extensions.Logging;
namespace Drift.Cli.Commands.Lint.Presentation;
diff --git a/src/Cli/Commands/Preview/AgentCommand.cs b/src/Cli/Commands/Preview/AgentCommand.cs
deleted file mode 100644
index 4c036ff7..00000000
--- a/src/Cli/Commands/Preview/AgentCommand.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System.CommandLine;
-
-namespace Drift.Cli.Commands.Preview;
-
-internal class AgentCommand : Command {
- internal AgentCommand() : base( "agent", "Manage the local Drift agent" ) {
- var runCmd = new Command( "run", "Start the agent process" );
- runCmd.Options.Add( new Option( "--adoptable" ) {
- Description = "Allow this agent to be adopted by another peer in the distributed agent network"
- } );
- // terminology: agent network or agent group?
- // support @ for supplying local file
- runCmd.Options.Add( new Option( "--join" ) {
- Description = "Join the distributed agent network using a JWT"
- } );
- runCmd.Options.Add( new Option( "--daemon", "-d" ) { Description = "Run the agent as a background daemon" } );
- runCmd.Options.Add( new Option( "--adoptable"
- ) { Description = "Allow this agent to be adopted by another peer in the distributed agent network" } );
- Subcommands.Add( runCmd );
-
- // Support other init systems in the future
- var installCmd = new Command( "install", "Create agent systemd service file" );
- installCmd.Options.Add( new Option( "--join" ) {
- Description = "Join the distributed agent network using a JWT"
- } );
- Subcommands.Add( installCmd );
-
- var uninstallCmd = new Command( "uninstall", "Remove agent systemd service file" );
- Subcommands.Add( uninstallCmd );
-
- var statusCmd = new Command( "status", "Show agent status" );
- Subcommands.Add( statusCmd );
-
- // logs?
- }
-}
\ No newline at end of file
diff --git a/src/Cli/Commands/Scan/AgentSubnetProvider.cs b/src/Cli/Commands/Scan/AgentSubnetProvider.cs
new file mode 100644
index 00000000..ed997b2a
--- /dev/null
+++ b/src/Cli/Commands/Scan/AgentSubnetProvider.cs
@@ -0,0 +1,39 @@
+using Drift.Domain;
+using Drift.Networking.Clustering;
+using Drift.Scanning.Subnets;
+
+namespace Drift.Cli.Commands.Scan;
+
+internal sealed class AgentSubnetProvider(
+ ILogger logger,
+ List agents,
+ ICluster cluster,
+ CancellationToken cancellationToken
+) : ISubnetProvider {
+ public async Task> GetAsync() {
+ logger.LogDebug( "Getting subnets from agents" );
+ var allSubnets = new List();
+
+ foreach ( var agent in agents ) {
+ logger.LogInformation( "Requesting subnets from agent {Id} ({Address})", agent.Id, agent.Address );
+
+ try {
+ var response = await cluster.GetSubnetsAsync( agent, cancellationToken );
+
+ logger.LogInformation(
+ "Received subnet(s) from agent {Id} ({Address}): {Subnets}",
+ agent.Id,
+ agent.Address,
+ string.Join( ", ", response.Subnets )
+ );
+
+ allSubnets.AddRange( response.Subnets );
+ }
+ catch ( Exception ex ) {
+ logger.LogInformation( ex, "Failed requesting subnets from agent {Id} ({Address})", agent.Id, agent.Address );
+ }
+ }
+
+ return allSubnets;
+ }
+}
\ No newline at end of file
diff --git a/src/Cli/Commands/Scan/ClusterExtensions.cs b/src/Cli/Commands/Scan/ClusterExtensions.cs
new file mode 100644
index 00000000..03347cba
--- /dev/null
+++ b/src/Cli/Commands/Scan/ClusterExtensions.cs
@@ -0,0 +1,19 @@
+using Drift.Agent.PeerProtocol.Subnets;
+using Drift.Networking.Clustering;
+
+namespace Drift.Cli.Commands.Scan;
+
+internal static class ClusterExtensions {
+ internal static Task GetSubnetsAsync(
+ this ICluster cluster,
+ Domain.Agent agent,
+ CancellationToken cancellationToken
+ ) {
+ return cluster.SendAndWaitAsync(
+ agent,
+ new SubnetsRequest(),
+ timeout: TimeSpan.FromSeconds( 10 ),
+ cancellationToken
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Cli/Commands/Scan/NonInteractive/NonInteractiveUi.cs b/src/Cli/Commands/Scan/NonInteractive/NonInteractiveUi.cs
index 169a91cb..5fc1bd79 100644
--- a/src/Cli/Commands/Scan/NonInteractive/NonInteractiveUi.cs
+++ b/src/Cli/Commands/Scan/NonInteractive/NonInteractiveUi.cs
@@ -8,7 +8,6 @@
using Drift.Cli.Presentation.Rendering;
using Drift.Domain;
using Drift.Domain.Scan;
-using Microsoft.Extensions.Logging;
using Spectre.Console;
namespace Drift.Cli.Commands.Scan.NonInteractive;
diff --git a/src/Cli/Commands/Scan/Rendering/LogScanRenderer.cs b/src/Cli/Commands/Scan/Rendering/LogScanRenderer.cs
index 54ad61a9..1e6c6b7f 100644
--- a/src/Cli/Commands/Scan/Rendering/LogScanRenderer.cs
+++ b/src/Cli/Commands/Scan/Rendering/LogScanRenderer.cs
@@ -2,7 +2,6 @@
using Drift.Cli.Presentation.Console.Managers.Abstractions;
using Drift.Cli.Presentation.Rendering;
using Drift.Cli.Presentation.Rendering.DeviceState;
-using Microsoft.Extensions.Logging;
namespace Drift.Cli.Commands.Scan.Rendering;
diff --git a/src/Cli/Commands/Scan/ResultProcessors/SubnetScanResultProcessor.cs b/src/Cli/Commands/Scan/ResultProcessors/SubnetScanResultProcessor.cs
index 0359f08a..2f4ebabf 100644
--- a/src/Cli/Commands/Scan/ResultProcessors/SubnetScanResultProcessor.cs
+++ b/src/Cli/Commands/Scan/ResultProcessors/SubnetScanResultProcessor.cs
@@ -18,7 +18,7 @@ namespace Drift.Cli.Commands.Scan.ResultProcessors;
internal static class SubnetScanResultProcessor {
private const string Na = "n/a";
- private static int _deviceIdCounter = 0;
+ private static int _deviceIdCounter;
internal static List Process( SubnetScanResult scanResult, Network? network ) {
// TODO test throw new Exception( "ads" );
diff --git a/src/Cli/Commands/Scan/ScanCommand.cs b/src/Cli/Commands/Scan/ScanCommand.cs
index 1aac7dc0..f23a344b 100644
--- a/src/Cli/Commands/Scan/ScanCommand.cs
+++ b/src/Cli/Commands/Scan/ScanCommand.cs
@@ -1,17 +1,18 @@
using System.CommandLine;
using Drift.Cli.Abstractions;
-using Drift.Cli.Commands.Common;
+using Drift.Cli.Commands.Common.Commands;
using Drift.Cli.Commands.Scan.Interactive;
using Drift.Cli.Commands.Scan.Interactive.Input;
using Drift.Cli.Commands.Scan.NonInteractive;
+using Drift.Cli.Presentation.Console.Logging;
using Drift.Cli.Presentation.Console.Managers.Abstractions;
using Drift.Cli.SpecFile;
using Drift.Common.Network;
using Drift.Domain;
using Drift.Domain.Scan;
+using Drift.Networking.Clustering;
using Drift.Scanning.Subnets;
using Drift.Scanning.Subnets.Interface;
-using Microsoft.Extensions.Logging;
namespace Drift.Cli.Commands.Scan;
@@ -60,23 +61,35 @@ internal class ScanCommandHandler(
IOutputManager output,
INetworkScanner scanner,
IInterfaceSubnetProvider interfaceSubnetProvider,
- ISpecFileProvider specProvider
+ ISpecFileProvider specProvider,
+ ICluster cluster
) : ICommandHandler {
public async Task Invoke( ScanParameters parameters, CancellationToken cancellationToken ) {
output.Log.LogDebug( "Running scan command" );
- Network? network;
+ Inventory? inventory;
try {
- network = ( await specProvider.GetDeserializedAsync( parameters.SpecFile ) )?.Network;
+ inventory = await specProvider.GetDeserializedAsync( parameters.SpecFile );
}
catch ( FileNotFoundException ) {
return ExitCodes.GeneralError;
}
var subnetProviders = new List { interfaceSubnetProvider };
- if ( network != null ) {
- subnetProviders.Add( new PredefinedSubnetProvider( network.Subnets ) );
+ if ( inventory?.Network != null ) {
+ subnetProviders.Add( new PredefinedSubnetProvider( inventory.Network.Subnets ) );
+ }
+
+ if ( inventory?.Agents.Any() ?? false ) {
+ subnetProviders.Add(
+ new AgentSubnetProvider(
+ output.GetLogger(),
+ inventory.Agents,
+ cluster,
+ cancellationToken
+ )
+ );
}
var subnetProvider = new CompositeSubnetProvider( subnetProviders );
@@ -84,7 +97,7 @@ public async Task Invoke( ScanParameters parameters, CancellationToken canc
output.Normal.WriteLineVerbose( $"Using {subnetProvider.GetType().Name}" );
output.Log.LogDebug( "Using {SubnetProviderType}", subnetProvider.GetType().Name );
- var subnets = subnetProvider.Get();
+ var subnets = await subnetProvider.GetAsync();
var scanRequest = new NetworkScanOptions { Cidrs = subnets };
@@ -110,12 +123,19 @@ public async Task Invoke( ScanParameters parameters, CancellationToken canc
Task uiTask;
if ( parameters.Interactive ) {
- var ui = new InteractiveUi( output, network, scanner, scanRequest, new DefaultKeyMap(), parameters.ShowLogPanel );
+ var ui = new InteractiveUi(
+ output,
+ inventory?.Network,
+ scanner,
+ scanRequest,
+ new DefaultKeyMap(),
+ parameters.ShowLogPanel
+ );
uiTask = ui.RunAsync();
}
else {
var ui = new NonInteractiveUi( output, scanner );
- uiTask = ui.RunAsync( scanRequest, network, parameters.OutputFormat );
+ uiTask = ui.RunAsync( scanRequest, inventory?.Network, parameters.OutputFormat );
}
Task.WaitAll( uiTask );
diff --git a/src/Cli/Commands/Scan/ScanParameters.cs b/src/Cli/Commands/Scan/ScanParameters.cs
index 37178c9e..63235d26 100644
--- a/src/Cli/Commands/Scan/ScanParameters.cs
+++ b/src/Cli/Commands/Scan/ScanParameters.cs
@@ -1,9 +1,10 @@
using System.CommandLine;
using Drift.Cli.Commands.Common;
+using Drift.Cli.Commands.Common.Parameters;
namespace Drift.Cli.Commands.Scan;
-internal record ScanParameters : DefaultParameters {
+internal record ScanParameters : SpecParameters {
internal static class Options {
internal static readonly Option Interactive = new("--interactive", "-i") {
Description = "Interactive mode", Arity = ArgumentArity.Zero
diff --git a/src/Cli/DriftCli.cs b/src/Cli/DriftCli.cs
index e50e8b62..2b988b6b 100644
--- a/src/Cli/DriftCli.cs
+++ b/src/Cli/DriftCli.cs
@@ -2,7 +2,6 @@
using Drift.Cli.Abstractions;
using Drift.Cli.Infrastructure;
using Drift.Cli.Presentation.Rendering;
-using Microsoft.Extensions.DependencyInjection;
namespace Drift.Cli;
diff --git a/src/Cli/ExecutionEnvironment.cs b/src/Cli/ExecutionEnvironment.cs
index 96674e39..f1933a23 100644
--- a/src/Cli/ExecutionEnvironment.cs
+++ b/src/Cli/ExecutionEnvironment.cs
@@ -5,7 +5,7 @@ namespace Drift.Cli;
internal static class ExecutionEnvironment {
internal static DriftExecutionEnvironment GetCurrent() {
- var envVar = System.Environment.GetEnvironmentVariable( nameof(EnvVar.DRIFT_EXECUTION__ENVIRONMENT) );
+ var envVar = Environment.GetEnvironmentVariable( nameof(EnvVar.DRIFT_EXECUTION__ENVIRONMENT) );
return Get( envVar );
}
diff --git a/src/Cli/Infrastructure/RootCommandFactory.cs b/src/Cli/Infrastructure/RootCommandFactory.cs
index 1161862f..91687751 100644
--- a/src/Cli/Infrastructure/RootCommandFactory.cs
+++ b/src/Cli/Infrastructure/RootCommandFactory.cs
@@ -2,6 +2,9 @@
using System.CommandLine.Help;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
+using Drift.Agent.PeerProtocol;
+using Drift.Cli.Commands.Agent;
+using Drift.Cli.Commands.Agent.Subcommands.Start;
using Drift.Cli.Commands.Common;
using Drift.Cli.Commands.Help;
using Drift.Cli.Commands.Init;
@@ -14,11 +17,12 @@
using Drift.Cli.SpecFile;
using Drift.Domain.ExecutionEnvironment;
using Drift.Domain.Scan;
+using Drift.Networking.Clustering;
+using Drift.Networking.PeerStreaming.Client;
+using Drift.Networking.PeerStreaming.Core;
using Drift.Scanning;
using Drift.Scanning.Scanners;
using Drift.Scanning.Subnets.Interface;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
namespace Drift.Cli.Infrastructure;
@@ -66,6 +70,15 @@ private static void ConfigureDefaults( IServiceCollection services, bool toConso
ConfigureSpecProvider( services );
ConfigureSubnetProvider( services );
ConfigureNetworkScanner( services );
+ ConfigureAgentCluster( services );
+ }
+
+ private static void ConfigureAgentCluster( IServiceCollection services ) {
+ services.AddPeerStreamingCore( new PeerStreamingOptions {
+ MessageAssembly = typeof(PeerProtocolAssemblyMarker).Assembly
+ } );
+ services.AddPeerStreamingClient();
+ services.AddClustering();
}
private static void ConfigureExecutionEnvironment( IServiceCollection services ) {
@@ -76,7 +89,10 @@ private static RootCommand CreateRootCommand( IServiceProvider provider ) {
// TODO 'from' or 'against'?
var rootCommand =
new RootCommand( $"{Chars.SatelliteAntenna} Drift CLI — monitor network drift against your declared state" ) {
- new InitCommand( provider ), new ScanCommand( provider ), new LintCommand( provider )
+ new InitCommand( provider ),
+ new ScanCommand( provider ),
+ new LintCommand( provider ),
+ new AgentCommand( provider )
};
rootCommand.TreatUnmatchedTokensAsErrors = true;
@@ -95,6 +111,7 @@ private static void ConfigureOutput( IServiceCollection services, bool toConsole
var factory = sp.GetRequiredService();
return factory.Create( parseResult, plainConsole );
} );
+ // Note: since ILogger is scoped, singletons cannot access logging via DI
services.AddScoped( sp => sp.GetRequiredService().GetLogger() );
}
@@ -102,7 +119,7 @@ private static void ConfigureSpecProvider( IServiceCollection services ) {
services.AddScoped();
}
- private static void ConfigureSubnetProvider( IServiceCollection services ) {
+ public static void ConfigureSubnetProvider( IServiceCollection services ) {
services.AddScoped();
}
@@ -110,6 +127,7 @@ private static void ConfigureBuiltInCommandHandlers( IServiceCollection services
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
}
private static void ConfigureDynamicCommands( IServiceCollection services, CommandRegistration[] commands ) {
diff --git a/src/Cli/Presentation/Console/Logging/NormalOutputLoggerAdapter.cs b/src/Cli/Presentation/Console/Logging/NormalOutputLoggerAdapter.cs
index dbb31e36..330d3204 100644
--- a/src/Cli/Presentation/Console/Logging/NormalOutputLoggerAdapter.cs
+++ b/src/Cli/Presentation/Console/Logging/NormalOutputLoggerAdapter.cs
@@ -1,5 +1,4 @@
using Drift.Cli.Presentation.Console.Managers.Abstractions;
-using Microsoft.Extensions.Logging;
namespace Drift.Cli.Presentation.Console.Logging;
@@ -17,18 +16,38 @@ public void Log(
case LogLevel.Critical:
case LogLevel.Error:
normalOutput.WriteLineError( message );
+ if ( exception != null ) {
+ normalOutput.WriteLineError( exception.ToString() );
+ }
+
break;
case LogLevel.Warning:
normalOutput.WriteLineWarning( message );
+ if ( exception != null ) {
+ normalOutput.WriteLineWarning( exception.ToString() );
+ }
+
break;
case LogLevel.Information:
normalOutput.WriteLine( message );
+ if ( exception != null ) {
+ normalOutput.WriteLine( exception.ToString() );
+ }
+
break;
case LogLevel.Debug:
normalOutput.WriteLineVerbose( message );
+ if ( exception != null ) {
+ normalOutput.WriteLineVerbose( exception.ToString() );
+ }
+
break;
case LogLevel.Trace:
normalOutput.WriteLineVeryVerbose( message );
+ if ( exception != null ) {
+ normalOutput.WriteLineVeryVerbose( exception.ToString() );
+ }
+
break;
case LogLevel.None:
break;
diff --git a/src/Cli/Presentation/Console/Logging/OutputManagerExtensions.cs b/src/Cli/Presentation/Console/Logging/OutputManagerExtensions.cs
index f252e41e..d65e3166 100644
--- a/src/Cli/Presentation/Console/Logging/OutputManagerExtensions.cs
+++ b/src/Cli/Presentation/Console/Logging/OutputManagerExtensions.cs
@@ -1,6 +1,5 @@
using Drift.Cli.Presentation.Console.Managers.Abstractions;
using Drift.Common.Logging;
-using Microsoft.Extensions.Logging;
namespace Drift.Cli.Presentation.Console.Logging;
diff --git a/src/Cli/Presentation/Console/Managers/Abstractions/ILogOutput.cs b/src/Cli/Presentation/Console/Managers/Abstractions/ILogOutput.cs
index 05bd518c..0dedb05b 100644
--- a/src/Cli/Presentation/Console/Managers/Abstractions/ILogOutput.cs
+++ b/src/Cli/Presentation/Console/Managers/Abstractions/ILogOutput.cs
@@ -1,5 +1,3 @@
-using Microsoft.Extensions.Logging;
-
namespace Drift.Cli.Presentation.Console.Managers.Abstractions;
internal interface ILogOutput : ILogger;
\ No newline at end of file
diff --git a/src/Cli/Presentation/Console/Managers/ConsoleOutputManager.cs b/src/Cli/Presentation/Console/Managers/ConsoleOutputManager.cs
index 5295d769..a16bd5a0 100644
--- a/src/Cli/Presentation/Console/Managers/ConsoleOutputManager.cs
+++ b/src/Cli/Presentation/Console/Managers/ConsoleOutputManager.cs
@@ -1,6 +1,5 @@
using Drift.Cli.Presentation.Console.Managers.Abstractions;
using Drift.Cli.Presentation.Console.Managers.Outputs;
-using Microsoft.Extensions.Logging;
namespace Drift.Cli.Presentation.Console.Managers;
diff --git a/src/Cli/Presentation/Console/Managers/NullOutputManager.cs b/src/Cli/Presentation/Console/Managers/NullOutputManager.cs
index b6460270..a80b6003 100644
--- a/src/Cli/Presentation/Console/Managers/NullOutputManager.cs
+++ b/src/Cli/Presentation/Console/Managers/NullOutputManager.cs
@@ -1,5 +1,4 @@
using Drift.Cli.Presentation.Console.Managers.Abstractions;
-using Microsoft.Extensions.Logging;
using Spectre.Console;
namespace Drift.Cli.Presentation.Console.Managers;
diff --git a/src/Cli/Presentation/Console/Managers/Outputs/LogOutput.cs b/src/Cli/Presentation/Console/Managers/Outputs/LogOutput.cs
index 3d87c45a..d035010a 100644
--- a/src/Cli/Presentation/Console/Managers/Outputs/LogOutput.cs
+++ b/src/Cli/Presentation/Console/Managers/Outputs/LogOutput.cs
@@ -1,5 +1,4 @@
using Drift.Cli.Presentation.Console.Managers.Abstractions;
-using Microsoft.Extensions.Logging;
namespace Drift.Cli.Presentation.Console.Managers.Outputs;
diff --git a/src/Cli/Presentation/Console/Managers/Outputs/NormalOutput.cs b/src/Cli/Presentation/Console/Managers/Outputs/NormalOutput.cs
index e8fb4d76..f197d94f 100644
--- a/src/Cli/Presentation/Console/Managers/Outputs/NormalOutput.cs
+++ b/src/Cli/Presentation/Console/Managers/Outputs/NormalOutput.cs
@@ -79,14 +79,13 @@ private static void WriteLineInternal(
// ... on bgcolor]
}
}
- else {
- if ( foreground.HasValue ) {
- System.Console.ForegroundColor = foreground.Value;
- }
- if ( background.HasValue ) {
- System.Console.BackgroundColor = background.Value;
- }
+ if ( foreground.HasValue ) {
+ System.Console.ForegroundColor = foreground.Value;
+ }
+
+ if ( background.HasValue ) {
+ System.Console.BackgroundColor = background.Value;
}
textWriter.Write( line );
@@ -100,9 +99,8 @@ private static void WriteLineInternal(
System.Console.BackgroundColor = background.Value;
}
}
- else {
- System.Console.ResetColor();
- }
+
+ System.Console.ResetColor();
textWriter.WriteLine();
}
diff --git a/src/Cli/Presentation/Console/OutputManagerFactory.cs b/src/Cli/Presentation/Console/OutputManagerFactory.cs
index 976338ec..691a6ad8 100644
--- a/src/Cli/Presentation/Console/OutputManagerFactory.cs
+++ b/src/Cli/Presentation/Console/OutputManagerFactory.cs
@@ -5,7 +5,6 @@
using Drift.Cli.Presentation.Console.Managers;
using Drift.Cli.Presentation.Console.Managers.Abstractions;
using Drift.Common.IO;
-using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Serilog;
using Serilog.Events;
diff --git a/src/Cli/Properties/launchSettings.json b/src/Cli/Properties/launchSettings.json
index e8bbdf6a..d9f79120 100644
--- a/src/Cli/Properties/launchSettings.json
+++ b/src/Cli/Properties/launchSettings.json
@@ -20,7 +20,7 @@
},
"scan ~": {
"commandName": "Project",
- "commandLineArgs": "scan ~",
+ "commandLineArgs": "scan ~ -v",
"dotnetRunMessages": true,
"environmentVariables": {}
},
@@ -47,6 +47,18 @@
"commandLineArgs": "lint ~",
"dotnetRunMessages": true,
"environmentVariables": {}
+ },
+ "agent start (home)": {
+ "commandName": "Project",
+ "commandLineArgs": "agent start ~ -vv -o log --adoptable",
+ "dotnetRunMessages": true,
+ "environmentVariables": {}
+ },
+ "agent start --port 51516 (home)": {
+ "commandName": "Project",
+ "commandLineArgs": "agent start ~ --port 51516",
+ "dotnetRunMessages": true,
+ "environmentVariables": {}
}
}
}
diff --git a/src/Cli/SpecFile/FileSystemSpecProvider.cs b/src/Cli/SpecFile/FileSystemSpecProvider.cs
index 7c46a0df..c2b312e7 100644
--- a/src/Cli/SpecFile/FileSystemSpecProvider.cs
+++ b/src/Cli/SpecFile/FileSystemSpecProvider.cs
@@ -5,7 +5,6 @@
using Drift.Spec.Schema;
using Drift.Spec.Serialization;
using Drift.Spec.Validation;
-using Microsoft.Extensions.Logging;
namespace Drift.Cli.SpecFile;
diff --git a/src/Cli/SpecFile/SpecFilePathResolver.cs b/src/Cli/SpecFile/SpecFilePathResolver.cs
index 7b6c54db..4b7ee1c5 100644
--- a/src/Cli/SpecFile/SpecFilePathResolver.cs
+++ b/src/Cli/SpecFile/SpecFilePathResolver.cs
@@ -1,5 +1,4 @@
using Drift.Cli.Presentation.Console.Managers.Abstractions;
-using Microsoft.Extensions.Logging;
namespace Drift.Cli.SpecFile;
diff --git a/src/Diff.Tests/DiffTest.cs b/src/Diff.Tests/DiffTest.cs
index fbb171db..5ee475d5 100644
--- a/src/Diff.Tests/DiffTest.cs
+++ b/src/Diff.Tests/DiffTest.cs
@@ -1,6 +1,4 @@
using System.Globalization;
-using System.Text.Json;
-using System.Text.Json.Serialization;
using Drift.Diff.Domain;
using Drift.Domain;
using Drift.Domain.Device.Addresses;
@@ -8,6 +6,7 @@
using Drift.Domain.Device.Discovered;
using Drift.Domain.Extensions;
using Drift.Domain.Scan;
+using Drift.EnvironmentConfig.Converters;
using Drift.TestUtilities;
using JsonConverter = Drift.Serialization.JsonConverter;
@@ -247,21 +246,4 @@ private static void Print( List diffs ) {
Console.WriteLine( $"{diff.PropertyPath}: {diff.DiffType} — '{diff.Original}' → '{diff.Updated}'" );
}
}
-
- private sealed class IpAddressConverter : JsonConverter {
- public override System.Net.IPAddress Read(
- ref Utf8JsonReader reader,
- Type typeToConvert,
- JsonSerializerOptions options
- ) {
- string? ip = reader.GetString();
- var ipAddress = ( ip == null ) ? null : System.Net.IPAddress.Parse( ip );
- return ipAddress ?? throw new Exception( "Cannot read" ); // System.Net.IPAddress.None;
- }
-
- public override void Write( Utf8JsonWriter writer, System.Net.IPAddress value, JsonSerializerOptions options ) {
- ArgumentNullException.ThrowIfNull( writer );
- writer.WriteStringValue( value?.ToString() );
- }
- }
}
\ No newline at end of file
diff --git a/src/Domain/AgentId.cs b/src/Domain/AgentId.cs
new file mode 100644
index 00000000..29da6a02
--- /dev/null
+++ b/src/Domain/AgentId.cs
@@ -0,0 +1,37 @@
+using System.Text.Json.Serialization;
+
+namespace Drift.Domain;
+
+public class AgentId {
+ private const string Prefix = "agentid_";
+
+ public string Value {
+ get;
+ set;
+ }
+
+ [JsonConstructor]
+ public AgentId() {
+ }
+
+ public AgentId( string value ) {
+ if ( !value.StartsWith( Prefix ) )
+ throw new FormatException( $"AgentId must start with '{Prefix}'." );
+
+ Value = value;
+ }
+
+ public static AgentId New() => new AgentId( Prefix + Guid.NewGuid() );
+
+ public static implicit operator AgentId( string value ) => new AgentId( value );
+
+ public static implicit operator string( AgentId id ) => id.Value;
+
+ public bool IsGuidBased =>
+ Guid.TryParse( Value[Prefix.Length..], out _ );
+
+ public Guid? AsGuidOrNull =>
+ Guid.TryParse( Value[Prefix.Length..], out var guid ) ? guid : null;
+
+ public override string ToString() => Value;
+}
\ No newline at end of file
diff --git a/src/Domain/Environment.cs b/src/Domain/Environment.cs
new file mode 100644
index 00000000..4a0a52a6
--- /dev/null
+++ b/src/Domain/Environment.cs
@@ -0,0 +1,65 @@
+using System.Text.Json.Serialization;
+
+// TODO remove when no longer a draft
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
+
+namespace Drift.Domain;
+
+/*[JsonSerializable( typeof(Environment) )] // Enable source generation for this type
+[JsonSourceGenerationOptions( GenerationMode = JsonSourceGenerationMode.Default )]
+public partial class EnvironmentContext : JsonSerializerContext {
+}*/
+
+public record Environment {
+ public required string Name {
+ get;
+ init;
+ }
+
+ public bool Active {
+ get;
+ set;
+ }
+
+ public List Agents {
+ get;
+ set;
+ }
+}
+
+public record Agent {
+ public string Id {
+ get;
+ set;
+ }
+
+ /*public IpAddress Address {
+ //TODO support hostname too, maybe even mac!?
+ get;
+ set;
+ }*/
+
+ public string Address {
+ get;
+ set;
+ }
+
+ public AgentAuthentication Authentication {
+ get;
+ set;
+ }
+}
+
+public record AgentAuthentication {
+ [JsonIgnore( Condition = JsonIgnoreCondition.Never )]
+ public AuthType Type {
+ get;
+ set;
+ }
+}
+
+public enum AuthType {
+ None = 1,
+ ApiKey = 2,
+ Certificate =3
+}
\ No newline at end of file
diff --git a/src/Domain/Inventory.cs b/src/Domain/Inventory.cs
index 2ece762e..347999be 100644
--- a/src/Domain/Inventory.cs
+++ b/src/Domain/Inventory.cs
@@ -5,4 +5,9 @@ public required Network Network {
get;
init;
}
+
+ public List Agents {
+ get;
+ set;
+ } = [];
}
\ No newline at end of file
diff --git a/src/Domain/RequestId.cs b/src/Domain/RequestId.cs
new file mode 100644
index 00000000..0247e632
--- /dev/null
+++ b/src/Domain/RequestId.cs
@@ -0,0 +1,19 @@
+namespace Drift.Domain;
+
+public record RequestId( Guid Value ) {
+ private const string Prefix = "requestid_";
+
+ public static implicit operator RequestId( string value ) {
+ if ( !value.StartsWith( Prefix ) )
+ throw new FormatException( $"Invalid RequestId format. Must start with '{Prefix}'." );
+
+ var guidPart = value[Prefix.Length..];
+
+ if ( !Guid.TryParse( guidPart, out var guid ) )
+ throw new FormatException( "Invalid GUID in RequestId." );
+
+ return new RequestId( guid );
+ }
+
+ public static implicit operator string( RequestId id ) => $"{Prefix}{id.Value}";
+}
\ No newline at end of file
diff --git a/src/EnvironmentConfig/Converters/CidrBlockConverter.cs b/src/EnvironmentConfig/Converters/CidrBlockConverter.cs
new file mode 100644
index 00000000..009fbab8
--- /dev/null
+++ b/src/EnvironmentConfig/Converters/CidrBlockConverter.cs
@@ -0,0 +1,25 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Drift.Domain;
+
+namespace Drift.EnvironmentConfig.Converters;
+
+public sealed class CidrBlockConverter : JsonConverter {
+ public override CidrBlock Read(
+ ref Utf8JsonReader reader,
+ Type typeToConvert,
+ JsonSerializerOptions options
+ ) {
+ var cidrString = reader.GetString();
+ if ( string.IsNullOrEmpty( cidrString ) ) {
+ throw new JsonException( "CIDR block string cannot be null or empty" );
+ }
+
+ return new CidrBlock( cidrString );
+ }
+
+ public override void Write( Utf8JsonWriter writer, CidrBlock value, JsonSerializerOptions options ) {
+ ArgumentNullException.ThrowIfNull( writer );
+ writer.WriteStringValue( value.ToString() );
+ }
+}
\ No newline at end of file
diff --git a/src/EnvironmentConfig/Converters/IpAddressConverter.cs b/src/EnvironmentConfig/Converters/IpAddressConverter.cs
new file mode 100644
index 00000000..f0ce208b
--- /dev/null
+++ b/src/EnvironmentConfig/Converters/IpAddressConverter.cs
@@ -0,0 +1,21 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Drift.EnvironmentConfig.Converters;
+
+public sealed class IpAddressConverter : JsonConverter {
+ public override System.Net.IPAddress Read(
+ ref Utf8JsonReader reader,
+ Type typeToConvert,
+ JsonSerializerOptions options
+ ) {
+ string? ip = reader.GetString();
+ var ipAddress = ( ip == null ) ? null : System.Net.IPAddress.Parse( ip );
+ return ipAddress ?? throw new Exception( "Cannot read" ); // System.Net.IPAddress.None;
+ }
+
+ public override void Write( Utf8JsonWriter writer, System.Net.IPAddress value, JsonSerializerOptions options ) {
+ ArgumentNullException.ThrowIfNull( writer );
+ writer.WriteStringValue( value?.ToString() );
+ }
+}
diff --git a/src/Networking.Clustering/Cluster.cs b/src/Networking.Clustering/Cluster.cs
new file mode 100644
index 00000000..ef57286e
--- /dev/null
+++ b/src/Networking.Clustering/Cluster.cs
@@ -0,0 +1,77 @@
+using Drift.Networking.PeerStreaming.Core.Abstractions;
+using Drift.Networking.PeerStreaming.Core.Messages;
+using Microsoft.Extensions.Logging;
+
+namespace Drift.Networking.Clustering;
+
+internal sealed class Cluster(
+ IPeerMessageEnvelopeConverter envelopeConverter,
+ IPeerStreamManager peerStreamManager,
+ PeerResponseCorrelator responseCorrelator,
+ ILogger logger
+) : ICluster {
+ public async Task SendAsync(
+ Domain.Agent agent,
+ IPeerMessage message,
+ CancellationToken cancellationToken = default
+ ) {
+ try {
+ await SendInternalAsync( agent, message, cancellationToken );
+ }
+ catch ( Exception ex ) {
+ logger.LogWarning( ex, "Send to {Peer} failed", agent );
+ }
+ }
+
+ /* public async Task BroadcastAsync( PeerMessage message, CancellationToken cancellationToken = default ) {
+ var peers = peerStreamManager.GetConnectedPeers();
+
+ var tasks = peers.Select( async peer => {
+ // TODO optimistically assume connection is alive, but automatically reconnect if it's not
+ try {
+ await SendInternalAsync( peer, message, cancellationToken );
+ }
+ catch ( Exception ex ) {
+ logger.LogWarning( ex, "Broadcast to {Peer} failed", peer );
+ }
+ } );
+
+ await Task.WhenAll( tasks );
+ }*/
+
+ public async Task SendInternalAsync(
+ Domain.Agent agent,
+ IPeerMessage message,
+ CancellationToken cancellationToken = default
+ ) {
+ var connection = peerStreamManager.GetOrCreate( new Uri( agent.Address ), "agentid_local1" );
+ var envelope = envelopeConverter.ToEnvelope( message );
+ await connection.SendAsync( envelope );
+ }
+
+ public async Task SendAndWaitAsync(
+ Domain.Agent agent,
+ IPeerMessage message,
+ TimeSpan? timeout = null,
+ CancellationToken cancellationToken = default
+ ) where TResponse : IPeerMessage {
+ var correlationId = Guid.NewGuid().ToString();
+ var envelope = envelopeConverter.ToEnvelope( message );
+ envelope.CorrelationId = correlationId;
+
+ // Register correlator BEFORE sending
+ var responseTask = responseCorrelator.WaitForResponseAsync(
+ correlationId,
+ timeout ?? TimeSpan.FromSeconds( 30 ),
+ cancellationToken
+ );
+
+ // Request
+ var connection = peerStreamManager.GetOrCreate( new Uri( agent.Address ), "agentid_local1" );
+ await connection.SendAsync( envelope );
+
+ // Response
+ var response = await responseTask;
+ return envelopeConverter.FromEnvelope( response );
+ }
+}
\ No newline at end of file
diff --git a/src/Networking.Clustering/Enrollment.cs b/src/Networking.Clustering/Enrollment.cs
new file mode 100644
index 00000000..92143071
--- /dev/null
+++ b/src/Networking.Clustering/Enrollment.cs
@@ -0,0 +1,10 @@
+namespace Drift.Networking.Clustering;
+
+public class EnrollmentRequest( bool parametersAdoptable, string? parametersJoin ) {
+ public EnrollmentMethod Method => parametersAdoptable ? EnrollmentMethod.Adoption : EnrollmentMethod.Jwt;
+}
+
+public enum EnrollmentMethod {
+ Adoption,
+ Jwt
+}
\ No newline at end of file
diff --git a/src/Networking.Clustering/ICluster.cs b/src/Networking.Clustering/ICluster.cs
new file mode 100644
index 00000000..b3527f9c
--- /dev/null
+++ b/src/Networking.Clustering/ICluster.cs
@@ -0,0 +1,19 @@
+using Drift.Networking.PeerStreaming.Core.Abstractions;
+
+namespace Drift.Networking.Clustering;
+
+public interface ICluster {
+ Task SendAsync( Domain.Agent agent, IPeerMessage message, CancellationToken cancellationToken = default );
+
+ Task SendAndWaitAsync(
+ Domain.Agent agent,
+ IPeerMessage message,
+ TimeSpan? timeout = null,
+ CancellationToken cancellationToken = default
+ ) where TResponse : IPeerMessage;
+
+ /*Task BroadcastAsync( PeerMessage message, CancellationToken cancellationToken = default );
+ Task> RequestSubnetsAsync( string peerAddress, CancellationToken cancellationToken = default );
+ Task EnsureConnectedAsync( string peerAddress, CancellationToken cancellationToken = default );
+ IReadOnlyCollection GetConnectedPeers();*/
+}
\ No newline at end of file
diff --git a/src/Networking.Clustering/Networking.Clustering.csproj b/src/Networking.Clustering/Networking.Clustering.csproj
new file mode 100644
index 00000000..9c366ae1
--- /dev/null
+++ b/src/Networking.Clustering/Networking.Clustering.csproj
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/src/Networking.Clustering/ServiceCollectionExtensions.cs b/src/Networking.Clustering/ServiceCollectionExtensions.cs
new file mode 100644
index 00000000..183cb7e0
--- /dev/null
+++ b/src/Networking.Clustering/ServiceCollectionExtensions.cs
@@ -0,0 +1,9 @@
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Drift.Networking.Clustering;
+
+public static class ServiceCollectionExtensions {
+ public static void AddClustering( this IServiceCollection services ) {
+ services.AddScoped();
+ }
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Client/DefaultPeerClientFactory.cs b/src/Networking.PeerStreaming.Client/DefaultPeerClientFactory.cs
new file mode 100644
index 00000000..85cbf055
--- /dev/null
+++ b/src/Networking.PeerStreaming.Client/DefaultPeerClientFactory.cs
@@ -0,0 +1,13 @@
+using Drift.Networking.Grpc.Generated;
+using Drift.Networking.PeerStreaming.Core.Abstractions;
+using Grpc.Net.Client;
+
+namespace Drift.Networking.PeerStreaming.Client;
+
+internal sealed class DefaultPeerClientFactory : IPeerClientFactory {
+ public (PeerService.PeerServiceClient Client, GrpcChannel Channel) Create( Uri address ) {
+ var channel = GrpcChannel.ForAddress( address, new GrpcChannelOptions() );
+ var client = new PeerService.PeerServiceClient( channel );
+ return ( client, channel );
+ }
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Client/Networking.PeerStreaming.Client.csproj b/src/Networking.PeerStreaming.Client/Networking.PeerStreaming.Client.csproj
new file mode 100644
index 00000000..e6584c54
--- /dev/null
+++ b/src/Networking.PeerStreaming.Client/Networking.PeerStreaming.Client.csproj
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Networking.PeerStreaming.Client/ServiceCollectionExtensions.cs b/src/Networking.PeerStreaming.Client/ServiceCollectionExtensions.cs
new file mode 100644
index 00000000..81cf54a9
--- /dev/null
+++ b/src/Networking.PeerStreaming.Client/ServiceCollectionExtensions.cs
@@ -0,0 +1,10 @@
+using Drift.Networking.PeerStreaming.Core.Abstractions;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Drift.Networking.PeerStreaming.Client;
+
+public static class ServiceCollectionExtensions {
+ public static void AddPeerStreamingClient( this IServiceCollection services ) {
+ services.AddSingleton();
+ }
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core.Abstractions/ConnectionSide.cs b/src/Networking.PeerStreaming.Core.Abstractions/ConnectionSide.cs
new file mode 100644
index 00000000..d516ef7b
--- /dev/null
+++ b/src/Networking.PeerStreaming.Core.Abstractions/ConnectionSide.cs
@@ -0,0 +1,6 @@
+namespace Drift.Networking.PeerStreaming.Core.Abstractions;
+
+public enum ConnectionSide {
+ Incoming,
+ Outgoing
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core.Abstractions/IPeerClientFactory.cs b/src/Networking.PeerStreaming.Core.Abstractions/IPeerClientFactory.cs
new file mode 100644
index 00000000..f7a92656
--- /dev/null
+++ b/src/Networking.PeerStreaming.Core.Abstractions/IPeerClientFactory.cs
@@ -0,0 +1,8 @@
+using Drift.Networking.Grpc.Generated;
+using Grpc.Net.Client;
+
+namespace Drift.Networking.PeerStreaming.Core.Abstractions;
+
+public interface IPeerClientFactory {
+ (PeerService.PeerServiceClient Client, GrpcChannel Channel) Create( Uri address );
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessage.cs b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessage.cs
new file mode 100644
index 00000000..6ea62d0b
--- /dev/null
+++ b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessage.cs
@@ -0,0 +1,7 @@
+namespace Drift.Networking.PeerStreaming.Core.Abstractions;
+
+public interface IPeerMessage {
+ string MessageType {
+ get;
+ }
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageEnvelopeConverter.cs b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageEnvelopeConverter.cs
new file mode 100644
index 00000000..49b546fe
--- /dev/null
+++ b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageEnvelopeConverter.cs
@@ -0,0 +1,9 @@
+using Drift.Networking.Grpc.Generated;
+
+namespace Drift.Networking.PeerStreaming.Core.Abstractions;
+
+public interface IPeerMessageEnvelopeConverter {
+ public PeerMessage ToEnvelope( IPeerMessage message, string? requestId = null );
+
+ public T FromEnvelope( PeerMessage envelope ) where T : IPeerMessage;
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageHandler.cs b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageHandler.cs
new file mode 100644
index 00000000..b4f6fd5e
--- /dev/null
+++ b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageHandler.cs
@@ -0,0 +1,28 @@
+using Drift.Networking.Grpc.Generated;
+
+namespace Drift.Networking.PeerStreaming.Core.Abstractions;
+
+public interface IPeerMessageHandler {
+ string MessageType {
+ get;
+ }
+
+ Task HandleAsync(
+ PeerMessage envelope,
+ IPeerMessageEnvelopeConverter envelopeConverter,
+ CancellationToken cancellationToken = default
+ );
+}
+
+public interface IPeerMessageHandler : IPeerMessageHandler where T : IPeerMessage {
+ async Task IPeerMessageHandler.HandleAsync(
+ PeerMessage envelope,
+ IPeerMessageEnvelopeConverter envelopeConverter,
+ CancellationToken cancellationToken
+ ) {
+ var typedMessage = envelopeConverter.FromEnvelope( envelope );
+ return await HandleAsync( typedMessage, cancellationToken );
+ }
+
+ Task HandleAsync( T message, CancellationToken cancellationToken = default );
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core.Abstractions/IPeerStream.cs b/src/Networking.PeerStreaming.Core.Abstractions/IPeerStream.cs
new file mode 100644
index 00000000..6d34c9b8
--- /dev/null
+++ b/src/Networking.PeerStreaming.Core.Abstractions/IPeerStream.cs
@@ -0,0 +1,20 @@
+using Drift.Domain;
+using Drift.Networking.Grpc.Generated;
+
+namespace Drift.Networking.PeerStreaming.Core.Abstractions;
+
+public interface IPeerStream : IAsyncDisposable {
+ public int InstanceNo {
+ get;
+ }
+
+ public AgentId AgentId {
+ get;
+ }
+
+ public Task ReadTask {
+ get;
+ }
+
+ public Task SendAsync( PeerMessage message );
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core.Abstractions/IPeerStreamManager.cs b/src/Networking.PeerStreaming.Core.Abstractions/IPeerStreamManager.cs
new file mode 100644
index 00000000..e115a60a
--- /dev/null
+++ b/src/Networking.PeerStreaming.Core.Abstractions/IPeerStreamManager.cs
@@ -0,0 +1,15 @@
+using Drift.Domain;
+using Drift.Networking.Grpc.Generated;
+using Grpc.Core;
+
+namespace Drift.Networking.PeerStreaming.Core.Abstractions;
+
+public interface IPeerStreamManager {
+ public IPeerStream GetOrCreate( Uri peerAddress, AgentId id );
+
+ public IPeerStream Create(
+ IAsyncStreamReader requestStream,
+ IAsyncStreamWriter responseStream,
+ ServerCallContext context
+ );
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core.Abstractions/Networking.PeerStreaming.Core.Abstractions.csproj b/src/Networking.PeerStreaming.Core.Abstractions/Networking.PeerStreaming.Core.Abstractions.csproj
new file mode 100644
index 00000000..97a14ed6
--- /dev/null
+++ b/src/Networking.PeerStreaming.Core.Abstractions/Networking.PeerStreaming.Core.Abstractions.csproj
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Networking.PeerStreaming.Core/Common/GrpcMetadataExtensions.cs b/src/Networking.PeerStreaming.Core/Common/GrpcMetadataExtensions.cs
new file mode 100644
index 00000000..77717661
--- /dev/null
+++ b/src/Networking.PeerStreaming.Core/Common/GrpcMetadataExtensions.cs
@@ -0,0 +1,16 @@
+using Drift.Domain;
+using Grpc.Core;
+
+namespace Drift.Networking.PeerStreaming.Core.Common;
+
+internal static class GrpcMetadataExtensions {
+ internal static AgentId GetAgentId( this Metadata metadata ) {
+ var v = metadata.Get( "agent-id" );
+
+ if ( v == null ) {
+ throw new Exception( "AgentId not found in gRPC metadata" );
+ }
+
+ return new AgentId( v.Value );
+ }
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core/Messages/PeerMessageDispatcher.cs b/src/Networking.PeerStreaming.Core/Messages/PeerMessageDispatcher.cs
new file mode 100644
index 00000000..b75c4632
--- /dev/null
+++ b/src/Networking.PeerStreaming.Core/Messages/PeerMessageDispatcher.cs
@@ -0,0 +1,55 @@
+using Drift.Networking.Grpc.Generated;
+using Drift.Networking.PeerStreaming.Core.Abstractions;
+using Microsoft.Extensions.Logging;
+
+namespace Drift.Networking.PeerStreaming.Core.Messages;
+
+public sealed class PeerMessageDispatcher {
+ private readonly PeerResponseCorrelator _responseCorrelator;
+ private readonly IPeerMessageEnvelopeConverter _envelopeConverter;
+ private readonly ILogger _logger;
+ private readonly Dictionary _handlers;
+
+ public PeerMessageDispatcher(
+ IEnumerable handlers,
+ IPeerMessageEnvelopeConverter envelopeConverter,
+ PeerResponseCorrelator responseCorrelator,
+ ILogger logger
+ ) {
+ _responseCorrelator = responseCorrelator;
+ _envelopeConverter = envelopeConverter;
+ _logger = logger;
+ _handlers = handlers.ToDictionary( h => h.MessageType, StringComparer.OrdinalIgnoreCase );
+ }
+
+ public async Task DispatchAsync( PeerMessage message, PeerStream peerStream, CancellationToken ct = default ) {
+ _logger.LogDebug( "Dispatching message: {Type}", message.MessageType );
+
+ // If this is a response to a pending request, complete it
+ if ( !string.IsNullOrEmpty( message.ReplyTo ) ) {
+ if ( _responseCorrelator.TryCompleteResponse( message.ReplyTo, message ) ) {
+ _logger.LogDebug( "Completed pending request: {CorrelationId}", message.ReplyTo );
+ }
+ else {
+ _logger.LogWarning( "Ignoring response for unknown correlation ID: {CorrelationId}", message.ReplyTo );
+ }
+
+ return;
+ }
+
+ // Otherwise, dispatch to handler
+ if ( _handlers.TryGetValue( message.MessageType, out var handler ) ) {
+ var response = await handler.HandleAsync( message, _envelopeConverter, ct );
+
+ if ( response != null ) {
+ var responseEnvelope = _envelopeConverter.ToEnvelope( response );
+ responseEnvelope.ReplyTo = message.CorrelationId;
+ await peerStream.SendAsync( responseEnvelope );
+ }
+
+ return;
+ }
+
+ throw new NotImplementedException( "Unknown message type '" + message.MessageType + "'" );
+ }
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core/Messages/PeerMessageEnvelopeConverter.cs b/src/Networking.PeerStreaming.Core/Messages/PeerMessageEnvelopeConverter.cs
new file mode 100644
index 00000000..b87284ae
--- /dev/null
+++ b/src/Networking.PeerStreaming.Core/Messages/PeerMessageEnvelopeConverter.cs
@@ -0,0 +1,48 @@
+using System.Text.Json;
+using Drift.EnvironmentConfig.Converters;
+using Drift.Networking.Grpc.Generated;
+using Drift.Networking.PeerStreaming.Core.Abstractions;
+
+namespace Drift.Networking.PeerStreaming.Core.Messages;
+
+internal sealed class PeerMessageEnvelopeConverter : IPeerMessageEnvelopeConverter {
+ private readonly Dictionary _typeMap = new();
+ private readonly JsonSerializerOptions _serializerOptions;
+
+ public PeerMessageEnvelopeConverter( IPeerMessageTypesProvider provider ) : this( provider.Get() ) {
+ }
+
+ public PeerMessageEnvelopeConverter( params Type[] messageTypes ) : this( (IEnumerable) messageTypes ) {
+ }
+
+ private PeerMessageEnvelopeConverter( IEnumerable messageTypes ) {
+ foreach ( var type in messageTypes ) {
+ // TODO improve
+ var instance = (IPeerMessage) Activator.CreateInstance( type )!;
+ _typeMap[instance.MessageType] = type;
+ }
+
+ _serializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
+ _serializerOptions.Converters.Add( new IpAddressConverter() );
+ _serializerOptions.Converters.Add( new CidrBlockConverter() );
+ }
+
+ public PeerMessage ToEnvelope( IPeerMessage message, string? requestId = null ) {
+ var json = JsonSerializer.Serialize( message, message.GetType(), _serializerOptions );
+ return new PeerMessage { MessageType = message.MessageType, Message = json, };
+ }
+
+ public T FromEnvelope( PeerMessage envelope ) where T : IPeerMessage {
+ if ( !_typeMap.TryGetValue( envelope.MessageType, out var type ) ) {
+ throw new InvalidOperationException( $"Unknown message type: {envelope.MessageType}" );
+ }
+
+ if ( type != typeof(T) ) {
+ throw new InvalidOperationException(
+ $"Message type mismatch: expected {typeof(T).Name}, got {envelope.MessageType}"
+ );
+ }
+
+ return (T) JsonSerializer.Deserialize( envelope.Message, type, _serializerOptions )!;
+ }
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core/Messages/PeerMessageTypesProvider.cs b/src/Networking.PeerStreaming.Core/Messages/PeerMessageTypesProvider.cs
new file mode 100644
index 00000000..c5d239b8
--- /dev/null
+++ b/src/Networking.PeerStreaming.Core/Messages/PeerMessageTypesProvider.cs
@@ -0,0 +1,23 @@
+using System.Reflection;
+using Drift.Networking.PeerStreaming.Core.Abstractions;
+
+namespace Drift.Networking.PeerStreaming.Core.Messages;
+
+public interface IPeerMessageTypesProvider {
+ IEnumerable Get();
+}
+
+public class AssemblyScanPeerMessageTypesProvider( params Assembly[] assemblies ) : IPeerMessageTypesProvider {
+ public IEnumerable Get() {
+ var types = assemblies
+ .SelectMany( a => a.GetTypes() )
+ .Where( t => typeof(IPeerMessage).IsAssignableFrom( t ) && !t.IsAbstract && !t.IsInterface );
+
+ if ( types.Count() == 0 ) {
+ throw new InvalidOperationException(
+ $"No types implementing {nameof(IPeerMessage)} found in assemblies: {string.Join( ", ", assemblies.Select( a => a.GetName().Name ) )}" );
+ }
+
+ return types;
+ }
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core/Messages/PeerResponseCorrelator.cs b/src/Networking.PeerStreaming.Core/Messages/PeerResponseCorrelator.cs
new file mode 100644
index 00000000..760634d3
--- /dev/null
+++ b/src/Networking.PeerStreaming.Core/Messages/PeerResponseCorrelator.cs
@@ -0,0 +1,43 @@
+using System.Collections.Concurrent;
+using Drift.Networking.Grpc.Generated;
+using Microsoft.Extensions.Logging;
+
+namespace Drift.Networking.PeerStreaming.Core.Messages;
+
+//TODO private?
+public sealed class PeerResponseCorrelator {
+ private readonly ConcurrentDictionary> _pendingRequests = new();
+ private readonly ILogger _logger;
+
+ public PeerResponseCorrelator( ILogger logger ) {
+ _logger = logger;
+ }
+
+ public Task WaitForResponseAsync( string correlationId, TimeSpan timeout, CancellationToken ct ) {
+ var tcs = new TaskCompletionSource();
+
+ if ( !_pendingRequests.TryAdd( correlationId, tcs ) ) {
+ throw new InvalidOperationException( $"Correlation ID {correlationId} already exists" );
+ }
+
+ var cts = CancellationTokenSource.CreateLinkedTokenSource( ct );
+ cts.CancelAfter( timeout );
+
+ cts.Token.Register( () => {
+ if ( _pendingRequests.TryRemove( correlationId, out var removed ) ) {
+ removed.TrySetCanceled();
+ }
+ } );
+
+ return tcs.Task;
+ }
+
+ public bool TryCompleteResponse( string correlationId, PeerMessage response ) {
+ if ( _pendingRequests.TryRemove( correlationId, out var tcs ) ) {
+ return tcs.TrySetResult( response );
+ }
+
+ _logger.LogWarning( "Received response for unknown correlation ID: {CorrelationId}", correlationId );
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core/Networking.PeerStreaming.Core.csproj b/src/Networking.PeerStreaming.Core/Networking.PeerStreaming.Core.csproj
new file mode 100644
index 00000000..2864ec36
--- /dev/null
+++ b/src/Networking.PeerStreaming.Core/Networking.PeerStreaming.Core.csproj
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Networking.PeerStreaming.Core/PeerStream.cs b/src/Networking.PeerStreaming.Core/PeerStream.cs
new file mode 100644
index 00000000..ab26c1ac
--- /dev/null
+++ b/src/Networking.PeerStreaming.Core/PeerStream.cs
@@ -0,0 +1,115 @@
+using Drift.Domain;
+using Drift.Networking.Grpc.Generated;
+using Drift.Networking.PeerStreaming.Core.Abstractions;
+using Drift.Networking.PeerStreaming.Core.Messages;
+using Grpc.Core;
+using Microsoft.Extensions.Logging;
+
+namespace Drift.Networking.PeerStreaming.Core;
+
+public sealed class PeerStream : IPeerStream {
+ private static int _instanceCounter;
+ private readonly IAsyncStreamReader _reader;
+ private readonly IAsyncStreamWriter _writer;
+ private readonly PeerMessageDispatcher _dispatcher;
+ private readonly ILogger _logger;
+ private readonly CancellationToken _cancellationToken;
+
+ public int InstanceNo {
+ get;
+ } = Interlocked.Increment( ref _instanceCounter );
+
+ private ConnectionSide Side {
+ get;
+ }
+
+ private Uri? Address {
+ get;
+ }
+
+ public required AgentId AgentId {
+ get;
+ init;
+ }
+
+ public Task ReadTask {
+ get;
+ private init;
+ }
+
+ public PeerStream(
+ IAsyncStreamReader reader,
+ IAsyncStreamWriter writer,
+ PeerMessageDispatcher dispatcher,
+ ILogger logger,
+ CancellationToken cancellationToken
+ ) {
+ Side = ConnectionSide.Incoming;
+ _reader = reader;
+ _writer = writer;
+ _dispatcher = dispatcher;
+ _logger = logger;
+ _cancellationToken = cancellationToken;
+ // The read loop is considering the cancellation token to ensure a clean shutdown. Don't pass it to the task.
+ ReadTask = Task.Run( ReadLoopAsync, CancellationToken.None );
+ }
+
+ public PeerStream(
+ Uri address,
+ IAsyncStreamReader reader,
+ IAsyncStreamWriter writer,
+ PeerMessageDispatcher dispatcher,
+ ILogger logger,
+ CancellationToken cancellationToken
+ ) : this( reader, writer, dispatcher, logger, cancellationToken ) {
+ Side = ConnectionSide.Outgoing;
+ Address = address;
+ }
+
+ public async Task SendAsync( PeerMessage message ) {
+ await _writer.WriteAsync( message, _cancellationToken ); //TODO also take another cancellation token (combine)
+ }
+
+ private async Task ReadLoopAsync() {
+ _logger.LogDebug( "Read loop starting..." );
+
+ try {
+ await foreach ( var message in _reader.ReadAllAsync( _cancellationToken ) ) {
+ try {
+ _logger.LogDebug( "Received message. Dispatching to handler..." );
+ await _dispatcher.DispatchAsync( message, this, CancellationToken.None );
+ }
+ catch ( Exception ex ) {
+ _logger.LogError( ex, "Message dispatch failed" );
+ }
+ }
+
+ _logger.LogDebug( "Read loop ended gracefully (end of stream)" );
+ }
+ catch ( OperationCanceledException ) {
+ // Justification: exception is control flow, not an error
+#pragma warning disable S6667
+ _logger.LogDebug( "Read loop ended gracefully (cancelled)" );
+#pragma warning restore S6667
+ }
+ catch ( Exception ex ) {
+ _logger.LogError( ex, "Read loop failed" );
+ }
+ }
+
+ public async ValueTask DisposeAsync() {
+ Console.WriteLine( "Disposing " + this );
+
+ /*if ( _call != null ) {
+ // I.e., outgoing stream (client initiated)
+ await _call.RequestStream.CompleteAsync();
+ }*/
+
+ await ReadTask;
+ }
+
+ public override string ToString() {
+ return
+ $"{nameof(PeerStream)}[#{InstanceNo}, {nameof(AgentId)}={AgentId}, {nameof(Side)}={Side}, {nameof(Address)}={Address?.ToString() ?? "n/a"}]";
+ }
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core/PeerStreamManager.cs b/src/Networking.PeerStreaming.Core/PeerStreamManager.cs
new file mode 100644
index 00000000..3fc9e96e
--- /dev/null
+++ b/src/Networking.PeerStreaming.Core/PeerStreamManager.cs
@@ -0,0 +1,71 @@
+using System.Collections.Concurrent;
+using Drift.Domain;
+using Drift.Networking.Grpc.Generated;
+using Drift.Networking.PeerStreaming.Core.Abstractions;
+using Drift.Networking.PeerStreaming.Core.Common;
+using Drift.Networking.PeerStreaming.Core.Messages;
+using Grpc.Core;
+using Microsoft.Extensions.Logging;
+
+namespace Drift.Networking.PeerStreaming.Core;
+
+internal sealed class PeerStreamManager(
+ ILogger logger,
+ IPeerClientFactory? peerClientFactory,
+ PeerMessageDispatcher dispatcher,
+ PeerStreamingOptions options
+) : IPeerStreamManager {
+ private readonly ConcurrentDictionary _streams = new();
+
+ public IPeerStream GetOrCreate( Uri peerAddress, AgentId id ) {
+ logger.LogDebug(
+ "Getting or creating {ConnectionSide} stream to agent {Id} ({Address})",
+ ConnectionSide.Outgoing,
+ id,
+ peerAddress
+ );
+
+ return _streams.GetOrAdd( id, agentId => Create( peerAddress, agentId ) );
+ }
+
+ private IPeerStream Create( Uri peerAddress, AgentId id ) {
+ if ( peerClientFactory == null ) {
+ throw new Exception( $"Cannot create outbound stream since {nameof(peerClientFactory)} is null" );
+ }
+
+ var (client, _) = peerClientFactory.Create( peerAddress );
+ var callOptions = new CallOptions( new Metadata { { "agent-id", id } } );
+ var call = client.PeerStream( callOptions );
+
+ var stream = new PeerStream(
+ peerAddress,
+ call.ResponseStream,
+ call.RequestStream,
+ dispatcher,
+ logger,
+ options.StoppingToken
+ ) { AgentId = id };
+ Add( stream );
+ return stream;
+ }
+
+ public IPeerStream Create(
+ IAsyncStreamReader requestStream,
+ IAsyncStreamWriter responseStream,
+ ServerCallContext context
+ ) {
+ var agentId = context.RequestHeaders.GetAgentId();
+
+ logger.LogInformation( "Creating {ConnectionSide} stream from agent {Id}", ConnectionSide.Incoming, agentId );
+
+ var stream =
+ new PeerStream( requestStream, responseStream, dispatcher, logger, options.StoppingToken ) { AgentId = agentId };
+ Add( stream );
+ return stream;
+ }
+
+ private void Add( IPeerStream stream ) {
+ logger.LogTrace( "Created {Stream}", stream );
+ _streams[stream.AgentId] = stream;
+ }
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core/PeerStreamingOptions.cs b/src/Networking.PeerStreaming.Core/PeerStreamingOptions.cs
new file mode 100644
index 00000000..2ca2a36e
--- /dev/null
+++ b/src/Networking.PeerStreaming.Core/PeerStreamingOptions.cs
@@ -0,0 +1,15 @@
+using System.Reflection;
+
+namespace Drift.Networking.PeerStreaming.Core;
+
+public sealed class PeerStreamingOptions {
+ public CancellationToken StoppingToken {
+ get;
+ set;
+ }
+
+ public Assembly MessageAssembly {
+ get;
+ init;
+ } = Assembly.GetExecutingAssembly();
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core/ServiceCollectionExtensions.cs b/src/Networking.PeerStreaming.Core/ServiceCollectionExtensions.cs
new file mode 100644
index 00000000..06993192
--- /dev/null
+++ b/src/Networking.PeerStreaming.Core/ServiceCollectionExtensions.cs
@@ -0,0 +1,22 @@
+using Drift.Networking.PeerStreaming.Core.Abstractions;
+using Drift.Networking.PeerStreaming.Core.Messages;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Drift.Networking.PeerStreaming.Core;
+
+public static class ServiceCollectionExtensions {
+ public static void AddPeerStreamingCore(
+ this IServiceCollection services,
+ PeerStreamingOptions options
+ ) {
+ services.AddSingleton( options );
+ services.AddSingleton(
+ new PeerMessageEnvelopeConverter(
+ new AssemblyScanPeerMessageTypesProvider( options.MessageAssembly )
+ )
+ );
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ }
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Grpc/Networking.PeerStreaming.Grpc.csproj b/src/Networking.PeerStreaming.Grpc/Networking.PeerStreaming.Grpc.csproj
new file mode 100644
index 00000000..c510d63c
--- /dev/null
+++ b/src/Networking.PeerStreaming.Grpc/Networking.PeerStreaming.Grpc.csproj
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+ all
+
+
+
+
diff --git a/src/Networking.PeerStreaming.Grpc/Protos/peer.proto b/src/Networking.PeerStreaming.Grpc/Protos/peer.proto
new file mode 100644
index 00000000..c0fccd55
--- /dev/null
+++ b/src/Networking.PeerStreaming.Grpc/Protos/peer.proto
@@ -0,0 +1,17 @@
+syntax = "proto3";
+
+option csharp_namespace = "Drift.Networking.Grpc.Generated";
+
+package peer;
+
+// AgentService?
+service PeerService {
+ rpc PeerStream (stream PeerMessage) returns (stream PeerMessage);
+}
+
+message PeerMessage {
+ string message_type = 1;
+ string message = 2;
+ string correlation_id = 3; // For request-response matching
+ string reply_to = 4; // If this is a response, which request it replies to
+}
diff --git a/src/Networking.PeerStreaming.Server/InboundPeerService.cs b/src/Networking.PeerStreaming.Server/InboundPeerService.cs
new file mode 100644
index 00000000..a018d098
--- /dev/null
+++ b/src/Networking.PeerStreaming.Server/InboundPeerService.cs
@@ -0,0 +1,31 @@
+using Drift.Networking.Grpc.Generated;
+using Drift.Networking.PeerStreaming.Core.Abstractions;
+using Grpc.Core;
+using Microsoft.Extensions.Logging;
+
+namespace Drift.Networking.PeerStreaming.Server;
+
+// Handle incoming connections (AKA server-side).
+internal sealed class InboundPeerService( IPeerStreamManager peerStreamManager, ILogger logger )
+ : PeerService.PeerServiceBase {
+ public override async Task PeerStream(
+ IAsyncStreamReader requestStream,
+ IServerStreamWriter responseStream,
+ ServerCallContext context
+ ) {
+ try {
+ logger.LogInformation( "Inbound stream started" );
+ var stream = peerStreamManager.Create( requestStream, responseStream, context );
+ logger.LogInformation( "Peer stream #{StreamNo} created", stream.InstanceNo );
+
+ // The stream is closed when the method returns.
+ // We thus wait for the read loop to complete (meaning that this client is no longer interested in the stream).
+ await stream.ReadTask;
+
+ logger.LogInformation( "Peer stream #{StreamNo} completed", stream.InstanceNo );
+ }
+ catch ( Exception ex ) {
+ logger.LogError( ex, "Inbound stream failed" );
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Server/Networking.PeerStreaming.Server.csproj b/src/Networking.PeerStreaming.Server/Networking.PeerStreaming.Server.csproj
new file mode 100644
index 00000000..f0d05b8c
--- /dev/null
+++ b/src/Networking.PeerStreaming.Server/Networking.PeerStreaming.Server.csproj
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Networking.PeerStreaming.Server/PeerStreamingServerMarker.cs b/src/Networking.PeerStreaming.Server/PeerStreamingServerMarker.cs
new file mode 100644
index 00000000..2b09ab2f
--- /dev/null
+++ b/src/Networking.PeerStreaming.Server/PeerStreamingServerMarker.cs
@@ -0,0 +1,8 @@
+namespace Drift.Networking.PeerStreaming.Server;
+
+internal sealed class PeerStreamingServerMarker {
+ internal bool EndpointsMapped {
+ get;
+ set;
+ }
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Server/ServiceCollectionExtensions.cs b/src/Networking.PeerStreaming.Server/ServiceCollectionExtensions.cs
new file mode 100644
index 00000000..79843ab4
--- /dev/null
+++ b/src/Networking.PeerStreaming.Server/ServiceCollectionExtensions.cs
@@ -0,0 +1,47 @@
+using Grpc.AspNetCore.Server;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Drift.Networking.PeerStreaming.Server;
+
+public static class ServiceCollectionExtensions {
+ public static void AddPeerStreamingServer(
+ this IServiceCollection services,
+ Action? configureOptions = null
+ ) {
+ services.AddSingleton();
+ services.AddGrpc( options => configureOptions?.Invoke( options ) );
+ services.AddTransient();
+ }
+
+ public static void MapPeerStreamingServerEndpoints( this IEndpointRouteBuilder app ) {
+ var marker = app.ServiceProvider.GetService();
+
+ if ( marker == null ) {
+ throw new InvalidOperationException(
+ $"Unable to find the required services. Add them by calling '{nameof(IServiceCollection)}.{nameof(AddPeerStreamingServer)}'."
+ );
+ }
+
+ app.MapGrpcService();
+
+ marker.EndpointsMapped = true;
+ }
+}
+
+internal sealed class PeerStreamingServerValidationFilter : IStartupFilter {
+ public Action Configure( Action next ) {
+ return app => {
+ next( app );
+
+ var marker = app.ApplicationServices.GetRequiredService();
+
+ if ( !marker.EndpointsMapped ) {
+ throw new InvalidOperationException(
+ $"Server endpoints were not mapped. Map them by calling '{nameof(IEndpointRouteBuilder)}.{nameof(ServiceCollectionExtensions.MapPeerStreamingServerEndpoints)}'." );
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Tests/AssemblyInfo.cs b/src/Networking.PeerStreaming.Tests/AssemblyInfo.cs
new file mode 100644
index 00000000..5493e66a
--- /dev/null
+++ b/src/Networking.PeerStreaming.Tests/AssemblyInfo.cs
@@ -0,0 +1 @@
+[assembly: Category( "Unit" )]
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Tests/Helpers/InMemoryDuplexStreamPair.cs b/src/Networking.PeerStreaming.Tests/Helpers/InMemoryDuplexStreamPair.cs
new file mode 100644
index 00000000..fccd08ca
--- /dev/null
+++ b/src/Networking.PeerStreaming.Tests/Helpers/InMemoryDuplexStreamPair.cs
@@ -0,0 +1,121 @@
+using System.Threading.Channels;
+using Grpc.Core;
+using Channel = System.Threading.Channels.Channel;
+
+namespace Drift.Networking.PeerStreaming.Tests.Helpers;
+
+internal sealed record DuplexStreamEndpoint( TRequest RequestStream, TResponse ResponseStream );
+
+///
+/// Provides an in-memory bidirectional gRPC stream pair (one endpoint is the client, the other is the server).
+///
+internal static class InMemoryDuplexStreamPair {
+ public static (
+ DuplexStreamEndpoint, IAsyncStreamReader> Client,
+ DuplexStreamEndpoint, IServerStreamWriter> Server
+ )
+ Create( ServerCallContext serverContext ) where TRequest : class where TResponse : class {
+ var clientToServer = Channel.CreateUnbounded();
+ var serverToClient = Channel.CreateUnbounded();
+
+ var server = new DuplexStreamEndpoint, IServerStreamWriter>(
+ new InMemoryServerStreamReader( clientToServer.Reader, serverContext ),
+ new InMemoryServerStreamWriter( serverToClient.Writer, serverContext )
+ );
+
+ var client = new DuplexStreamEndpoint, IAsyncStreamReader>(
+ new InMemoryStreamWriter( clientToServer.Writer ),
+ new InMemoryStreamReader( serverToClient.Reader )
+ );
+
+ return ( client, server );
+ }
+
+ private sealed class InMemoryStreamWriter : IClientStreamWriter where T : class {
+ private readonly ChannelWriter _writer;
+
+ public WriteOptions? WriteOptions {
+ get;
+ set;
+ }
+
+ internal InMemoryStreamWriter( ChannelWriter writer ) {
+ _writer = writer;
+ }
+
+ public async Task WriteAsync( T message ) {
+ await _writer.WriteAsync( message );
+ }
+
+ public Task CompleteAsync() {
+ _writer.Complete();
+ return Task.CompletedTask;
+ }
+ }
+
+ private sealed class InMemoryServerStreamReader : InMemoryStreamReader where T : class {
+ private readonly ServerCallContext _context;
+
+ internal InMemoryServerStreamReader( ChannelReader reader, ServerCallContext context ) : base( reader ) {
+ _context = context;
+ }
+
+ public override Task MoveNext( CancellationToken cancellationToken ) {
+ _context.CancellationToken.ThrowIfCancellationRequested();
+ return base.MoveNext( cancellationToken );
+ }
+ }
+
+ private class InMemoryStreamReader : IAsyncStreamReader where T : class {
+ private readonly ChannelReader _reader;
+
+ public T Current {
+ get;
+ private set;
+ } = null!;
+
+ internal InMemoryStreamReader( ChannelReader reader ) {
+ _reader = reader;
+ }
+
+ public virtual async Task MoveNext( CancellationToken cancellationToken ) {
+ if ( await _reader.WaitToReadAsync( cancellationToken ) && _reader.TryRead( out var message ) ) {
+ Current = message;
+ return true;
+ }
+
+ Current = null!;
+ return false;
+ }
+ }
+
+ private sealed class InMemoryServerStreamWriter : IServerStreamWriter where T : class {
+ private readonly ChannelWriter _writer;
+ private readonly ServerCallContext _context;
+
+ public WriteOptions? WriteOptions {
+ get {
+ return new WriteOptions();
+ }
+
+ set {
+ throw new NotSupportedException();
+ }
+ }
+
+ internal InMemoryServerStreamWriter( ChannelWriter writer, ServerCallContext context ) {
+ _writer = writer;
+ _context = context;
+ }
+
+ public Task WriteAsync( T message ) {
+ _context.CancellationToken.ThrowIfCancellationRequested();
+
+ if ( !_writer.TryWrite( message ) ) {
+ throw new InvalidOperationException( "Unable to write message." );
+ }
+
+ return Task.CompletedTask;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Tests/Helpers/TestServerCallContext.cs b/src/Networking.PeerStreaming.Tests/Helpers/TestServerCallContext.cs
new file mode 100644
index 00000000..f4c71991
--- /dev/null
+++ b/src/Networking.PeerStreaming.Tests/Helpers/TestServerCallContext.cs
@@ -0,0 +1,77 @@
+using Grpc.Core;
+
+namespace Drift.Networking.PeerStreaming.Tests.Helpers;
+
+internal sealed class TestServerCallContext : ServerCallContext {
+ private readonly Metadata _requestHeaders;
+ private readonly CancellationToken _cancellationToken;
+ private readonly Metadata _responseTrailers;
+ private readonly AuthContext _authContext;
+ private readonly Dictionary
diff --git a/src/EnvironmentConfig/Converters/CidrBlockConverter.cs b/src/Serialization/Converters/CidrBlockConverter.cs
similarity index 93%
rename from src/EnvironmentConfig/Converters/CidrBlockConverter.cs
rename to src/Serialization/Converters/CidrBlockConverter.cs
index 009fbab8..cfcf45fa 100644
--- a/src/EnvironmentConfig/Converters/CidrBlockConverter.cs
+++ b/src/Serialization/Converters/CidrBlockConverter.cs
@@ -2,7 +2,7 @@
using System.Text.Json.Serialization;
using Drift.Domain;
-namespace Drift.EnvironmentConfig.Converters;
+namespace Drift.Serialization.Converters;
public sealed class CidrBlockConverter : JsonConverter {
public override CidrBlock Read(
diff --git a/src/EnvironmentConfig/Converters/IpAddressConverter.cs b/src/Serialization/Converters/IpAddressConverter.cs
similarity index 93%
rename from src/EnvironmentConfig/Converters/IpAddressConverter.cs
rename to src/Serialization/Converters/IpAddressConverter.cs
index f0ce208b..2ffb2b8c 100644
--- a/src/EnvironmentConfig/Converters/IpAddressConverter.cs
+++ b/src/Serialization/Converters/IpAddressConverter.cs
@@ -1,7 +1,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
-namespace Drift.EnvironmentConfig.Converters;
+namespace Drift.Serialization.Converters;
public sealed class IpAddressConverter : JsonConverter {
public override System.Net.IPAddress Read(
diff --git a/src/Serialization/Serialization.csproj b/src/Serialization/Serialization.csproj
index c6321610..493404aa 100644
--- a/src/Serialization/Serialization.csproj
+++ b/src/Serialization/Serialization.csproj
@@ -1,3 +1,7 @@
+
+
+
+
From 8468f1df0203cf336f4c05c8c4c558ee58b5d64c Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Mon, 10 Nov 2025 21:16:28 +0100
Subject: [PATCH 04/31] f
---
.../Networking.PeerStreaming.Core.csproj | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/Networking.PeerStreaming.Core/Networking.PeerStreaming.Core.csproj b/src/Networking.PeerStreaming.Core/Networking.PeerStreaming.Core.csproj
index ef15c1a2..d0e60a0d 100644
--- a/src/Networking.PeerStreaming.Core/Networking.PeerStreaming.Core.csproj
+++ b/src/Networking.PeerStreaming.Core/Networking.PeerStreaming.Core.csproj
@@ -8,7 +8,6 @@
-
From 89fe877862c2c08f0ca26cdf4d622970f35468fd Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Mon, 10 Nov 2025 22:43:28 +0100
Subject: [PATCH 05/31] FeatureFlagTest
---
.../TemporarySettingsLocationProvider.cs | 2 +-
src/Cli.Tests/Cli.Tests.csproj | 1 +
src/Cli.Tests/FeatureFlagTest.cs | 76 +++++++++++++++++++
...CommandBase.cs => ContainerCommandBase.cs} | 0
4 files changed, 78 insertions(+), 1 deletion(-)
create mode 100644 src/Cli.Tests/FeatureFlagTest.cs
rename src/Cli/Commands/Common/Commands/{SpecCommandBase.cs => ContainerCommandBase.cs} (100%)
diff --git a/src/Cli.Settings.Tests/TemporarySettingsLocationProvider.cs b/src/Cli.Settings.Tests/TemporarySettingsLocationProvider.cs
index 6b9777ce..5083187a 100644
--- a/src/Cli.Settings.Tests/TemporarySettingsLocationProvider.cs
+++ b/src/Cli.Settings.Tests/TemporarySettingsLocationProvider.cs
@@ -2,7 +2,7 @@
namespace Drift.Cli.Settings.Tests;
-internal sealed class TemporarySettingsLocationProvider : ISettingsLocationProvider {
+public sealed class TemporarySettingsLocationProvider : ISettingsLocationProvider {
private readonly string _directory = Path.Combine( Path.GetTempPath(), Guid.NewGuid().ToString() );
public string GetDirectory() => _directory;
diff --git a/src/Cli.Tests/Cli.Tests.csproj b/src/Cli.Tests/Cli.Tests.csproj
index 53d4e722..5cfe4b53 100644
--- a/src/Cli.Tests/Cli.Tests.csproj
+++ b/src/Cli.Tests/Cli.Tests.csproj
@@ -5,6 +5,7 @@
+
diff --git a/src/Cli.Tests/FeatureFlagTest.cs b/src/Cli.Tests/FeatureFlagTest.cs
new file mode 100644
index 00000000..1beba7e0
--- /dev/null
+++ b/src/Cli.Tests/FeatureFlagTest.cs
@@ -0,0 +1,76 @@
+using System.CommandLine;
+using Drift.Cli.Commands.Common.Commands;
+using Drift.Cli.Commands.Common.Parameters;
+using Drift.Cli.Infrastructure;
+using Drift.Cli.Presentation.Console.Managers.Abstractions;
+using Drift.Cli.Settings.Serialization;
+using Drift.Cli.Settings.Tests;
+using Drift.Cli.Settings.V1_preview;
+using Drift.Cli.Settings.V1_preview.FeatureFlags;
+using Drift.Cli.Tests.Utils;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Drift.Cli.Tests;
+
+internal sealed class FeatureFlagTest {
+ private const string DummyCodeCommand = "dummy";
+ private const int DummyCommandExitCode = 1337;
+ private static readonly FeatureFlag MyFeature = new("myFeature");
+ private static readonly ISettingsLocationProvider SettingsLocationProvider = new TemporarySettingsLocationProvider();
+
+ [Test]
+ public async Task SettingsControlFlag( [Values( false, true )] bool featureEnabled ) {
+ // Arrange
+ if ( Directory.Exists( SettingsLocationProvider.GetDirectory() ) ) {
+ Directory.Delete( SettingsLocationProvider.GetDirectory(), true );
+ }
+
+ new CliSettings { Features = [new FeatureFlagSetting( MyFeature, featureEnabled )] }.Save(
+ logger: NullLogger.Instance,
+ location: SettingsLocationProvider
+ );
+
+ RootCommandFactory.CommandRegistration[] customCommands = [
+ new(typeof(DummyTestCommandHandler), sp => new DummyTestCommand( sp ))
+ ];
+
+ // Act
+ var result = await DriftTestCli.InvokeFromTestAsync( $"{DummyCodeCommand}", customCommands: customCommands );
+
+ // Assert
+ using ( Assert.EnterMultipleScope() ) {
+ Assert.That( result.ExitCode, Is.EqualTo( DummyCommandExitCode ) );
+ var expectedOutput = featureEnabled ? "Feature is enabled" : "Feature is disabled";
+ Assert.That( result.Output.ToString(), Is.EqualTo( expectedOutput ) );
+ Assert.That( result.Error.ToString(), Is.Empty );
+ }
+ }
+
+ private sealed class DummyTestCommand( IServiceProvider provider )
+ : CommandBase(
+ DummyCodeCommand,
+ "Command that switches behavior using a feature flag",
+ provider
+ ) {
+ protected override DummyTestParameters CreateParameters( ParseResult result ) {
+ return new DummyTestParameters( result );
+ }
+ }
+
+ private sealed class DummyTestCommandHandler( IOutputManager output ) : ICommandHandler {
+ public Task Invoke( DummyTestParameters parameters, CancellationToken cancellationToken ) {
+ output.Normal.Write(
+ CliSettings.Load( location: SettingsLocationProvider ).IsFeatureEnabled( MyFeature )
+ ? "Feature is enabled"
+ : "Feature is disabled"
+ );
+
+ return Task.FromResult( DummyCommandExitCode );
+ }
+ }
+
+ private sealed record DummyTestParameters : BaseParameters {
+ public DummyTestParameters( ParseResult parseResult ) : base( parseResult ) {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Cli/Commands/Common/Commands/SpecCommandBase.cs b/src/Cli/Commands/Common/Commands/ContainerCommandBase.cs
similarity index 100%
rename from src/Cli/Commands/Common/Commands/SpecCommandBase.cs
rename to src/Cli/Commands/Common/Commands/ContainerCommandBase.cs
From b7618423ae4a6812920ede3d4095ef6ac6a888f6 Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Mon, 10 Nov 2025 23:04:51 +0100
Subject: [PATCH 06/31] f
---
src/Cli.Tests/FeatureFlagTest.cs | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/src/Cli.Tests/FeatureFlagTest.cs b/src/Cli.Tests/FeatureFlagTest.cs
index 1beba7e0..8af7cae0 100644
--- a/src/Cli.Tests/FeatureFlagTest.cs
+++ b/src/Cli.Tests/FeatureFlagTest.cs
@@ -19,16 +19,18 @@ internal sealed class FeatureFlagTest {
private static readonly ISettingsLocationProvider SettingsLocationProvider = new TemporarySettingsLocationProvider();
[Test]
- public async Task SettingsControlFlag( [Values( false, true )] bool featureEnabled ) {
+ public async Task SettingsControlFlag( [Values( false, true, null )] bool? featureEnabled ) {
// Arrange
if ( Directory.Exists( SettingsLocationProvider.GetDirectory() ) ) {
Directory.Delete( SettingsLocationProvider.GetDirectory(), true );
}
- new CliSettings { Features = [new FeatureFlagSetting( MyFeature, featureEnabled )] }.Save(
- logger: NullLogger.Instance,
- location: SettingsLocationProvider
- );
+ var settings = new CliSettings();
+ if ( featureEnabled != null ) {
+ settings.Features = [new FeatureFlagSetting( MyFeature, featureEnabled.Value )];
+ }
+
+ settings.Save( logger: NullLogger.Instance, location: SettingsLocationProvider );
RootCommandFactory.CommandRegistration[] customCommands = [
new(typeof(DummyTestCommandHandler), sp => new DummyTestCommand( sp ))
@@ -40,10 +42,12 @@ public async Task SettingsControlFlag( [Values( false, true )] bool featureEnabl
// Assert
using ( Assert.EnterMultipleScope() ) {
Assert.That( result.ExitCode, Is.EqualTo( DummyCommandExitCode ) );
- var expectedOutput = featureEnabled ? "Feature is enabled" : "Feature is disabled";
+ var expectedOutput = featureEnabled == true ? "Feature is enabled" : "Feature is disabled";
Assert.That( result.Output.ToString(), Is.EqualTo( expectedOutput ) );
Assert.That( result.Error.ToString(), Is.Empty );
}
+
+ Console.WriteLine( result.Output.ToString() );
}
private sealed class DummyTestCommand( IServiceProvider provider )
From 3423a8ee9794bf37e213c6156cdf06c359d44e75 Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Mon, 10 Nov 2025 23:09:33 +0100
Subject: [PATCH 07/31] deps bump
---
Directory.Packages.props | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 7c57ddb1..59f3b0ef 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -5,11 +5,11 @@
true
-
+
-
+
From d541444e7507fa2cff090e7492d4fe295a073526 Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Wed, 26 Nov 2025 21:06:31 +0100
Subject: [PATCH 08/31] f
---
src/Agent.PeerProtocol/Agent.PeerProtocol.csproj | 6 ------
1 file changed, 6 deletions(-)
diff --git a/src/Agent.PeerProtocol/Agent.PeerProtocol.csproj b/src/Agent.PeerProtocol/Agent.PeerProtocol.csproj
index 13dacae1..538b67e3 100644
--- a/src/Agent.PeerProtocol/Agent.PeerProtocol.csproj
+++ b/src/Agent.PeerProtocol/Agent.PeerProtocol.csproj
@@ -1,11 +1,5 @@
-
- net9.0
- enable
- enable
-
-
From 6b7cb2deaa9660b291e30fa1d031c68415b0ade5 Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Mon, 1 Dec 2025 22:24:48 +0100
Subject: [PATCH 09/31] works
---
.../Adopt/AdoptRequestHandler.cs | 5 ++-
.../Adopt/AdoptRequestPayload.cs | 7 +++-
src/Agent.PeerProtocol/Adopt/NullResponse.cs | 14 +++++++
.../Agent.PeerProtocol.csproj | 1 +
.../PeerProtocolJsonContext.cs | 10 +++++
.../PeerProtocolTypesProvider.cs | 12 ++++++
.../ServiceCollectionExtensions.cs | 16 ++++++-
.../Subnets/SubnetsRequest.cs | 15 ++++++-
.../Subnets/SubnetsRequestHandler.cs | 6 +--
.../Subnets/SubnetsResponse.cs | 20 ++++++++-
.../Commands/Common/Commands/CommandBase.cs | 6 +--
src/Cli/Commands/Scan/ClusterExtensions.cs | 2 +-
src/Networking.Clustering/Cluster.cs | 28 ++++++-------
src/Networking.Clustering/ICluster.cs | 8 ++--
.../IPeerMessage.cs | 8 +++-
.../IPeerMessageEnvelopeConverter.cs | 2 +-
.../IPeerMessageHandler.cs | 42 ++++++++++++++++---
.../IPeerMessageTypesProvider.cs | 7 ++++
.../IPeerStreamManager.cs | 2 +-
.../Messages/PeerMessageDispatcher.cs | 10 ++---
.../Messages/PeerMessageEnvelopeConverter.cs | 41 ++++--------------
.../Messages/PeerMessageTypesProvider.cs | 23 ----------
.../PeerStream.cs | 10 +++--
.../PeerStreamManager.cs | 8 ++++
.../ServiceCollectionExtensions.cs | 6 +--
.../Helpers/TestPeerMessage.cs | 28 +++++++++++++
.../PeerMessageTypesProviderTests.cs | 16 +++++++
.../PeerStreamManagerTests.cs | 29 +++----------
28 files changed, 249 insertions(+), 133 deletions(-)
create mode 100644 src/Agent.PeerProtocol/Adopt/NullResponse.cs
create mode 100644 src/Agent.PeerProtocol/PeerProtocolJsonContext.cs
create mode 100644 src/Agent.PeerProtocol/PeerProtocolTypesProvider.cs
create mode 100644 src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageTypesProvider.cs
delete mode 100644 src/Networking.PeerStreaming.Core/Messages/PeerMessageTypesProvider.cs
create mode 100644 src/Networking.PeerStreaming.Tests/Helpers/TestPeerMessage.cs
create mode 100644 src/Networking.PeerStreaming.Tests/PeerMessageTypesProviderTests.cs
diff --git a/src/Agent.PeerProtocol/Adopt/AdoptRequestHandler.cs b/src/Agent.PeerProtocol/Adopt/AdoptRequestHandler.cs
index c586a04a..635bb5cb 100644
--- a/src/Agent.PeerProtocol/Adopt/AdoptRequestHandler.cs
+++ b/src/Agent.PeerProtocol/Adopt/AdoptRequestHandler.cs
@@ -4,12 +4,13 @@
namespace Drift.Agent.PeerProtocol.Adopt;
-internal sealed class AdoptRequestHandler : IPeerMessageHandler {
+internal sealed class AdoptRequestHandler : IPeerMessageHandler {
private readonly ILogger _logger; // Example: inject what you need
public string MessageType => "adopt-request";
- public async Task HandleAsync( AdoptRequestPayload message, CancellationToken cancellationToken = default ) {
+ public async Task HandleAsync( AdoptRequestPayload message,
+ CancellationToken cancellationToken = default ) {
_logger.LogInformation( $"[AdoptRequest] Controller: {message.ControllerId}" );
return null;
}
diff --git a/src/Agent.PeerProtocol/Adopt/AdoptRequestPayload.cs b/src/Agent.PeerProtocol/Adopt/AdoptRequestPayload.cs
index 17c39167..d5348c6b 100644
--- a/src/Agent.PeerProtocol/Adopt/AdoptRequestPayload.cs
+++ b/src/Agent.PeerProtocol/Adopt/AdoptRequestPayload.cs
@@ -1,9 +1,10 @@
+using System.Text.Json.Serialization.Metadata;
using Drift.Networking.PeerStreaming.Core.Abstractions;
namespace Drift.Agent.PeerProtocol.Adopt;
internal sealed class AdoptRequestPayload : IPeerMessage {
- public string MessageType => "adopt-request";
+ public static string MessageType => "adopt-request";
public string Jwt {
get;
@@ -14,4 +15,8 @@ public string ControllerId {
get;
set;
}
+
+ public static JsonTypeInfo JsonInfo {
+ get;
+ }
}
\ No newline at end of file
diff --git a/src/Agent.PeerProtocol/Adopt/NullResponse.cs b/src/Agent.PeerProtocol/Adopt/NullResponse.cs
new file mode 100644
index 00000000..ae23a1c3
--- /dev/null
+++ b/src/Agent.PeerProtocol/Adopt/NullResponse.cs
@@ -0,0 +1,14 @@
+using System.Text.Json.Serialization.Metadata;
+using Drift.Networking.PeerStreaming.Core.Abstractions;
+
+namespace Drift.Agent.PeerProtocol.Adopt;
+
+public class NullResponse : IPeerMessage {
+ public static string MessageType {
+ get;
+ }
+
+ public static JsonTypeInfo JsonInfo {
+ get;
+ }
+}
\ No newline at end of file
diff --git a/src/Agent.PeerProtocol/Agent.PeerProtocol.csproj b/src/Agent.PeerProtocol/Agent.PeerProtocol.csproj
index 538b67e3..fd5ef1e8 100644
--- a/src/Agent.PeerProtocol/Agent.PeerProtocol.csproj
+++ b/src/Agent.PeerProtocol/Agent.PeerProtocol.csproj
@@ -3,6 +3,7 @@
+
diff --git a/src/Agent.PeerProtocol/PeerProtocolJsonContext.cs b/src/Agent.PeerProtocol/PeerProtocolJsonContext.cs
new file mode 100644
index 00000000..19d20e2e
--- /dev/null
+++ b/src/Agent.PeerProtocol/PeerProtocolJsonContext.cs
@@ -0,0 +1,10 @@
+using System.Text.Json.Serialization;
+using Drift.Agent.PeerProtocol.Subnets;
+
+namespace Drift.Agent.PeerProtocol;
+
+/*
+[JsonSerializable( typeof(SubnetsRequest) )]
+[JsonSerializable( typeof(SubnetsResponse) )]
+internal partial class PeerProtocolJsonContext : JsonSerializerContext {
+}*/
\ No newline at end of file
diff --git a/src/Agent.PeerProtocol/PeerProtocolTypesProvider.cs b/src/Agent.PeerProtocol/PeerProtocolTypesProvider.cs
new file mode 100644
index 00000000..093e0519
--- /dev/null
+++ b/src/Agent.PeerProtocol/PeerProtocolTypesProvider.cs
@@ -0,0 +1,12 @@
+using System.Text.Json.Serialization.Metadata;
+using Drift.Networking.PeerStreaming.Core.Abstractions;
+
+namespace Drift.Agent.PeerProtocol;
+
+internal sealed class PeerProtocolTypesProvider : IPeerMessageTypesProvider {
+ internal static readonly Dictionary Map = new();
+
+ public Dictionary Get() {
+ return Map;
+ }
+}
\ No newline at end of file
diff --git a/src/Agent.PeerProtocol/ServiceCollectionExtensions.cs b/src/Agent.PeerProtocol/ServiceCollectionExtensions.cs
index 06b4412f..daa7cf8d 100644
--- a/src/Agent.PeerProtocol/ServiceCollectionExtensions.cs
+++ b/src/Agent.PeerProtocol/ServiceCollectionExtensions.cs
@@ -7,7 +7,19 @@ namespace Drift.Agent.PeerProtocol;
public static class ServiceCollectionExtensions {
public static void AddPeerProtocol( this IServiceCollection services ) {
//TODO need both?
- services.AddScoped, SubnetsRequestHandler>();
- services.AddScoped();
+ // services.AddScoped();
+ // services.AddScoped, SubnetsRequestHandler>();
+ services.AddPeerMessageHandler();
+
+ //services.AddScoped();
+ }
+
+ private static IServiceCollection AddPeerMessageHandler( this IServiceCollection services )
+ where THandler : class, IPeerMessageHandler
+ where TReq : IPeerMessage
+ where TRes : IPeerMessage {
+ services.AddScoped();
+ services.AddScoped, THandler>();
+ return services;
}
}
\ No newline at end of file
diff --git a/src/Agent.PeerProtocol/Subnets/SubnetsRequest.cs b/src/Agent.PeerProtocol/Subnets/SubnetsRequest.cs
index 2cb6044d..6be84052 100644
--- a/src/Agent.PeerProtocol/Subnets/SubnetsRequest.cs
+++ b/src/Agent.PeerProtocol/Subnets/SubnetsRequest.cs
@@ -1,7 +1,18 @@
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
using Drift.Networking.PeerStreaming.Core.Abstractions;
namespace Drift.Agent.PeerProtocol.Subnets;
public sealed class SubnetsRequest : IPeerMessage {
- public string MessageType => "subnetsrequest";
-}
\ No newline at end of file
+ static SubnetsRequest() {
+ PeerProtocolTypesProvider.Map[MessageType] = JsonInfo;
+ }
+
+ public static string MessageType => "subnetsrequest";
+
+ public static JsonTypeInfo JsonInfo => SubnetsRequestJsonContext.Default.SubnetsRequest;
+}
+
+[JsonSerializable( typeof(SubnetsRequest) )]
+internal sealed partial class SubnetsRequestJsonContext : JsonSerializerContext;
\ No newline at end of file
diff --git a/src/Agent.PeerProtocol/Subnets/SubnetsRequestHandler.cs b/src/Agent.PeerProtocol/Subnets/SubnetsRequestHandler.cs
index 48654152..0b883d37 100644
--- a/src/Agent.PeerProtocol/Subnets/SubnetsRequestHandler.cs
+++ b/src/Agent.PeerProtocol/Subnets/SubnetsRequestHandler.cs
@@ -9,10 +9,10 @@ namespace Drift.Agent.PeerProtocol.Subnets;
internal sealed class SubnetsRequestHandler(
IInterfaceSubnetProvider interfaceSubnetProvider,
ILogger logger
-) : IPeerMessageHandler {
- public string MessageType => "subnetsrequest";
+) : IPeerMessageHandler {
+ public string MessageType => SubnetsRequest.MessageType;
- public async Task HandleAsync(
+ public async Task HandleAsync(
SubnetsRequest message,
CancellationToken cancellationToken = default
) {
diff --git a/src/Agent.PeerProtocol/Subnets/SubnetsResponse.cs b/src/Agent.PeerProtocol/Subnets/SubnetsResponse.cs
index 25b7c549..d28c3380 100644
--- a/src/Agent.PeerProtocol/Subnets/SubnetsResponse.cs
+++ b/src/Agent.PeerProtocol/Subnets/SubnetsResponse.cs
@@ -1,13 +1,29 @@
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
using Drift.Domain;
using Drift.Networking.PeerStreaming.Core.Abstractions;
+using Drift.Serialization.Converters;
namespace Drift.Agent.PeerProtocol.Subnets;
public sealed class SubnetsResponse : IPeerMessage {
- public string MessageType => "subnetsresponse";
+ static SubnetsResponse() {
+ PeerProtocolTypesProvider.Map[MessageType] = JsonInfo;
+ }
+
+ public static string MessageType => "subnetsresponse";
public required IReadOnlyList Subnets {
get;
init;
}
-}
\ No newline at end of file
+
+ public static JsonTypeInfo JsonInfo => SubnetsResponseJsonContext.Default.SubnetsResponse;
+}
+
+[JsonSourceGenerationOptions(
+ Converters = [typeof(CidrBlockConverter), typeof(IpAddressConverter)],
+ PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase
+)]
+[JsonSerializable( typeof(SubnetsResponse) )]
+internal sealed partial class SubnetsResponseJsonContext : JsonSerializerContext;
\ No newline at end of file
diff --git a/src/Cli/Commands/Common/Commands/CommandBase.cs b/src/Cli/Commands/Common/Commands/CommandBase.cs
index cdfd1c50..5318539f 100644
--- a/src/Cli/Commands/Common/Commands/CommandBase.cs
+++ b/src/Cli/Commands/Common/Commands/CommandBase.cs
@@ -13,8 +13,8 @@ protected CommandBase( string name, string description, IServiceProvider provide
Add( CommonParameters.Options.OutputFormat );
Add( CommonParameters.Arguments.Spec );
- SetAction( ( parseResult, cancellationToken ) => {
- using var scope = provider.CreateScope();
+ SetAction( async ( parseResult, cancellationToken ) => {
+ await using var scope = provider.CreateAsyncScope();
var serviceProvider = scope.ServiceProvider;
serviceProvider.GetRequiredService().ParseResult = parseResult;
@@ -22,7 +22,7 @@ protected CommandBase( string name, string description, IServiceProvider provide
var handler = serviceProvider.GetRequiredService();
var parameters = CreateParameters( parseResult );
- return handler.Invoke( parameters, cancellationToken );
+ return await handler.Invoke( parameters, cancellationToken );
} );
}
diff --git a/src/Cli/Commands/Scan/ClusterExtensions.cs b/src/Cli/Commands/Scan/ClusterExtensions.cs
index 03347cba..929031a3 100644
--- a/src/Cli/Commands/Scan/ClusterExtensions.cs
+++ b/src/Cli/Commands/Scan/ClusterExtensions.cs
@@ -9,7 +9,7 @@ internal static Task GetSubnetsAsync(
Domain.Agent agent,
CancellationToken cancellationToken
) {
- return cluster.SendAndWaitAsync(
+ return cluster.SendAndWaitAsync(
agent,
new SubnetsRequest(),
timeout: TimeSpan.FromSeconds( 10 ),
diff --git a/src/Networking.Clustering/Cluster.cs b/src/Networking.Clustering/Cluster.cs
index ef57286e..475ede31 100644
--- a/src/Networking.Clustering/Cluster.cs
+++ b/src/Networking.Clustering/Cluster.cs
@@ -10,18 +10,18 @@ internal sealed class Cluster(
PeerResponseCorrelator responseCorrelator,
ILogger logger
) : ICluster {
- public async Task SendAsync(
+ /*public async Task SendAsync(
Domain.Agent agent,
- IPeerMessage message,
+ TMessage message,
CancellationToken cancellationToken = default
- ) {
+ ) where TMessage : IPeerMessage {
try {
- await SendInternalAsync( agent, message, cancellationToken );
+ await SendInternalAsync( agent, message, cancellationToken );
}
catch ( Exception ex ) {
logger.LogWarning( ex, "Send to {Peer} failed", agent );
}
- }
+ }*/
/* public async Task BroadcastAsync( PeerMessage message, CancellationToken cancellationToken = default ) {
var peers = peerStreamManager.GetConnectedPeers();
@@ -39,24 +39,24 @@ public async Task SendAsync(
await Task.WhenAll( tasks );
}*/
- public async Task SendInternalAsync(
+ /*public async Task SendInternalAsync(
Domain.Agent agent,
- IPeerMessage message,
+ TMessage message,
CancellationToken cancellationToken = default
- ) {
+ ) where TMessage : IPeerMessage {
var connection = peerStreamManager.GetOrCreate( new Uri( agent.Address ), "agentid_local1" );
- var envelope = envelopeConverter.ToEnvelope( message );
+ var envelope = envelopeConverter.ToEnvelope( message );
await connection.SendAsync( envelope );
- }
+ }*/
- public async Task SendAndWaitAsync(
+ public async Task SendAndWaitAsync(
Domain.Agent agent,
- IPeerMessage message,
+ TReq message,
TimeSpan? timeout = null,
CancellationToken cancellationToken = default
- ) where TResponse : IPeerMessage {
+ ) where TResponse : IPeerMessage where TReq : IPeerMessage {
var correlationId = Guid.NewGuid().ToString();
- var envelope = envelopeConverter.ToEnvelope( message );
+ var envelope = envelopeConverter.ToEnvelope( message );
envelope.CorrelationId = correlationId;
// Register correlator BEFORE sending
diff --git a/src/Networking.Clustering/ICluster.cs b/src/Networking.Clustering/ICluster.cs
index b3527f9c..0bb79136 100644
--- a/src/Networking.Clustering/ICluster.cs
+++ b/src/Networking.Clustering/ICluster.cs
@@ -3,14 +3,14 @@
namespace Drift.Networking.Clustering;
public interface ICluster {
- Task SendAsync( Domain.Agent agent, IPeerMessage message, CancellationToken cancellationToken = default );
+ //Task SendAsync( Domain.Agent agent, IPeerMessage message, CancellationToken cancellationToken = default );
- Task SendAndWaitAsync(
+ Task SendAndWaitAsync(
Domain.Agent agent,
- IPeerMessage message,
+ TReq message,
TimeSpan? timeout = null,
CancellationToken cancellationToken = default
- ) where TResponse : IPeerMessage;
+ ) where TResponse : IPeerMessage where TReq : IPeerMessage;
/*Task BroadcastAsync( PeerMessage message, CancellationToken cancellationToken = default );
Task> RequestSubnetsAsync( string peerAddress, CancellationToken cancellationToken = default );
diff --git a/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessage.cs b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessage.cs
index 6ea62d0b..5bb4e451 100644
--- a/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessage.cs
+++ b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessage.cs
@@ -1,7 +1,13 @@
+using System.Text.Json.Serialization.Metadata;
+
namespace Drift.Networking.PeerStreaming.Core.Abstractions;
public interface IPeerMessage {
- string MessageType {
+ static abstract string MessageType {
+ get;
+ }
+
+ static abstract JsonTypeInfo JsonInfo {
get;
}
}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageEnvelopeConverter.cs b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageEnvelopeConverter.cs
index 49b546fe..69a02c1a 100644
--- a/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageEnvelopeConverter.cs
+++ b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageEnvelopeConverter.cs
@@ -3,7 +3,7 @@
namespace Drift.Networking.PeerStreaming.Core.Abstractions;
public interface IPeerMessageEnvelopeConverter {
- public PeerMessage ToEnvelope( IPeerMessage message, string? requestId = null );
+ public PeerMessage ToEnvelope( IPeerMessage message, string? requestId = null ) where T : IPeerMessage;
public T FromEnvelope( PeerMessage envelope ) where T : IPeerMessage;
}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageHandler.cs b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageHandler.cs
index b4f6fd5e..945deb35 100644
--- a/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageHandler.cs
+++ b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageHandler.cs
@@ -2,7 +2,7 @@
namespace Drift.Networking.PeerStreaming.Core.Abstractions;
-public interface IPeerMessageHandler {
+/*public interface IPeerMessageHandler {
string MessageType {
get;
}
@@ -12,17 +12,49 @@ string MessageType {
IPeerMessageEnvelopeConverter envelopeConverter,
CancellationToken cancellationToken = default
);
+}*/
+
+public interface IPeerMessageHandlerBase {
+ string MessageType {
+ get;
+ }
+
+ Task DispatchAsync(
+ PeerMessage envelope,
+ IPeerMessageEnvelopeConverter converter,
+ CancellationToken cancellationToken
+ );
}
-public interface IPeerMessageHandler : IPeerMessageHandler where T : IPeerMessage {
- async Task IPeerMessageHandler.HandleAsync(
+public interface IPeerMessageHandler : IPeerMessageHandlerBase
+ where TRequest : IPeerMessage
+ where TResponse : IPeerMessage {
+ // TODO unused now
+ async Task HandleAsync(
PeerMessage envelope,
IPeerMessageEnvelopeConverter envelopeConverter,
CancellationToken cancellationToken
) {
- var typedMessage = envelopeConverter.FromEnvelope( envelope );
+ var typedMessage = envelopeConverter.FromEnvelope( envelope );
return await HandleAsync( typedMessage, cancellationToken );
}
- Task HandleAsync( T message, CancellationToken cancellationToken = default );
+ Task HandleAsync( TRequest message, CancellationToken cancellationToken = default );
+
+ async Task IPeerMessageHandlerBase.DispatchAsync(
+ PeerMessage envelope,
+ IPeerMessageEnvelopeConverter converter,
+ CancellationToken cancellationToken ) {
+ // Deserialize strongly typed request
+ var request = converter.FromEnvelope( envelope );
+
+ // Process request
+ var response = await HandleAsync( request, cancellationToken );
+
+ if ( response is null )
+ return null;
+
+ // Serialize strongly typed response
+ return converter.ToEnvelope( response );
+ }
}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageTypesProvider.cs b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageTypesProvider.cs
new file mode 100644
index 00000000..37d6fe8d
--- /dev/null
+++ b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageTypesProvider.cs
@@ -0,0 +1,7 @@
+using System.Text.Json.Serialization.Metadata;
+
+namespace Drift.Networking.PeerStreaming.Core.Abstractions;
+
+public interface IPeerMessageTypesProvider {
+ Dictionary Get();
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core.Abstractions/IPeerStreamManager.cs b/src/Networking.PeerStreaming.Core.Abstractions/IPeerStreamManager.cs
index e115a60a..cf4576e3 100644
--- a/src/Networking.PeerStreaming.Core.Abstractions/IPeerStreamManager.cs
+++ b/src/Networking.PeerStreaming.Core.Abstractions/IPeerStreamManager.cs
@@ -4,7 +4,7 @@
namespace Drift.Networking.PeerStreaming.Core.Abstractions;
-public interface IPeerStreamManager {
+public interface IPeerStreamManager : IAsyncDisposable {
public IPeerStream GetOrCreate( Uri peerAddress, AgentId id );
public IPeerStream Create(
diff --git a/src/Networking.PeerStreaming.Core/Messages/PeerMessageDispatcher.cs b/src/Networking.PeerStreaming.Core/Messages/PeerMessageDispatcher.cs
index b75c4632..06640bcd 100644
--- a/src/Networking.PeerStreaming.Core/Messages/PeerMessageDispatcher.cs
+++ b/src/Networking.PeerStreaming.Core/Messages/PeerMessageDispatcher.cs
@@ -8,10 +8,10 @@ public sealed class PeerMessageDispatcher {
private readonly PeerResponseCorrelator _responseCorrelator;
private readonly IPeerMessageEnvelopeConverter _envelopeConverter;
private readonly ILogger _logger;
- private readonly Dictionary _handlers;
+ private readonly Dictionary _handlers;
public PeerMessageDispatcher(
- IEnumerable handlers,
+ IEnumerable handlers,
IPeerMessageEnvelopeConverter envelopeConverter,
PeerResponseCorrelator responseCorrelator,
ILogger logger
@@ -39,10 +39,10 @@ ILogger logger
// Otherwise, dispatch to handler
if ( _handlers.TryGetValue( message.MessageType, out var handler ) ) {
- var response = await handler.HandleAsync( message, _envelopeConverter, ct );
+ var responseEnvelope = await handler.DispatchAsync( message, _envelopeConverter, ct );
- if ( response != null ) {
- var responseEnvelope = _envelopeConverter.ToEnvelope( response );
+ if ( responseEnvelope != null ) {
+ //var responseEnvelope = _envelopeConverter.ToEnvelope( response );
responseEnvelope.ReplyTo = message.CorrelationId;
await peerStream.SendAsync( responseEnvelope );
}
diff --git a/src/Networking.PeerStreaming.Core/Messages/PeerMessageEnvelopeConverter.cs b/src/Networking.PeerStreaming.Core/Messages/PeerMessageEnvelopeConverter.cs
index 0ea0f24b..2a56ed2e 100644
--- a/src/Networking.PeerStreaming.Core/Messages/PeerMessageEnvelopeConverter.cs
+++ b/src/Networking.PeerStreaming.Core/Messages/PeerMessageEnvelopeConverter.cs
@@ -1,4 +1,5 @@
using System.Text.Json;
+using System.Text.Json.Serialization.Metadata;
using Drift.Networking.Grpc.Generated;
using Drift.Networking.PeerStreaming.Core.Abstractions;
using Drift.Serialization.Converters;
@@ -6,43 +7,19 @@
namespace Drift.Networking.PeerStreaming.Core.Messages;
internal sealed class PeerMessageEnvelopeConverter : IPeerMessageEnvelopeConverter {
- private readonly Dictionary _typeMap = new();
- private readonly JsonSerializerOptions _serializerOptions;
-
- public PeerMessageEnvelopeConverter( IPeerMessageTypesProvider provider ) : this( provider.Get() ) {
- }
-
- public PeerMessageEnvelopeConverter( params Type[] messageTypes ) : this( (IEnumerable) messageTypes ) {
+ public PeerMessage ToEnvelope( IPeerMessage message, string? requestId = null )
+ where T : IPeerMessage {
+ string json = JsonSerializer.Serialize( message, T.JsonInfo );
+ return new PeerMessage { MessageType = T.MessageType, Message = json, };
}
- private PeerMessageEnvelopeConverter( IEnumerable messageTypes ) {
- foreach ( var type in messageTypes ) {
- // TODO improve
- var instance = (IPeerMessage) Activator.CreateInstance( type )!;
- _typeMap[instance.MessageType] = type;
- }
-
- _serializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
- _serializerOptions.Converters.Add( new IpAddressConverter() );
- _serializerOptions.Converters.Add( new CidrBlockConverter() );
- }
-
- public PeerMessage ToEnvelope( IPeerMessage message, string? requestId = null ) {
- var json = JsonSerializer.Serialize( message, message.GetType(), _serializerOptions );
- return new PeerMessage { MessageType = message.MessageType, Message = json, };
- }
-
- public T FromEnvelope( PeerMessage envelope ) where T : IPeerMessage {
- if ( !_typeMap.TryGetValue( envelope.MessageType, out var type ) ) {
- throw new InvalidOperationException( $"Unknown message type: {envelope.MessageType}" );
- }
-
- if ( type != typeof(T) ) {
+ public TSelf FromEnvelope( PeerMessage envelope ) where TSelf : IPeerMessage {
+ if ( envelope.MessageType != TSelf.MessageType ) {
throw new InvalidOperationException(
- $"Message type mismatch: expected {typeof(T).Name}, got {envelope.MessageType}"
+ $"Envelope contains '{envelope.MessageType}' but caller expects '{TSelf.MessageType}'."
);
}
- return (T) JsonSerializer.Deserialize( envelope.Message, type, _serializerOptions )!;
+ return JsonSerializer.Deserialize( envelope.Message, TSelf.JsonInfo.Options )!;
}
}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core/Messages/PeerMessageTypesProvider.cs b/src/Networking.PeerStreaming.Core/Messages/PeerMessageTypesProvider.cs
deleted file mode 100644
index c5d239b8..00000000
--- a/src/Networking.PeerStreaming.Core/Messages/PeerMessageTypesProvider.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System.Reflection;
-using Drift.Networking.PeerStreaming.Core.Abstractions;
-
-namespace Drift.Networking.PeerStreaming.Core.Messages;
-
-public interface IPeerMessageTypesProvider {
- IEnumerable Get();
-}
-
-public class AssemblyScanPeerMessageTypesProvider( params Assembly[] assemblies ) : IPeerMessageTypesProvider {
- public IEnumerable Get() {
- var types = assemblies
- .SelectMany( a => a.GetTypes() )
- .Where( t => typeof(IPeerMessage).IsAssignableFrom( t ) && !t.IsAbstract && !t.IsInterface );
-
- if ( types.Count() == 0 ) {
- throw new InvalidOperationException(
- $"No types implementing {nameof(IPeerMessage)} found in assemblies: {string.Join( ", ", assemblies.Select( a => a.GetName().Name ) )}" );
- }
-
- return types;
- }
-}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core/PeerStream.cs b/src/Networking.PeerStreaming.Core/PeerStream.cs
index ab26c1ac..7e91820f 100644
--- a/src/Networking.PeerStreaming.Core/PeerStream.cs
+++ b/src/Networking.PeerStreaming.Core/PeerStream.cs
@@ -76,6 +76,9 @@ private async Task ReadLoopAsync() {
try {
await foreach ( var message in _reader.ReadAllAsync( _cancellationToken ) ) {
try {
+ using var scope = _logger.BeginScope(
+ new Dictionary { ["RequestId"] = message.CorrelationId ?? "no-id" }
+ );
_logger.LogDebug( "Received message. Dispatching to handler..." );
await _dispatcher.DispatchAsync( message, this, CancellationToken.None );
}
@@ -100,10 +103,11 @@ private async Task ReadLoopAsync() {
public async ValueTask DisposeAsync() {
Console.WriteLine( "Disposing " + this );
- /*if ( _call != null ) {
+ if ( _writer is IClientStreamWriter clientWriter ) {
// I.e., outgoing stream (client initiated)
- await _call.RequestStream.CompleteAsync();
- }*/
+ // Server streams are automatically completed by the gRPC framework
+ await clientWriter.CompleteAsync();
+ }
await ReadTask;
}
diff --git a/src/Networking.PeerStreaming.Core/PeerStreamManager.cs b/src/Networking.PeerStreaming.Core/PeerStreamManager.cs
index 3fc9e96e..21236126 100644
--- a/src/Networking.PeerStreaming.Core/PeerStreamManager.cs
+++ b/src/Networking.PeerStreaming.Core/PeerStreamManager.cs
@@ -68,4 +68,12 @@ private void Add( IPeerStream stream ) {
logger.LogTrace( "Created {Stream}", stream );
_streams[stream.AgentId] = stream;
}
+
+ public async ValueTask DisposeAsync() {
+ logger.LogDebug( "Disposing peer stream manager" );
+ foreach ( var stream in _streams.Values ) {
+ logger.LogTrace( "Disposing peer stream #{StreamNo}", stream.InstanceNo );
+ await stream.DisposeAsync();
+ }
+ }
}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core/ServiceCollectionExtensions.cs b/src/Networking.PeerStreaming.Core/ServiceCollectionExtensions.cs
index 06993192..4ca96603 100644
--- a/src/Networking.PeerStreaming.Core/ServiceCollectionExtensions.cs
+++ b/src/Networking.PeerStreaming.Core/ServiceCollectionExtensions.cs
@@ -10,11 +10,7 @@ public static void AddPeerStreamingCore(
PeerStreamingOptions options
) {
services.AddSingleton( options );
- services.AddSingleton(
- new PeerMessageEnvelopeConverter(
- new AssemblyScanPeerMessageTypesProvider( options.MessageAssembly )
- )
- );
+ services.AddSingleton();
services.AddScoped();
services.AddScoped();
services.AddScoped();
diff --git a/src/Networking.PeerStreaming.Tests/Helpers/TestPeerMessage.cs b/src/Networking.PeerStreaming.Tests/Helpers/TestPeerMessage.cs
new file mode 100644
index 00000000..30841bcd
--- /dev/null
+++ b/src/Networking.PeerStreaming.Tests/Helpers/TestPeerMessage.cs
@@ -0,0 +1,28 @@
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+using Drift.Networking.PeerStreaming.Core.Abstractions;
+
+namespace Drift.Networking.PeerStreaming.Tests.Helpers;
+
+internal class TestPeerMessage : IPeerMessage {
+ public static string MessageType => "testpeermessage";
+
+ public static JsonTypeInfo JsonInfo => TestPeerMessageJsonContext.Default.TestPeerMessage;
+}
+
+[JsonSerializable( typeof(TestPeerMessage) )]
+internal partial class TestPeerMessageJsonContext : JsonSerializerContext;
+
+internal class TestMessageHandler : IPeerMessageHandler {
+ public TestPeerMessage? LastMessage {
+ get;
+ private set;
+ }
+
+ public string MessageType => TestPeerMessage.MessageType;
+
+ public Task HandleAsync( TestPeerMessage message, CancellationToken cancellationToken = default ) {
+ LastMessage = message;
+ return Task.FromResult( null );
+ }
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Tests/PeerMessageTypesProviderTests.cs b/src/Networking.PeerStreaming.Tests/PeerMessageTypesProviderTests.cs
new file mode 100644
index 00000000..b57426da
--- /dev/null
+++ b/src/Networking.PeerStreaming.Tests/PeerMessageTypesProviderTests.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+using Drift.Networking.PeerStreaming.Core.Abstractions;
+using Drift.Networking.PeerStreaming.Core.Messages;
+using Drift.Networking.PeerStreaming.Tests.Helpers;
+
+namespace Drift.Networking.PeerStreaming.Tests;
+
+internal sealed class PeerMessageTypesProviderTests {
+ /*[Test]
+ public void Test() {
+ var provder = new AssemblyScanPeerMessageTypesProvider( typeof(PeerMessageTypesProviderTests).Assembly );
+
+ Assert.That( provder.Get(), Is.EquivalentTo( [typeof(TestPeerMessage)] ) );
+ }*/
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Tests/PeerStreamManagerTests.cs b/src/Networking.PeerStreaming.Tests/PeerStreamManagerTests.cs
index 9666766f..fb2cc29a 100644
--- a/src/Networking.PeerStreaming.Tests/PeerStreamManagerTests.cs
+++ b/src/Networking.PeerStreaming.Tests/PeerStreamManagerTests.cs
@@ -1,3 +1,5 @@
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
using Drift.Networking.Grpc.Generated;
using Drift.Networking.PeerStreaming.Core;
using Drift.Networking.PeerStreaming.Core.Abstractions;
@@ -27,38 +29,19 @@ public async Task IncomingMessageIsDispatchedToHandler() {
var duplexStreams = callContext.CreateDuplexStreams();
var serverStreams = duplexStreams.Server;
var stream = peerStreamManager.Create( serverStreams.RequestStream, serverStreams.ResponseStream, callContext );
- var converter = new PeerMessageEnvelopeConverter( typeof(TestMessage) );
+ var converter = new PeerMessageEnvelopeConverter();
// Act
var clientStreams = duplexStreams.Client;
- await clientStreams.RequestStream.WriteAsync( converter.ToEnvelope( new TestMessage() ) );
+ await clientStreams.RequestStream.WriteAsync( converter.ToEnvelope( new TestPeerMessage() ) );
await cts.CancelAsync();
await stream.ReadTask;
// Assert
- Assert.That( testMessageHandler._lastMessage, Is.Not.Null );
- Assert.That( testMessageHandler._lastMessage.MessageType, Is.EqualTo( "TestMessageType" ) );
+ Assert.That( testMessageHandler.LastMessage, Is.Not.Null );
+ //Assert.That( testMessageHandler.LastMessage.MessageType, Is.EqualTo( "TestMessageType" ) );
cts.Dispose();
}
-
- internal class TestMessage : IPeerMessage {
- public string MessageType => "TestMessageType";
- }
-
- internal class TestMessageHandler : IPeerMessageHandler {
- internal PeerMessage? _lastMessage;
-
- public string? MessageType => "TestMessageType";
-
- public async Task HandleAsync(
- PeerMessage envelope,
- IPeerMessageEnvelopeConverter envelopeConverter,
- CancellationToken cancellationToken = default
- ) {
- _lastMessage = envelope;
- return null;
- }
- }
}
\ No newline at end of file
From f19d9abf59774b8da947206404d4b984aca7e6d3 Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Mon, 1 Dec 2025 22:38:02 +0100
Subject: [PATCH 10/31] f
---
src/Networking.PeerStreaming.Core/PeerStream.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/Networking.PeerStreaming.Core/PeerStream.cs b/src/Networking.PeerStreaming.Core/PeerStream.cs
index 7e91820f..12b98691 100644
--- a/src/Networking.PeerStreaming.Core/PeerStream.cs
+++ b/src/Networking.PeerStreaming.Core/PeerStream.cs
@@ -76,6 +76,7 @@ private async Task ReadLoopAsync() {
try {
await foreach ( var message in _reader.ReadAllAsync( _cancellationToken ) ) {
try {
+ // TODO ensure this is printed in the output
using var scope = _logger.BeginScope(
new Dictionary { ["RequestId"] = message.CorrelationId ?? "no-id" }
);
From fd16962fd51a542fc06b12b3e6b98d3bfc605657 Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Mon, 1 Dec 2025 22:43:47 +0100
Subject: [PATCH 11/31] f
---
src/Cli.Tests/Commands/AgentCommandTests.cs | 2 +-
src/Cli/Cli.csproj | 8 --------
.../Commands/Agent/Subcommands/Start/AgentStartCommand.cs | 2 +-
.../Helpers/TestPeerMessage.cs | 6 +++---
.../Helpers/TestServerCallContext.cs | 7 ++-----
src/Networking.PeerStreaming.Tests/InboundTests.cs | 8 +++++---
.../Networking.PeerStreaming.Tests.csproj | 1 -
7 files changed, 12 insertions(+), 22 deletions(-)
diff --git a/src/Cli.Tests/Commands/AgentCommandTests.cs b/src/Cli.Tests/Commands/AgentCommandTests.cs
index c96eef4c..aea208c9 100644
--- a/src/Cli.Tests/Commands/AgentCommandTests.cs
+++ b/src/Cli.Tests/Commands/AgentCommandTests.cs
@@ -3,7 +3,7 @@
namespace Drift.Cli.Tests.Commands;
-internal class AgentCommandTests {
+internal sealed class AgentCommandTests {
[CancelAfter( 10000 )]
[Test]
public async Task RespectsCancellationToken() {
diff --git a/src/Cli/Cli.csproj b/src/Cli/Cli.csproj
index cb2a0d64..f69186a1 100644
--- a/src/Cli/Cli.csproj
+++ b/src/Cli/Cli.csproj
@@ -40,10 +40,6 @@
all
-
-
-
-
@@ -52,8 +48,4 @@
-
-
-
-
diff --git a/src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs b/src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs
index 9ae31289..088264eb 100644
--- a/src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs
+++ b/src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs
@@ -64,7 +64,7 @@ void ConfigureServices( IServiceCollection services ) {
}
}
- private AgentId? LoadAgentIdentity() {
+ private static AgentId? LoadAgentIdentity() {
if ( false ) {
return AgentId.New(); // TODO load from file
}
diff --git a/src/Networking.PeerStreaming.Tests/Helpers/TestPeerMessage.cs b/src/Networking.PeerStreaming.Tests/Helpers/TestPeerMessage.cs
index 30841bcd..7e07c394 100644
--- a/src/Networking.PeerStreaming.Tests/Helpers/TestPeerMessage.cs
+++ b/src/Networking.PeerStreaming.Tests/Helpers/TestPeerMessage.cs
@@ -4,16 +4,16 @@
namespace Drift.Networking.PeerStreaming.Tests.Helpers;
-internal class TestPeerMessage : IPeerMessage {
+internal sealed class TestPeerMessage : IPeerMessage {
public static string MessageType => "testpeermessage";
public static JsonTypeInfo JsonInfo => TestPeerMessageJsonContext.Default.TestPeerMessage;
}
[JsonSerializable( typeof(TestPeerMessage) )]
-internal partial class TestPeerMessageJsonContext : JsonSerializerContext;
+internal sealed partial class TestPeerMessageJsonContext : JsonSerializerContext;
-internal class TestMessageHandler : IPeerMessageHandler {
+internal sealed class TestMessageHandler : IPeerMessageHandler {
public TestPeerMessage? LastMessage {
get;
private set;
diff --git a/src/Networking.PeerStreaming.Tests/Helpers/TestServerCallContext.cs b/src/Networking.PeerStreaming.Tests/Helpers/TestServerCallContext.cs
index f4c71991..6774b092 100644
--- a/src/Networking.PeerStreaming.Tests/Helpers/TestServerCallContext.cs
+++ b/src/Networking.PeerStreaming.Tests/Helpers/TestServerCallContext.cs
@@ -8,7 +8,6 @@ internal sealed class TestServerCallContext : ServerCallContext {
private readonly Metadata _responseTrailers;
private readonly AuthContext _authContext;
private readonly Dictionary _userState;
- private WriteOptions? _writeOptions;
public Metadata? ResponseHeaders {
get;
@@ -45,10 +44,8 @@ protected override Status StatusCore {
}
protected override WriteOptions? WriteOptionsCore {
- get => _writeOptions;
- set {
- _writeOptions = value;
- }
+ get;
+ set;
}
protected override AuthContext AuthContextCore => _authContext;
diff --git a/src/Networking.PeerStreaming.Tests/InboundTests.cs b/src/Networking.PeerStreaming.Tests/InboundTests.cs
index 6d5410a3..0476da1f 100644
--- a/src/Networking.PeerStreaming.Tests/InboundTests.cs
+++ b/src/Networking.PeerStreaming.Tests/InboundTests.cs
@@ -57,9 +57,11 @@ public async Task InboundStreamRemainsOpenWhenNotCancelledTest() {
var duplexStreams = callContext.CreateDuplexStreams();
// Act
- var peerStreamTask =
- inboundPeerService.PeerStream( duplexStreams.Server.RequestStream, duplexStreams.Server.ResponseStream,
- callContext );
+ var peerStreamTask = inboundPeerService.PeerStream(
+ duplexStreams.Server.RequestStream,
+ duplexStreams.Server.ResponseStream,
+ callContext
+ );
// Assert
Assert.That( peerStreamTask.IsCompleted, Is.False );
diff --git a/src/Networking.PeerStreaming.Tests/Networking.PeerStreaming.Tests.csproj b/src/Networking.PeerStreaming.Tests/Networking.PeerStreaming.Tests.csproj
index b4ab182a..c4ba5682 100644
--- a/src/Networking.PeerStreaming.Tests/Networking.PeerStreaming.Tests.csproj
+++ b/src/Networking.PeerStreaming.Tests/Networking.PeerStreaming.Tests.csproj
@@ -9,7 +9,6 @@
-
From d6db429009ab2dae55eb612591f84ad43ba1e62b Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Mon, 1 Dec 2025 22:57:06 +0100
Subject: [PATCH 12/31] f
---
.../ServiceCollectionExtensions.cs | 2 +-
.../Subnets/SubnetsRequestHandler.cs | 3 +--
.../IPeerMessageHandler.cs | 24 +++++--------------
.../Messages/PeerMessageDispatcher.cs | 7 +++---
.../PeerStream.cs | 1 +
5 files changed, 12 insertions(+), 25 deletions(-)
diff --git a/src/Agent.PeerProtocol/ServiceCollectionExtensions.cs b/src/Agent.PeerProtocol/ServiceCollectionExtensions.cs
index daa7cf8d..40f82e0c 100644
--- a/src/Agent.PeerProtocol/ServiceCollectionExtensions.cs
+++ b/src/Agent.PeerProtocol/ServiceCollectionExtensions.cs
@@ -18,7 +18,7 @@ private static IServiceCollection AddPeerMessageHandler( t
where THandler : class, IPeerMessageHandler
where TReq : IPeerMessage
where TRes : IPeerMessage {
- services.AddScoped();
+ services.AddScoped();
services.AddScoped, THandler>();
return services;
}
diff --git a/src/Agent.PeerProtocol/Subnets/SubnetsRequestHandler.cs b/src/Agent.PeerProtocol/Subnets/SubnetsRequestHandler.cs
index 0b883d37..464305b1 100644
--- a/src/Agent.PeerProtocol/Subnets/SubnetsRequestHandler.cs
+++ b/src/Agent.PeerProtocol/Subnets/SubnetsRequestHandler.cs
@@ -19,10 +19,9 @@ ILogger logger
logger.LogInformation( "Handling subnet request" );
var subnets = await interfaceSubnetProvider.GetAsync();
- var response = new SubnetsResponse { Subnets = subnets };
logger.LogInformation( "Sending subnets: {Subnets}", string.Join( ", ", subnets ) );
- return response;
+ return new SubnetsResponse { Subnets = subnets };
}
}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageHandler.cs b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageHandler.cs
index 945deb35..61f9aa92 100644
--- a/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageHandler.cs
+++ b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageHandler.cs
@@ -14,47 +14,35 @@ string MessageType {
);
}*/
-public interface IPeerMessageHandlerBase {
+public interface IPeerMessageHandler {
string MessageType {
get;
}
- Task DispatchAsync(
+ Task HandleAsync(
PeerMessage envelope,
IPeerMessageEnvelopeConverter converter,
CancellationToken cancellationToken
);
}
-public interface IPeerMessageHandler : IPeerMessageHandlerBase
+public interface IPeerMessageHandler : IPeerMessageHandler
where TRequest : IPeerMessage
where TResponse : IPeerMessage {
- // TODO unused now
- async Task HandleAsync(
- PeerMessage envelope,
- IPeerMessageEnvelopeConverter envelopeConverter,
- CancellationToken cancellationToken
- ) {
- var typedMessage = envelopeConverter.FromEnvelope( envelope );
- return await HandleAsync( typedMessage, cancellationToken );
- }
-
Task HandleAsync( TRequest message, CancellationToken cancellationToken = default );
- async Task IPeerMessageHandlerBase.DispatchAsync(
+ async Task IPeerMessageHandler.HandleAsync(
PeerMessage envelope,
IPeerMessageEnvelopeConverter converter,
CancellationToken cancellationToken ) {
- // Deserialize strongly typed request
var request = converter.FromEnvelope( envelope );
- // Process request
var response = await HandleAsync( request, cancellationToken );
- if ( response is null )
+ if ( response is null ) {
return null;
+ }
- // Serialize strongly typed response
return converter.ToEnvelope( response );
}
}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core/Messages/PeerMessageDispatcher.cs b/src/Networking.PeerStreaming.Core/Messages/PeerMessageDispatcher.cs
index 06640bcd..1833fb22 100644
--- a/src/Networking.PeerStreaming.Core/Messages/PeerMessageDispatcher.cs
+++ b/src/Networking.PeerStreaming.Core/Messages/PeerMessageDispatcher.cs
@@ -8,10 +8,10 @@ public sealed class PeerMessageDispatcher {
private readonly PeerResponseCorrelator _responseCorrelator;
private readonly IPeerMessageEnvelopeConverter _envelopeConverter;
private readonly ILogger _logger;
- private readonly Dictionary _handlers;
+ private readonly Dictionary _handlers;
public PeerMessageDispatcher(
- IEnumerable handlers,
+ IEnumerable handlers,
IPeerMessageEnvelopeConverter envelopeConverter,
PeerResponseCorrelator responseCorrelator,
ILogger logger
@@ -39,10 +39,9 @@ ILogger logger
// Otherwise, dispatch to handler
if ( _handlers.TryGetValue( message.MessageType, out var handler ) ) {
- var responseEnvelope = await handler.DispatchAsync( message, _envelopeConverter, ct );
+ var responseEnvelope = await handler.HandleAsync( message, _envelopeConverter, ct );
if ( responseEnvelope != null ) {
- //var responseEnvelope = _envelopeConverter.ToEnvelope( response );
responseEnvelope.ReplyTo = message.CorrelationId;
await peerStream.SendAsync( responseEnvelope );
}
diff --git a/src/Networking.PeerStreaming.Core/PeerStream.cs b/src/Networking.PeerStreaming.Core/PeerStream.cs
index 12b98691..aa6484d7 100644
--- a/src/Networking.PeerStreaming.Core/PeerStream.cs
+++ b/src/Networking.PeerStreaming.Core/PeerStream.cs
@@ -82,6 +82,7 @@ private async Task ReadLoopAsync() {
);
_logger.LogDebug( "Received message. Dispatching to handler..." );
await _dispatcher.DispatchAsync( message, this, CancellationToken.None );
+ _logger.LogDebug( "Dispatch completed. Waiting for next message..." );
}
catch ( Exception ex ) {
_logger.LogError( ex, "Message dispatch failed" );
From 10e194859b8205e3b38cf35477d0ef4aba858cc8 Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Tue, 2 Dec 2025 22:20:19 +0100
Subject: [PATCH 13/31] tests and refactor
---
Drift.sln | 6 +
src/Agent.Hosting/AgentHost.cs | 1 -
.../Agent.PeerProtocol.Tests.csproj | 19 +++
.../PeerMessageHandlerTests.cs | 114 ++++++++++++++++++
.../Adopt/AdoptRequestHandler.cs | 3 +-
.../Adopt/AdoptRequestPayload.cs | 2 +-
.../Adopt/IAdoptRequestHandler.cs | 7 --
.../{Adopt => }/NullResponse.cs | 4 +-
.../PeerProtocolJsonContext.cs | 10 --
.../PeerProtocolTypesProvider.cs | 12 --
.../ServiceCollectionExtensions.cs | 20 +--
.../Subnets/SubnetsRequest.cs | 6 +-
.../Subnets/SubnetsRequestHandler.cs | 2 -
.../Subnets/SubnetsResponse.cs | 6 +-
src/Cli/Cli.csproj | 4 +-
src/Cli/Commands/Common/CommonParameters.cs | 1 -
src/Networking.Clustering/Cluster.cs | 8 +-
src/Networking.Clustering/ICluster.cs | 6 +-
.../IPeerMessage.cs | 6 +-
.../IPeerMessageHandler.cs | 19 +--
.../Messages/PeerMessageEnvelopeConverter.cs | 13 +-
.../Networking.PeerStreaming.Grpc.csproj | 4 +-
.../Helpers/TestPeerMessage.cs | 2 +-
.../PeerMessageTypesProviderTests.cs | 16 ---
.../PeerStreamManagerTests.cs | 4 -
25 files changed, 174 insertions(+), 121 deletions(-)
create mode 100644 src/Agent.PeerProtocol.Tests/Agent.PeerProtocol.Tests.csproj
create mode 100644 src/Agent.PeerProtocol.Tests/PeerMessageHandlerTests.cs
delete mode 100644 src/Agent.PeerProtocol/Adopt/IAdoptRequestHandler.cs
rename src/Agent.PeerProtocol/{Adopt => }/NullResponse.cs (70%)
delete mode 100644 src/Agent.PeerProtocol/PeerProtocolJsonContext.cs
delete mode 100644 src/Agent.PeerProtocol/PeerProtocolTypesProvider.cs
delete mode 100644 src/Networking.PeerStreaming.Tests/PeerMessageTypesProviderTests.cs
diff --git a/Drift.sln b/Drift.sln
index fe88c857..b9866062 100644
--- a/Drift.sln
+++ b/Drift.sln
@@ -97,6 +97,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FeatureFlagsDELETE", "src\F
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FeatureFlagsDELETE.Tests", "src\FeatureFlagsDELETE.Tests\FeatureFlagsDELETE.Tests.csproj", "{24EA16E4-A73B-42F5-B315-FB9AE1B3F9AF}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agent.PeerProtocol.Tests", "src\Agent.PeerProtocol.Tests\Agent.PeerProtocol.Tests.csproj", "{C4576156-BD24-463F-88F2-8A4378855BCC}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -245,6 +247,10 @@ Global
{24EA16E4-A73B-42F5-B315-FB9AE1B3F9AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{24EA16E4-A73B-42F5-B315-FB9AE1B3F9AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{24EA16E4-A73B-42F5-B315-FB9AE1B3F9AF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C4576156-BD24-463F-88F2-8A4378855BCC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C4576156-BD24-463F-88F2-8A4378855BCC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C4576156-BD24-463F-88F2-8A4378855BCC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C4576156-BD24-463F-88F2-8A4378855BCC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{8523E9E0-F412-41B7-B361-ADE639FFAF24} = {C0698EF0-61C8-403E-8E93-1F1D34C5B910}
diff --git a/src/Agent.Hosting/AgentHost.cs b/src/Agent.Hosting/AgentHost.cs
index 7a9eced8..aee866ec 100644
--- a/src/Agent.Hosting/AgentHost.cs
+++ b/src/Agent.Hosting/AgentHost.cs
@@ -50,7 +50,6 @@ private static WebApplication Build(
peerStreamingOptions.StoppingToken = app.Lifetime.ApplicationStopping;
app.MapPeerStreamingServerEndpoints();
- app.MapGet( "/", () => "Nothing to see here" );
app.Lifetime.ApplicationStarted.Register( () => {
logger.LogInformation( "Listening for incoming connections on port {Port}", port );
diff --git a/src/Agent.PeerProtocol.Tests/Agent.PeerProtocol.Tests.csproj b/src/Agent.PeerProtocol.Tests/Agent.PeerProtocol.Tests.csproj
new file mode 100644
index 00000000..e00ea9c0
--- /dev/null
+++ b/src/Agent.PeerProtocol.Tests/Agent.PeerProtocol.Tests.csproj
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Agent.PeerProtocol.Tests/PeerMessageHandlerTests.cs b/src/Agent.PeerProtocol.Tests/PeerMessageHandlerTests.cs
new file mode 100644
index 00000000..235ba1de
--- /dev/null
+++ b/src/Agent.PeerProtocol.Tests/PeerMessageHandlerTests.cs
@@ -0,0 +1,114 @@
+using System.Reflection;
+using Drift.Networking.PeerStreaming.Core.Abstractions;
+
+namespace Drift.Agent.PeerProtocol.Tests;
+
+internal sealed class PeerMessageHandlerTests {
+ private static readonly Assembly ProtocolAssembly = typeof(PeerProtocolAssemblyMarker).Assembly;
+ private static readonly IEnumerable RequestTypes = GetAllConcreteMessageTypes( typeof(IPeerRequestMessage) );
+ private static readonly IEnumerable ResponseTypes = GetAllConcreteMessageTypes( typeof(IPeerResponseMessage) );
+ private static readonly IEnumerable HandlerTypes = GetAllConcreteHandlerTypes();
+
+ [Test]
+ public void FindMessagesAndHandlersAndMessages() {
+ Assert.That( RequestTypes.ToList(), Has.Count.GreaterThan( 1 ), "No request messages found via reflection" );
+ Assert.That( ResponseTypes.ToList(), Has.Count.GreaterThan( 1 ), "No response messages found via reflection" );
+ Assert.That( HandlerTypes.ToList(), Has.Count.GreaterThan( 1 ), "No handlers found via reflection" );
+ }
+
+ [Test]
+ public void AllRequestMessagesHaveHandlers_AndNoExtraHandlers() {
+ var handledRequestTypes = HandlerTypes
+ .Select( t => t.GetInterfaces()
+ .First( i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IPeerMessageHandler<,>) )
+ .GetGenericArguments()[0] // 1st generic parameter = TRequest
+ )
+ .ToList();
+
+ var requestsWithoutHandler = RequestTypes
+ .Except( handledRequestTypes )
+ .Select( t => t.Name )
+ .ToList();
+
+ var extraHandlers = handledRequestTypes
+ .Except( RequestTypes )
+ .Select( t => t.Name )
+ .ToList();
+
+ Assert.That(
+ requestsWithoutHandler,
+ Is.Empty,
+ "Request messages without a handler: " + string.Join( ", ", requestsWithoutHandler )
+ );
+
+ Assert.That(
+ extraHandlers,
+ Is.Empty,
+ "Handlers for unknown request messages: " + string.Join( ", ", extraHandlers )
+ );
+ }
+
+ [Test]
+ public void AllResponseMessagesHaveHandlers_AndNoExtraHandlers() {
+ var handledResponseTypes = HandlerTypes
+ .Select( t => t.GetInterfaces()
+ .First( i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IPeerMessageHandler<,>) )
+ .GetGenericArguments()[1] ) // 2nd generic parameter = TResponse
+ .ToList();
+
+ var responsesWithoutHandler = ResponseTypes
+ .Except( handledResponseTypes )
+ .Select( t => t.Name )
+ .ToList();
+
+ var extraHandlers = handledResponseTypes
+ .Except( ResponseTypes )
+ .Select( t => t.Name )
+ .ToList();
+
+ Assert.That(
+ responsesWithoutHandler,
+ Is.Empty,
+ "Response messages without a handler: " + string.Join( ", ", responsesWithoutHandler )
+ );
+
+ Assert.That(
+ extraHandlers,
+ Is.Empty,
+ "Handlers for unknown response messages: " + string.Join( ", ", extraHandlers )
+ );
+ }
+
+ [TestCaseSource( nameof(RequestTypes) )]
+ [TestCaseSource( nameof(ResponseTypes) )]
+ public void Messages_HaveValidMessageTypeAndJsonInfo( Type type ) {
+ var messageTypeValue = type
+ .GetProperty( nameof(IPeerMessage.MessageType), BindingFlags.Public | BindingFlags.Static )!
+ .GetValue( null ) as string;
+
+ Assert.That( messageTypeValue, Is.Not.Null.And.Not.Empty );
+
+ var jsonInfoValue = type
+ .GetProperty( nameof(IPeerMessage.JsonInfo), BindingFlags.Public | BindingFlags.Static )!
+ .GetValue( null );
+
+ Assert.That( jsonInfoValue, Is.Not.Null );
+ }
+
+ private static List GetAllConcreteMessageTypes( Type baseType ) {
+ return ProtocolAssembly
+ .GetTypes()
+ .Where( t => t is { IsAbstract: false, IsInterface: false } )
+ .Where( baseType.IsAssignableFrom )
+ .ToList();
+ }
+
+ private static List GetAllConcreteHandlerTypes() {
+ return ProtocolAssembly
+ .GetTypes()
+ .Where( t => t is { IsAbstract: false, IsInterface: false } )
+ .Where( t => t.GetInterfaces()
+ .Any( i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IPeerMessageHandler<,>) ) )
+ .ToList();
+ }
+}
\ No newline at end of file
diff --git a/src/Agent.PeerProtocol/Adopt/AdoptRequestHandler.cs b/src/Agent.PeerProtocol/Adopt/AdoptRequestHandler.cs
index 635bb5cb..4a4b8c27 100644
--- a/src/Agent.PeerProtocol/Adopt/AdoptRequestHandler.cs
+++ b/src/Agent.PeerProtocol/Adopt/AdoptRequestHandler.cs
@@ -1,4 +1,3 @@
-using Drift.Agent.PeerProtocol.Subnets;
using Drift.Networking.PeerStreaming.Core.Abstractions;
using Microsoft.Extensions.Logging;
@@ -7,7 +6,7 @@ namespace Drift.Agent.PeerProtocol.Adopt;
internal sealed class AdoptRequestHandler : IPeerMessageHandler {
private readonly ILogger _logger; // Example: inject what you need
- public string MessageType => "adopt-request";
+ public string MessageType => AdoptRequestPayload.MessageType;
public async Task HandleAsync( AdoptRequestPayload message,
CancellationToken cancellationToken = default ) {
diff --git a/src/Agent.PeerProtocol/Adopt/AdoptRequestPayload.cs b/src/Agent.PeerProtocol/Adopt/AdoptRequestPayload.cs
index d5348c6b..24d46c5f 100644
--- a/src/Agent.PeerProtocol/Adopt/AdoptRequestPayload.cs
+++ b/src/Agent.PeerProtocol/Adopt/AdoptRequestPayload.cs
@@ -3,7 +3,7 @@
namespace Drift.Agent.PeerProtocol.Adopt;
-internal sealed class AdoptRequestPayload : IPeerMessage {
+internal sealed class AdoptRequestPayload : IPeerRequestMessage {
public static string MessageType => "adopt-request";
public string Jwt {
diff --git a/src/Agent.PeerProtocol/Adopt/IAdoptRequestHandler.cs b/src/Agent.PeerProtocol/Adopt/IAdoptRequestHandler.cs
deleted file mode 100644
index 0a83dfa3..00000000
--- a/src/Agent.PeerProtocol/Adopt/IAdoptRequestHandler.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Drift.Agent.PeerProtocol.Adopt;
-
-internal interface IAdoptRequestHandler {
- public string MessageType => "adopt-request";
-
- Task HandleAsync( AdoptRequestPayload message, CancellationToken cancellationToken = default );
-}
\ No newline at end of file
diff --git a/src/Agent.PeerProtocol/Adopt/NullResponse.cs b/src/Agent.PeerProtocol/NullResponse.cs
similarity index 70%
rename from src/Agent.PeerProtocol/Adopt/NullResponse.cs
rename to src/Agent.PeerProtocol/NullResponse.cs
index ae23a1c3..9ae4252e 100644
--- a/src/Agent.PeerProtocol/Adopt/NullResponse.cs
+++ b/src/Agent.PeerProtocol/NullResponse.cs
@@ -1,9 +1,9 @@
using System.Text.Json.Serialization.Metadata;
using Drift.Networking.PeerStreaming.Core.Abstractions;
-namespace Drift.Agent.PeerProtocol.Adopt;
+namespace Drift.Agent.PeerProtocol;
-public class NullResponse : IPeerMessage {
+public class NullResponse : IPeerResponseMessage {
public static string MessageType {
get;
}
diff --git a/src/Agent.PeerProtocol/PeerProtocolJsonContext.cs b/src/Agent.PeerProtocol/PeerProtocolJsonContext.cs
deleted file mode 100644
index 19d20e2e..00000000
--- a/src/Agent.PeerProtocol/PeerProtocolJsonContext.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System.Text.Json.Serialization;
-using Drift.Agent.PeerProtocol.Subnets;
-
-namespace Drift.Agent.PeerProtocol;
-
-/*
-[JsonSerializable( typeof(SubnetsRequest) )]
-[JsonSerializable( typeof(SubnetsResponse) )]
-internal partial class PeerProtocolJsonContext : JsonSerializerContext {
-}*/
\ No newline at end of file
diff --git a/src/Agent.PeerProtocol/PeerProtocolTypesProvider.cs b/src/Agent.PeerProtocol/PeerProtocolTypesProvider.cs
deleted file mode 100644
index 093e0519..00000000
--- a/src/Agent.PeerProtocol/PeerProtocolTypesProvider.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System.Text.Json.Serialization.Metadata;
-using Drift.Networking.PeerStreaming.Core.Abstractions;
-
-namespace Drift.Agent.PeerProtocol;
-
-internal sealed class PeerProtocolTypesProvider : IPeerMessageTypesProvider {
- internal static readonly Dictionary Map = new();
-
- public Dictionary Get() {
- return Map;
- }
-}
\ No newline at end of file
diff --git a/src/Agent.PeerProtocol/ServiceCollectionExtensions.cs b/src/Agent.PeerProtocol/ServiceCollectionExtensions.cs
index 40f82e0c..41baf7a1 100644
--- a/src/Agent.PeerProtocol/ServiceCollectionExtensions.cs
+++ b/src/Agent.PeerProtocol/ServiceCollectionExtensions.cs
@@ -5,21 +5,9 @@
namespace Drift.Agent.PeerProtocol;
public static class ServiceCollectionExtensions {
- public static void AddPeerProtocol( this IServiceCollection services ) {
- //TODO need both?
- // services.AddScoped();
- // services.AddScoped, SubnetsRequestHandler>();
- services.AddPeerMessageHandler();
-
- //services.AddScoped();
- }
-
- private static IServiceCollection AddPeerMessageHandler( this IServiceCollection services )
- where THandler : class, IPeerMessageHandler
- where TReq : IPeerMessage
- where TRes : IPeerMessage {
- services.AddScoped();
- services.AddScoped, THandler>();
- return services;
+ extension( IServiceCollection services ) {
+ public void AddPeerProtocol() {
+ services.AddScoped();
+ }
}
}
\ No newline at end of file
diff --git a/src/Agent.PeerProtocol/Subnets/SubnetsRequest.cs b/src/Agent.PeerProtocol/Subnets/SubnetsRequest.cs
index 6be84052..c143c54e 100644
--- a/src/Agent.PeerProtocol/Subnets/SubnetsRequest.cs
+++ b/src/Agent.PeerProtocol/Subnets/SubnetsRequest.cs
@@ -4,11 +4,7 @@
namespace Drift.Agent.PeerProtocol.Subnets;
-public sealed class SubnetsRequest : IPeerMessage {
- static SubnetsRequest() {
- PeerProtocolTypesProvider.Map[MessageType] = JsonInfo;
- }
-
+public sealed class SubnetsRequest : IPeerRequestMessage {
public static string MessageType => "subnetsrequest";
public static JsonTypeInfo JsonInfo => SubnetsRequestJsonContext.Default.SubnetsRequest;
diff --git a/src/Agent.PeerProtocol/Subnets/SubnetsRequestHandler.cs b/src/Agent.PeerProtocol/Subnets/SubnetsRequestHandler.cs
index 464305b1..68c88d86 100644
--- a/src/Agent.PeerProtocol/Subnets/SubnetsRequestHandler.cs
+++ b/src/Agent.PeerProtocol/Subnets/SubnetsRequestHandler.cs
@@ -1,5 +1,3 @@
-using System.Collections;
-using Drift.Networking.Grpc.Generated;
using Drift.Networking.PeerStreaming.Core.Abstractions;
using Drift.Scanning.Subnets.Interface;
using Microsoft.Extensions.Logging;
diff --git a/src/Agent.PeerProtocol/Subnets/SubnetsResponse.cs b/src/Agent.PeerProtocol/Subnets/SubnetsResponse.cs
index d28c3380..f42c26d4 100644
--- a/src/Agent.PeerProtocol/Subnets/SubnetsResponse.cs
+++ b/src/Agent.PeerProtocol/Subnets/SubnetsResponse.cs
@@ -6,11 +6,7 @@
namespace Drift.Agent.PeerProtocol.Subnets;
-public sealed class SubnetsResponse : IPeerMessage {
- static SubnetsResponse() {
- PeerProtocolTypesProvider.Map[MessageType] = JsonInfo;
- }
-
+public sealed class SubnetsResponse : IPeerResponseMessage {
public static string MessageType => "subnetsresponse";
public required IReadOnlyList Subnets {
diff --git a/src/Cli/Cli.csproj b/src/Cli/Cli.csproj
index f69186a1..31d53c6b 100644
--- a/src/Cli/Cli.csproj
+++ b/src/Cli/Cli.csproj
@@ -37,9 +37,7 @@
-
- all
-
+
diff --git a/src/Cli/Commands/Common/CommonParameters.cs b/src/Cli/Commands/Common/CommonParameters.cs
index d574e9f6..488b3183 100644
--- a/src/Cli/Commands/Common/CommonParameters.cs
+++ b/src/Cli/Commands/Common/CommonParameters.cs
@@ -1,7 +1,6 @@
using System.CommandLine;
using Drift.Cli.Presentation.Console;
using Drift.Cli.Settings.V1_preview;
-using Microsoft.Extensions.Logging;
namespace Drift.Cli.Commands.Common;
diff --git a/src/Networking.Clustering/Cluster.cs b/src/Networking.Clustering/Cluster.cs
index 475ede31..b7588124 100644
--- a/src/Networking.Clustering/Cluster.cs
+++ b/src/Networking.Clustering/Cluster.cs
@@ -49,14 +49,14 @@ ILogger logger
await connection.SendAsync( envelope );
}*/
- public async Task SendAndWaitAsync(
+ public async Task SendAndWaitAsync(
Domain.Agent agent,
- TReq message,
+ TRequest message,
TimeSpan? timeout = null,
CancellationToken cancellationToken = default
- ) where TResponse : IPeerMessage where TReq : IPeerMessage {
+ ) where TResponse : IPeerResponseMessage where TRequest : IPeerRequestMessage {
var correlationId = Guid.NewGuid().ToString();
- var envelope = envelopeConverter.ToEnvelope( message );
+ var envelope = envelopeConverter.ToEnvelope( message );
envelope.CorrelationId = correlationId;
// Register correlator BEFORE sending
diff --git a/src/Networking.Clustering/ICluster.cs b/src/Networking.Clustering/ICluster.cs
index 0bb79136..f856bfe3 100644
--- a/src/Networking.Clustering/ICluster.cs
+++ b/src/Networking.Clustering/ICluster.cs
@@ -5,12 +5,12 @@ namespace Drift.Networking.Clustering;
public interface ICluster {
//Task SendAsync( Domain.Agent agent, IPeerMessage message, CancellationToken cancellationToken = default );
- Task SendAndWaitAsync(
+ Task SendAndWaitAsync(
Domain.Agent agent,
- TReq message,
+ TRequest message,
TimeSpan? timeout = null,
CancellationToken cancellationToken = default
- ) where TResponse : IPeerMessage where TReq : IPeerMessage;
+ ) where TResponse : IPeerResponseMessage where TRequest : IPeerRequestMessage;
/*Task BroadcastAsync( PeerMessage message, CancellationToken cancellationToken = default );
Task> RequestSubnetsAsync( string peerAddress, CancellationToken cancellationToken = default );
diff --git a/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessage.cs b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessage.cs
index 5bb4e451..db14c364 100644
--- a/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessage.cs
+++ b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessage.cs
@@ -10,4 +10,8 @@ static abstract string MessageType {
static abstract JsonTypeInfo JsonInfo {
get;
}
-}
\ No newline at end of file
+}
+
+public interface IPeerRequestMessage : IPeerMessage;
+
+public interface IPeerResponseMessage : IPeerMessage;
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageHandler.cs b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageHandler.cs
index 61f9aa92..efaa427e 100644
--- a/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageHandler.cs
+++ b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageHandler.cs
@@ -2,19 +2,10 @@
namespace Drift.Networking.PeerStreaming.Core.Abstractions;
-/*public interface IPeerMessageHandler {
- string MessageType {
- get;
- }
-
- Task HandleAsync(
- PeerMessage envelope,
- IPeerMessageEnvelopeConverter envelopeConverter,
- CancellationToken cancellationToken = default
- );
-}*/
-
public interface IPeerMessageHandler {
+ ///
+ /// Gets the message type name that this handler can process.
+ ///
string MessageType {
get;
}
@@ -27,8 +18,8 @@ CancellationToken cancellationToken
}
public interface IPeerMessageHandler : IPeerMessageHandler
- where TRequest : IPeerMessage
- where TResponse : IPeerMessage {
+ where TRequest : IPeerRequestMessage
+ where TResponse : IPeerResponseMessage {
Task HandleAsync( TRequest message, CancellationToken cancellationToken = default );
async Task IPeerMessageHandler.HandleAsync(
diff --git a/src/Networking.PeerStreaming.Core/Messages/PeerMessageEnvelopeConverter.cs b/src/Networking.PeerStreaming.Core/Messages/PeerMessageEnvelopeConverter.cs
index 2a56ed2e..3b9a4467 100644
--- a/src/Networking.PeerStreaming.Core/Messages/PeerMessageEnvelopeConverter.cs
+++ b/src/Networking.PeerStreaming.Core/Messages/PeerMessageEnvelopeConverter.cs
@@ -1,25 +1,22 @@
using System.Text.Json;
-using System.Text.Json.Serialization.Metadata;
using Drift.Networking.Grpc.Generated;
using Drift.Networking.PeerStreaming.Core.Abstractions;
-using Drift.Serialization.Converters;
namespace Drift.Networking.PeerStreaming.Core.Messages;
internal sealed class PeerMessageEnvelopeConverter : IPeerMessageEnvelopeConverter {
- public PeerMessage ToEnvelope( IPeerMessage message, string? requestId = null )
- where T : IPeerMessage {
+ public PeerMessage ToEnvelope( IPeerMessage message, string? requestId = null ) where T : IPeerMessage {
string json = JsonSerializer.Serialize( message, T.JsonInfo );
return new PeerMessage { MessageType = T.MessageType, Message = json, };
}
- public TSelf FromEnvelope( PeerMessage envelope ) where TSelf : IPeerMessage {
- if ( envelope.MessageType != TSelf.MessageType ) {
+ public T FromEnvelope( PeerMessage envelope ) where T : IPeerMessage {
+ if ( envelope.MessageType != T.MessageType ) {
throw new InvalidOperationException(
- $"Envelope contains '{envelope.MessageType}' but caller expects '{TSelf.MessageType}'."
+ $"Envelope contains '{envelope.MessageType}' but caller expects '{T.MessageType}'."
);
}
- return JsonSerializer.Deserialize( envelope.Message, TSelf.JsonInfo.Options )!;
+ return JsonSerializer.Deserialize( envelope.Message, T.JsonInfo.Options )!;
}
}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Grpc/Networking.PeerStreaming.Grpc.csproj b/src/Networking.PeerStreaming.Grpc/Networking.PeerStreaming.Grpc.csproj
index c510d63c..5253f1c8 100644
--- a/src/Networking.PeerStreaming.Grpc/Networking.PeerStreaming.Grpc.csproj
+++ b/src/Networking.PeerStreaming.Grpc/Networking.PeerStreaming.Grpc.csproj
@@ -7,9 +7,7 @@
-
- all
-
+
diff --git a/src/Networking.PeerStreaming.Tests/Helpers/TestPeerMessage.cs b/src/Networking.PeerStreaming.Tests/Helpers/TestPeerMessage.cs
index 7e07c394..b5af1dfa 100644
--- a/src/Networking.PeerStreaming.Tests/Helpers/TestPeerMessage.cs
+++ b/src/Networking.PeerStreaming.Tests/Helpers/TestPeerMessage.cs
@@ -4,7 +4,7 @@
namespace Drift.Networking.PeerStreaming.Tests.Helpers;
-internal sealed class TestPeerMessage : IPeerMessage {
+internal sealed class TestPeerMessage : IPeerRequestMessage, IPeerResponseMessage {
public static string MessageType => "testpeermessage";
public static JsonTypeInfo JsonInfo => TestPeerMessageJsonContext.Default.TestPeerMessage;
diff --git a/src/Networking.PeerStreaming.Tests/PeerMessageTypesProviderTests.cs b/src/Networking.PeerStreaming.Tests/PeerMessageTypesProviderTests.cs
deleted file mode 100644
index b57426da..00000000
--- a/src/Networking.PeerStreaming.Tests/PeerMessageTypesProviderTests.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System.Text.Json.Serialization;
-using System.Text.Json.Serialization.Metadata;
-using Drift.Networking.PeerStreaming.Core.Abstractions;
-using Drift.Networking.PeerStreaming.Core.Messages;
-using Drift.Networking.PeerStreaming.Tests.Helpers;
-
-namespace Drift.Networking.PeerStreaming.Tests;
-
-internal sealed class PeerMessageTypesProviderTests {
- /*[Test]
- public void Test() {
- var provder = new AssemblyScanPeerMessageTypesProvider( typeof(PeerMessageTypesProviderTests).Assembly );
-
- Assert.That( provder.Get(), Is.EquivalentTo( [typeof(TestPeerMessage)] ) );
- }*/
-}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Tests/PeerStreamManagerTests.cs b/src/Networking.PeerStreaming.Tests/PeerStreamManagerTests.cs
index fb2cc29a..d1c3ca11 100644
--- a/src/Networking.PeerStreaming.Tests/PeerStreamManagerTests.cs
+++ b/src/Networking.PeerStreaming.Tests/PeerStreamManagerTests.cs
@@ -1,8 +1,4 @@
-using System.Text.Json.Serialization;
-using System.Text.Json.Serialization.Metadata;
-using Drift.Networking.Grpc.Generated;
using Drift.Networking.PeerStreaming.Core;
-using Drift.Networking.PeerStreaming.Core.Abstractions;
using Drift.Networking.PeerStreaming.Core.Messages;
using Drift.Networking.PeerStreaming.Tests.Helpers;
using Drift.TestUtilities;
From f892fbb43beb4891e370d8479c18e926ab8d7349 Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Tue, 2 Dec 2025 22:36:57 +0100
Subject: [PATCH 14/31] f
---
src/Agent.PeerProtocol.Tests/PeerMessageHandlerTests.cs | 2 +-
src/Agent.PeerProtocol/Adopt/AdoptRequestPayload.cs | 2 +-
src/Agent.PeerProtocol/Subnets/SubnetsRequest.cs | 2 +-
src/Networking.Clustering/Cluster.cs | 2 +-
src/Networking.Clustering/ICluster.cs | 2 +-
.../IPeerMessage.cs | 2 +-
.../IPeerMessageHandler.cs | 2 +-
src/Networking.PeerStreaming.Tests/Helpers/TestPeerMessage.cs | 4 ++--
8 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/src/Agent.PeerProtocol.Tests/PeerMessageHandlerTests.cs b/src/Agent.PeerProtocol.Tests/PeerMessageHandlerTests.cs
index 235ba1de..27e11534 100644
--- a/src/Agent.PeerProtocol.Tests/PeerMessageHandlerTests.cs
+++ b/src/Agent.PeerProtocol.Tests/PeerMessageHandlerTests.cs
@@ -5,7 +5,7 @@ namespace Drift.Agent.PeerProtocol.Tests;
internal sealed class PeerMessageHandlerTests {
private static readonly Assembly ProtocolAssembly = typeof(PeerProtocolAssemblyMarker).Assembly;
- private static readonly IEnumerable RequestTypes = GetAllConcreteMessageTypes( typeof(IPeerRequestMessage) );
+ private static readonly IEnumerable RequestTypes = GetAllConcreteMessageTypes( typeof(IPeerRequestMessage<>) );
private static readonly IEnumerable ResponseTypes = GetAllConcreteMessageTypes( typeof(IPeerResponseMessage) );
private static readonly IEnumerable HandlerTypes = GetAllConcreteHandlerTypes();
diff --git a/src/Agent.PeerProtocol/Adopt/AdoptRequestPayload.cs b/src/Agent.PeerProtocol/Adopt/AdoptRequestPayload.cs
index 24d46c5f..9854c08c 100644
--- a/src/Agent.PeerProtocol/Adopt/AdoptRequestPayload.cs
+++ b/src/Agent.PeerProtocol/Adopt/AdoptRequestPayload.cs
@@ -3,7 +3,7 @@
namespace Drift.Agent.PeerProtocol.Adopt;
-internal sealed class AdoptRequestPayload : IPeerRequestMessage {
+internal sealed class AdoptRequestPayload : IPeerRequestMessage {
public static string MessageType => "adopt-request";
public string Jwt {
diff --git a/src/Agent.PeerProtocol/Subnets/SubnetsRequest.cs b/src/Agent.PeerProtocol/Subnets/SubnetsRequest.cs
index c143c54e..291d46e8 100644
--- a/src/Agent.PeerProtocol/Subnets/SubnetsRequest.cs
+++ b/src/Agent.PeerProtocol/Subnets/SubnetsRequest.cs
@@ -4,7 +4,7 @@
namespace Drift.Agent.PeerProtocol.Subnets;
-public sealed class SubnetsRequest : IPeerRequestMessage {
+public sealed class SubnetsRequest : IPeerRequestMessage {
public static string MessageType => "subnetsrequest";
public static JsonTypeInfo JsonInfo => SubnetsRequestJsonContext.Default.SubnetsRequest;
diff --git a/src/Networking.Clustering/Cluster.cs b/src/Networking.Clustering/Cluster.cs
index b7588124..997b9858 100644
--- a/src/Networking.Clustering/Cluster.cs
+++ b/src/Networking.Clustering/Cluster.cs
@@ -54,7 +54,7 @@ public async Task SendAndWaitAsync(
TRequest message,
TimeSpan? timeout = null,
CancellationToken cancellationToken = default
- ) where TResponse : IPeerResponseMessage where TRequest : IPeerRequestMessage {
+ ) where TResponse : IPeerResponseMessage where TRequest : IPeerRequestMessage {
var correlationId = Guid.NewGuid().ToString();
var envelope = envelopeConverter.ToEnvelope( message );
envelope.CorrelationId = correlationId;
diff --git a/src/Networking.Clustering/ICluster.cs b/src/Networking.Clustering/ICluster.cs
index f856bfe3..d9fdec97 100644
--- a/src/Networking.Clustering/ICluster.cs
+++ b/src/Networking.Clustering/ICluster.cs
@@ -10,7 +10,7 @@ Task SendAndWaitAsync(
TRequest message,
TimeSpan? timeout = null,
CancellationToken cancellationToken = default
- ) where TResponse : IPeerResponseMessage where TRequest : IPeerRequestMessage;
+ ) where TResponse : IPeerResponseMessage where TRequest : IPeerRequestMessage;
/*Task BroadcastAsync( PeerMessage message, CancellationToken cancellationToken = default );
Task> RequestSubnetsAsync( string peerAddress, CancellationToken cancellationToken = default );
diff --git a/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessage.cs b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessage.cs
index db14c364..bcec7853 100644
--- a/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessage.cs
+++ b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessage.cs
@@ -12,6 +12,6 @@ static abstract JsonTypeInfo JsonInfo {
}
}
-public interface IPeerRequestMessage : IPeerMessage;
+public interface IPeerRequestMessage : IPeerMessage where TResponse : IPeerResponseMessage;
public interface IPeerResponseMessage : IPeerMessage;
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageHandler.cs b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageHandler.cs
index efaa427e..7b359698 100644
--- a/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageHandler.cs
+++ b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageHandler.cs
@@ -18,7 +18,7 @@ CancellationToken cancellationToken
}
public interface IPeerMessageHandler : IPeerMessageHandler
- where TRequest : IPeerRequestMessage
+ where TRequest : IPeerRequestMessage
where TResponse : IPeerResponseMessage {
Task HandleAsync( TRequest message, CancellationToken cancellationToken = default );
diff --git a/src/Networking.PeerStreaming.Tests/Helpers/TestPeerMessage.cs b/src/Networking.PeerStreaming.Tests/Helpers/TestPeerMessage.cs
index b5af1dfa..bd645c9e 100644
--- a/src/Networking.PeerStreaming.Tests/Helpers/TestPeerMessage.cs
+++ b/src/Networking.PeerStreaming.Tests/Helpers/TestPeerMessage.cs
@@ -4,8 +4,8 @@
namespace Drift.Networking.PeerStreaming.Tests.Helpers;
-internal sealed class TestPeerMessage : IPeerRequestMessage, IPeerResponseMessage {
- public static string MessageType => "testpeermessage";
+internal sealed class TestPeerMessage : IPeerRequestMessage, IPeerResponseMessage {
+ public static string MessageType => "test-peer-message";
public static JsonTypeInfo JsonInfo => TestPeerMessageJsonContext.Default.TestPeerMessage;
}
From d4da160f7701f1f601e2b36fd5b9ed2d3897dae8 Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Tue, 2 Dec 2025 22:41:42 +0100
Subject: [PATCH 15/31] Clustering -> Cluster
---
Drift.sln | 2 +-
src/Agent.Hosting/Agent.Hosting.csproj | 2 +-
src/Cli/Cli.csproj | 2 +-
src/{Networking.Clustering => Networking.Cluster}/Cluster.cs | 0
src/{Networking.Clustering => Networking.Cluster}/Enrollment.cs | 0
src/{Networking.Clustering => Networking.Cluster}/ICluster.cs | 0
.../Networking.Cluster.csproj} | 0
.../ServiceCollectionExtensions.cs | 0
8 files changed, 3 insertions(+), 3 deletions(-)
rename src/{Networking.Clustering => Networking.Cluster}/Cluster.cs (100%)
rename src/{Networking.Clustering => Networking.Cluster}/Enrollment.cs (100%)
rename src/{Networking.Clustering => Networking.Cluster}/ICluster.cs (100%)
rename src/{Networking.Clustering/Networking.Clustering.csproj => Networking.Cluster/Networking.Cluster.csproj} (100%)
rename src/{Networking.Clustering => Networking.Cluster}/ServiceCollectionExtensions.cs (100%)
diff --git a/Drift.sln b/Drift.sln
index b9866062..0033f7ad 100644
--- a/Drift.sln
+++ b/Drift.sln
@@ -75,7 +75,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Schemas", "src\Commo
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Networking.PeerStreaming.Grpc", "src\Networking.PeerStreaming.Grpc\Networking.PeerStreaming.Grpc.csproj", "{8ED3FF22-90D2-4F08-A079-55FE7127D1C7}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Networking.Clustering", "src\Networking.Clustering\Networking.Clustering.csproj", "{091D3DCE-F062-4D40-A8F6-5B6F123ED713}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Networking.Cluster", "src\Networking.Cluster\Networking.Cluster.csproj", "{091D3DCE-F062-4D40-A8F6-5B6F123ED713}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Networking.PeerStreaming.Core", "src\Networking.PeerStreaming.Core\Networking.PeerStreaming.Core.csproj", "{80445644-7342-4C6D-88E5-BF27126FE9A2}"
EndProject
diff --git a/src/Agent.Hosting/Agent.Hosting.csproj b/src/Agent.Hosting/Agent.Hosting.csproj
index 5d558217..93719ce4 100644
--- a/src/Agent.Hosting/Agent.Hosting.csproj
+++ b/src/Agent.Hosting/Agent.Hosting.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/src/Cli/Cli.csproj b/src/Cli/Cli.csproj
index 31d53c6b..7e18921b 100644
--- a/src/Cli/Cli.csproj
+++ b/src/Cli/Cli.csproj
@@ -26,7 +26,7 @@
-
+
diff --git a/src/Networking.Clustering/Cluster.cs b/src/Networking.Cluster/Cluster.cs
similarity index 100%
rename from src/Networking.Clustering/Cluster.cs
rename to src/Networking.Cluster/Cluster.cs
diff --git a/src/Networking.Clustering/Enrollment.cs b/src/Networking.Cluster/Enrollment.cs
similarity index 100%
rename from src/Networking.Clustering/Enrollment.cs
rename to src/Networking.Cluster/Enrollment.cs
diff --git a/src/Networking.Clustering/ICluster.cs b/src/Networking.Cluster/ICluster.cs
similarity index 100%
rename from src/Networking.Clustering/ICluster.cs
rename to src/Networking.Cluster/ICluster.cs
diff --git a/src/Networking.Clustering/Networking.Clustering.csproj b/src/Networking.Cluster/Networking.Cluster.csproj
similarity index 100%
rename from src/Networking.Clustering/Networking.Clustering.csproj
rename to src/Networking.Cluster/Networking.Cluster.csproj
diff --git a/src/Networking.Clustering/ServiceCollectionExtensions.cs b/src/Networking.Cluster/ServiceCollectionExtensions.cs
similarity index 100%
rename from src/Networking.Clustering/ServiceCollectionExtensions.cs
rename to src/Networking.Cluster/ServiceCollectionExtensions.cs
From 081145f7fbf52e727a93ae58845cc3f2e6386c70 Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Tue, 2 Dec 2025 22:42:33 +0100
Subject: [PATCH 16/31] f
---
src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs | 2 +-
src/Cli/Commands/Scan/AgentSubnetProvider.cs | 2 +-
src/Cli/Commands/Scan/ClusterExtensions.cs | 2 +-
src/Cli/Commands/Scan/ScanCommand.cs | 2 +-
src/Cli/Infrastructure/RootCommandFactory.cs | 2 +-
src/Networking.Cluster/Cluster.cs | 2 +-
src/Networking.Cluster/Enrollment.cs | 2 +-
src/Networking.Cluster/ICluster.cs | 2 +-
src/Networking.Cluster/ServiceCollectionExtensions.cs | 2 +-
9 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs b/src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs
index 088264eb..1ee0b8f6 100644
--- a/src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs
+++ b/src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs
@@ -7,7 +7,7 @@
using Drift.Cli.Presentation.Console.Logging;
using Drift.Cli.Presentation.Console.Managers.Abstractions;
using Drift.Domain;
-using Drift.Networking.Clustering;
+using Drift.Networking.Cluster;
namespace Drift.Cli.Commands.Agent.Subcommands.Start;
diff --git a/src/Cli/Commands/Scan/AgentSubnetProvider.cs b/src/Cli/Commands/Scan/AgentSubnetProvider.cs
index ed997b2a..8429c923 100644
--- a/src/Cli/Commands/Scan/AgentSubnetProvider.cs
+++ b/src/Cli/Commands/Scan/AgentSubnetProvider.cs
@@ -1,5 +1,5 @@
using Drift.Domain;
-using Drift.Networking.Clustering;
+using Drift.Networking.Cluster;
using Drift.Scanning.Subnets;
namespace Drift.Cli.Commands.Scan;
diff --git a/src/Cli/Commands/Scan/ClusterExtensions.cs b/src/Cli/Commands/Scan/ClusterExtensions.cs
index 929031a3..0edb60a9 100644
--- a/src/Cli/Commands/Scan/ClusterExtensions.cs
+++ b/src/Cli/Commands/Scan/ClusterExtensions.cs
@@ -1,5 +1,5 @@
using Drift.Agent.PeerProtocol.Subnets;
-using Drift.Networking.Clustering;
+using Drift.Networking.Cluster;
namespace Drift.Cli.Commands.Scan;
diff --git a/src/Cli/Commands/Scan/ScanCommand.cs b/src/Cli/Commands/Scan/ScanCommand.cs
index f23a344b..485d964f 100644
--- a/src/Cli/Commands/Scan/ScanCommand.cs
+++ b/src/Cli/Commands/Scan/ScanCommand.cs
@@ -10,7 +10,7 @@
using Drift.Common.Network;
using Drift.Domain;
using Drift.Domain.Scan;
-using Drift.Networking.Clustering;
+using Drift.Networking.Cluster;
using Drift.Scanning.Subnets;
using Drift.Scanning.Subnets.Interface;
diff --git a/src/Cli/Infrastructure/RootCommandFactory.cs b/src/Cli/Infrastructure/RootCommandFactory.cs
index 91687751..a7d46500 100644
--- a/src/Cli/Infrastructure/RootCommandFactory.cs
+++ b/src/Cli/Infrastructure/RootCommandFactory.cs
@@ -17,7 +17,7 @@
using Drift.Cli.SpecFile;
using Drift.Domain.ExecutionEnvironment;
using Drift.Domain.Scan;
-using Drift.Networking.Clustering;
+using Drift.Networking.Cluster;
using Drift.Networking.PeerStreaming.Client;
using Drift.Networking.PeerStreaming.Core;
using Drift.Scanning;
diff --git a/src/Networking.Cluster/Cluster.cs b/src/Networking.Cluster/Cluster.cs
index 997b9858..760cf7f3 100644
--- a/src/Networking.Cluster/Cluster.cs
+++ b/src/Networking.Cluster/Cluster.cs
@@ -2,7 +2,7 @@
using Drift.Networking.PeerStreaming.Core.Messages;
using Microsoft.Extensions.Logging;
-namespace Drift.Networking.Clustering;
+namespace Drift.Networking.Cluster;
internal sealed class Cluster(
IPeerMessageEnvelopeConverter envelopeConverter,
diff --git a/src/Networking.Cluster/Enrollment.cs b/src/Networking.Cluster/Enrollment.cs
index 92143071..9f425851 100644
--- a/src/Networking.Cluster/Enrollment.cs
+++ b/src/Networking.Cluster/Enrollment.cs
@@ -1,4 +1,4 @@
-namespace Drift.Networking.Clustering;
+namespace Drift.Networking.Cluster;
public class EnrollmentRequest( bool parametersAdoptable, string? parametersJoin ) {
public EnrollmentMethod Method => parametersAdoptable ? EnrollmentMethod.Adoption : EnrollmentMethod.Jwt;
diff --git a/src/Networking.Cluster/ICluster.cs b/src/Networking.Cluster/ICluster.cs
index d9fdec97..b64567b5 100644
--- a/src/Networking.Cluster/ICluster.cs
+++ b/src/Networking.Cluster/ICluster.cs
@@ -1,6 +1,6 @@
using Drift.Networking.PeerStreaming.Core.Abstractions;
-namespace Drift.Networking.Clustering;
+namespace Drift.Networking.Cluster;
public interface ICluster {
//Task SendAsync( Domain.Agent agent, IPeerMessage message, CancellationToken cancellationToken = default );
diff --git a/src/Networking.Cluster/ServiceCollectionExtensions.cs b/src/Networking.Cluster/ServiceCollectionExtensions.cs
index 183cb7e0..5d26edd7 100644
--- a/src/Networking.Cluster/ServiceCollectionExtensions.cs
+++ b/src/Networking.Cluster/ServiceCollectionExtensions.cs
@@ -1,6 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
-namespace Drift.Networking.Clustering;
+namespace Drift.Networking.Cluster;
public static class ServiceCollectionExtensions {
public static void AddClustering( this IServiceCollection services ) {
From 1fa930a42a05a22e7576819f577d0b648bc6ce4c Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Wed, 3 Dec 2025 22:31:50 +0100
Subject: [PATCH 17/31] subnet source
---
.../Subnets/SubnetsRequestHandler.cs | 2 +-
.../ScanCommandTests.RemoteScan.verified.txt | 6 +--
src/Cli.Tests/Commands/ScanCommandTests.cs | 4 +-
...eptionReturnsUnknownErrorTest.verified.txt | 6 ++-
src/Cli/Commands/Init/InitCommand.cs | 4 +-
src/Cli/Commands/Scan/AgentSubnetProvider.cs | 15 +++++---
src/Cli/Commands/Scan/ScanCommand.cs | 38 ++++++++++---------
.../Subnets/CompositeSubnetProvider.cs | 4 +-
src/Scanning/Subnets/IResolvedSubnet.cs | 26 +++++++++++++
src/Scanning/Subnets/ISubnetProvider.cs | 4 +-
.../Interface/InterfaceSubnetProviderBase.cs | 5 +--
.../Subnets/PredefinedSubnetProvider.cs | 8 +++-
src/Spec/Dtos/V1_preview/DriftSpec.cs | 5 +++
.../V1_preview/Mappers/Mapper.ToDomain.cs | 3 +-
14 files changed, 86 insertions(+), 44 deletions(-)
create mode 100644 src/Scanning/Subnets/IResolvedSubnet.cs
diff --git a/src/Agent.PeerProtocol/Subnets/SubnetsRequestHandler.cs b/src/Agent.PeerProtocol/Subnets/SubnetsRequestHandler.cs
index 68c88d86..dea4933c 100644
--- a/src/Agent.PeerProtocol/Subnets/SubnetsRequestHandler.cs
+++ b/src/Agent.PeerProtocol/Subnets/SubnetsRequestHandler.cs
@@ -16,7 +16,7 @@ ILogger logger
) {
logger.LogInformation( "Handling subnet request" );
- var subnets = await interfaceSubnetProvider.GetAsync();
+ var subnets = ( await interfaceSubnetProvider.GetAsync() ).Select( s => s.Cidr ).ToList();
logger.LogInformation( "Sending subnets: {Subnets}", string.Join( ", ", subnets ) );
diff --git a/src/Cli.Tests/Commands/ScanCommandTests.RemoteScan.verified.txt b/src/Cli.Tests/Commands/ScanCommandTests.RemoteScan.verified.txt
index 68cf64c2..b42fe364 100644
--- a/src/Cli.Tests/Commands/ScanCommandTests.RemoteScan.verified.txt
+++ b/src/Cli.Tests/Commands/ScanCommandTests.RemoteScan.verified.txt
@@ -1,8 +1,8 @@
-Requesting subnets from agent agentid_local1 (http://localhost:51515)
-Received subnet(s) from agent agentid_local1 (http://localhost:51515): 192.168.0.0/24
+Requesting subnets from agent local1
+Received subnet(s) from agent local1: 192.168.0.0/24
Scanning 1 subnet
- 192.168.0.0/24 (254 addresses, estimated scan time is 00:00:05.0800000) via agentid_local1, local
+ 192.168.0.0/24 (254 addresses, estimated scan time is 00:00:05.0800000) via local, agentid_local1
192.168.0.0/24 (1 devices)
└── 192.168.0.100 11-11-11-11-11-11 Online (unknown device)
\ No newline at end of file
diff --git a/src/Cli.Tests/Commands/ScanCommandTests.cs b/src/Cli.Tests/Commands/ScanCommandTests.cs
index e3edbd71..8835f4a4 100644
--- a/src/Cli.Tests/Commands/ScanCommandTests.cs
+++ b/src/Cli.Tests/Commands/ScanCommandTests.cs
@@ -227,7 +227,9 @@ public async Task RemoteScan() {
[
new DiscoveredDevice { Addresses = [new IpV4Address( "192.168.0.100" ), new MacAddress( "11:11:11:11:11:11" )] }
],
- new Inventory { Network = new Network(), Agents = [new Domain.Agent { Address = "http://localhost:51515" }] }
+ new Inventory {
+ Network = new Network(), Agents = [new Domain.Agent { Id = "local1", Address = "http://localhost:51515" }]
+ }
);
var serviceConfigAgent = ConfigureServices(
diff --git a/src/Cli.Tests/ExitCodeTests.UnhandledExceptionReturnsUnknownErrorTest.verified.txt b/src/Cli.Tests/ExitCodeTests.UnhandledExceptionReturnsUnknownErrorTest.verified.txt
index c5c74bf4..6a514c74 100644
--- a/src/Cli.Tests/ExitCodeTests.UnhandledExceptionReturnsUnknownErrorTest.verified.txt
+++ b/src/Cli.Tests/ExitCodeTests.UnhandledExceptionReturnsUnknownErrorTest.verified.txt
@@ -1,6 +1,8 @@
✗ This exception was thrown from ExceptionCommandHandler
at Drift.Cli.Tests.ExitCodeTests.ExceptionCommandHandler.Invoke(DummyParameters parameters, CancellationToken cancellationToken) in {ProjectDirectory}ExitCodeTests.cs:line 105
- at Drift.Cli.Commands.Common.Commands.CommandBase`2.<>c__DisplayClass0_0.<.ctor>b__0(ParseResult parseResult, CancellationToken cancellationToken) in {SolutionDirectory}src/Cli/Commands/Common/Commands/CommandBase.cs:line 25
- at System.CommandLine.Invocation.AnonymousAsynchronousCommandLineAction.InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken)
+ at Drift.Cli.Commands.Common.Commands.CommandBase`2.<>c__DisplayClass0_0.<<-ctor>b__0>d.MoveNext() in {SolutionDirectory}src/Cli/Commands/Common/Commands/CommandBase.cs:line 25
+--- End of stack trace from previous location ---
+ at Drift.Cli.Commands.Common.Commands.CommandBase`2.<>c__DisplayClass0_0.<<-ctor>b__0>d.MoveNext() in {SolutionDirectory}src/Cli/Commands/Common/Commands/CommandBase.cs:line 25
+--- End of stack trace from previous location ---
at System.CommandLine.Invocation.InvocationPipeline.InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken)
at Drift.Cli.DriftCli.InvokeAsync(String[] args, Boolean toConsole, Boolean plainConsole, Action`1 configureServices, CommandRegistration[] customCommands, Action`1 configureInvocation, CancellationToken cancellationToken) in {SolutionDirectory}src/Cli/DriftCli.cs:line 40
diff --git a/src/Cli/Commands/Init/InitCommand.cs b/src/Cli/Commands/Init/InitCommand.cs
index 581ab266..24eec1a0 100644
--- a/src/Cli/Commands/Init/InitCommand.cs
+++ b/src/Cli/Commands/Init/InitCommand.cs
@@ -178,7 +178,9 @@ private async Task Initialize( InitOptions options ) {
return false;
}
- var scanOptions = new NetworkScanOptions { Cidrs = ( await interfaceSubnetProvider.GetAsync() ).ToList() };
+ var scanOptions = new NetworkScanOptions {
+ Cidrs = ( await interfaceSubnetProvider.GetAsync() ).Select( subnet => subnet.Cidr ).ToList()
+ };
LogSubnetDetails( scanOptions );
diff --git a/src/Cli/Commands/Scan/AgentSubnetProvider.cs b/src/Cli/Commands/Scan/AgentSubnetProvider.cs
index 8429c923..5d6df123 100644
--- a/src/Cli/Commands/Scan/AgentSubnetProvider.cs
+++ b/src/Cli/Commands/Scan/AgentSubnetProvider.cs
@@ -10,24 +10,27 @@ internal sealed class AgentSubnetProvider(
ICluster cluster,
CancellationToken cancellationToken
) : ISubnetProvider {
- public async Task> GetAsync() {
+ public async Task> GetAsync() {
logger.LogDebug( "Getting subnets from agents" );
- var allSubnets = new List();
+ var allSubnets = new List();
foreach ( var agent in agents ) {
- logger.LogInformation( "Requesting subnets from agent {Id} ({Address})", agent.Id, agent.Address );
+ logger.LogInformation( "Requesting subnets from agent {Id}", agent.Id );
try {
var response = await cluster.GetSubnetsAsync( agent, cancellationToken );
logger.LogInformation(
- "Received subnet(s) from agent {Id} ({Address}): {Subnets}",
+ "Received subnet(s) from agent {Id}: {Subnets}",
agent.Id,
- agent.Address,
string.Join( ", ", response.Subnets )
);
- allSubnets.AddRange( response.Subnets );
+ allSubnets.AddRange( response.Subnets.Select( cidr =>
+ new ResolvedSubnet( cidr, SubnetSource.Agent(
+ new AgentId( "agentid_" + agent.Id ) // TODO Fix agent id
+ ) ) )
+ );
}
catch ( Exception ex ) {
logger.LogInformation( ex, "Failed requesting subnets from agent {Id} ({Address})", agent.Id, agent.Address );
diff --git a/src/Cli/Commands/Scan/ScanCommand.cs b/src/Cli/Commands/Scan/ScanCommand.cs
index 485d964f..e8722b48 100644
--- a/src/Cli/Commands/Scan/ScanCommand.cs
+++ b/src/Cli/Commands/Scan/ScanCommand.cs
@@ -17,14 +17,8 @@
namespace Drift.Cli.Commands.Scan;
/*
- * Ideas:
- * Interactive mode:
- * ➤ New host found: 192.168.1.42
- * ➤ Port 22 no longer open on 192.168.1.10
- * → Would you like to update the declared state? [y/N]
-
* Monitor mode:
- * drift monitor --reference declared.yaml --interval 10m --notify slack,email,log,webhook
+ * drift monitor declared.yaml --interval 10m --notify slack,email,log,webhook
*/
internal class ScanCommand : CommandBase {
public ScanCommand( IServiceProvider provider ) : base( "scan", "Scan the network and detect drift", provider ) {
@@ -97,27 +91,37 @@ public async Task Invoke( ScanParameters parameters, CancellationToken canc
output.Normal.WriteLineVerbose( $"Using {subnetProvider.GetType().Name}" );
output.Log.LogDebug( "Using {SubnetProviderType}", subnetProvider.GetType().Name );
- var subnets = await subnetProvider.GetAsync();
+ var groupedSubnets = ( await subnetProvider.GetAsync() )
+ .GroupBy( subnet => subnet.Cidr )
+ .Select( group => new { Cidr = group.Key, Sources = group.Select( r => r.Source ).Distinct().ToList() } )
+ .ToList();
- var scanRequest = new NetworkScanOptions { Cidrs = subnets };
+ var scanRequest = new NetworkScanOptions { Cidrs = groupedSubnets.Select( group => group.Cidr ).ToList() };
// TODO many more varieties
- output.Normal.WriteLine( 0, $"Scanning {subnets.Count} subnet{( subnets.Count > 1 ? "s" : string.Empty )}" );
- foreach ( var cidr in subnets ) {
+ output.Normal.WriteLine(
+ 0,
+ $"Scanning {groupedSubnets.Count} subnet{( groupedSubnets.Count > 1 ? "s" : string.Empty )}"
+ );
+ foreach ( var subnet in groupedSubnets ) {
+ var sourceList = string.Join( ", ", subnet.Sources );
// TODO write name if from spec: Ui.WriteLine( 1, $"{subnet.Id}: {subnet.Network}" );
- output.Normal.Write( 1, $"{cidr}", ConsoleColor.Cyan );
+ output.Normal.Write( 1, $"{subnet.Cidr}", ConsoleColor.Cyan );
output.Normal.WriteLine(
- " (" + IpNetworkUtils.GetIpRangeCount( cidr ) +
+ " (" + IpNetworkUtils.GetIpRangeCount( subnet.Cidr ) +
" addresses, estimated scan time is " +
scanRequest.EstimatedDuration(
- cidr ) + // TODO .Humanize( 2, CultureInfo.InvariantCulture, minUnit: TimeUnit.Second )
- ")", ConsoleColor.DarkGray );
+ subnet.Cidr ) + // TODO .Humanize( 2, CultureInfo.InvariantCulture, minUnit: TimeUnit.Second )
+ ") via " +
+ sourceList,
+ ConsoleColor.DarkGray
+ );
}
output.Log.LogInformation(
"Scanning {SubnetCount} subnet(s): {SubnetList}",
- subnets.Count,
- string.Join( ", ", subnets )
+ groupedSubnets.Count,
+ string.Join( ", ", groupedSubnets.Select( s => s.Cidr ) )
);
Task uiTask;
diff --git a/src/Scanning/Subnets/CompositeSubnetProvider.cs b/src/Scanning/Subnets/CompositeSubnetProvider.cs
index 9fa20229..8a14fd54 100644
--- a/src/Scanning/Subnets/CompositeSubnetProvider.cs
+++ b/src/Scanning/Subnets/CompositeSubnetProvider.cs
@@ -1,12 +1,10 @@
-using Drift.Domain;
-
namespace Drift.Scanning.Subnets;
// TODO needed?
public class CompositeSubnetProvider( IEnumerable providers ) : ISubnetProvider {
private readonly List _providers = providers.ToList();
- public async Task> GetAsync() {
+ public async Task> GetAsync() {
var results = await Task.WhenAll( _providers.Select( p => p.GetAsync() ) );
return results.SelectMany( x => x ).Distinct().ToList();
}
diff --git a/src/Scanning/Subnets/IResolvedSubnet.cs b/src/Scanning/Subnets/IResolvedSubnet.cs
new file mode 100644
index 00000000..c1d43f19
--- /dev/null
+++ b/src/Scanning/Subnets/IResolvedSubnet.cs
@@ -0,0 +1,26 @@
+using Drift.Domain;
+
+namespace Drift.Scanning.Subnets;
+
+public abstract record SubnetSource {
+ public static readonly Local Local = new();
+
+ public static Agent Agent( AgentId agentId ) {
+ ArgumentNullException.ThrowIfNull( agentId );
+ return new Agent( agentId );
+ }
+}
+
+public sealed record Agent( AgentId AgentId ) : SubnetSource {
+ public override string ToString() {
+ return AgentId;
+ }
+}
+
+public sealed record Local : SubnetSource {
+ public override string ToString() {
+ return "local";
+ }
+}
+
+public sealed record ResolvedSubnet( CidrBlock Cidr, SubnetSource Source );
\ No newline at end of file
diff --git a/src/Scanning/Subnets/ISubnetProvider.cs b/src/Scanning/Subnets/ISubnetProvider.cs
index 63d1a826..e8067faa 100644
--- a/src/Scanning/Subnets/ISubnetProvider.cs
+++ b/src/Scanning/Subnets/ISubnetProvider.cs
@@ -1,7 +1,5 @@
-using Drift.Domain;
-
namespace Drift.Scanning.Subnets;
public interface ISubnetProvider {
- Task> GetAsync();
+ Task> GetAsync();
}
\ No newline at end of file
diff --git a/src/Scanning/Subnets/Interface/InterfaceSubnetProviderBase.cs b/src/Scanning/Subnets/Interface/InterfaceSubnetProviderBase.cs
index dabf8eac..48cb1817 100644
--- a/src/Scanning/Subnets/Interface/InterfaceSubnetProviderBase.cs
+++ b/src/Scanning/Subnets/Interface/InterfaceSubnetProviderBase.cs
@@ -1,6 +1,5 @@
using System.Net.NetworkInformation;
using Drift.Common.Network;
-using Drift.Domain;
using Microsoft.Extensions.Logging;
namespace Drift.Scanning.Subnets.Interface;
@@ -8,7 +7,7 @@ namespace Drift.Scanning.Subnets.Interface;
public abstract class InterfaceSubnetProviderBase( ILogger? logger ) : IInterfaceSubnetProvider {
public abstract List GetInterfaces();
- public Task> GetAsync() {
+ public Task> GetAsync() {
var interfaces = GetInterfaces();
var interfaceDescriptions =
string.Join(
@@ -41,7 +40,7 @@ public Task> GetAsync() {
string.Join( ", ", cidrs )
);
- return Task.FromResult( cidrs );
+ return Task.FromResult( cidrs.Select( cidr => new ResolvedSubnet( cidr, SubnetSource.Local ) ).ToList() );
}
private static bool IsUp( INetworkInterface i ) {
diff --git a/src/Scanning/Subnets/PredefinedSubnetProvider.cs b/src/Scanning/Subnets/PredefinedSubnetProvider.cs
index dbc6d72e..0cf08797 100644
--- a/src/Scanning/Subnets/PredefinedSubnetProvider.cs
+++ b/src/Scanning/Subnets/PredefinedSubnetProvider.cs
@@ -3,11 +3,15 @@
namespace Drift.Scanning.Subnets;
public class PredefinedSubnetProvider( IEnumerable subnets ) : ISubnetProvider {
- public Task> GetAsync() {
+ public Task> GetAsync() {
return Task.FromResult(
subnets
.Where( s => s.Enabled ?? true )
- .Select( s => new CidrBlock( s.Address ) )
+ .Select( s => new ResolvedSubnet(
+ new CidrBlock( s.Address ),
+ // TODO how to determine source when from spec?
+ SubnetSource.Local
+ ) )
.ToList()
);
}
diff --git a/src/Spec/Dtos/V1_preview/DriftSpec.cs b/src/Spec/Dtos/V1_preview/DriftSpec.cs
index bda6a3a7..d786b487 100644
--- a/src/Spec/Dtos/V1_preview/DriftSpec.cs
+++ b/src/Spec/Dtos/V1_preview/DriftSpec.cs
@@ -154,6 +154,11 @@ public bool? ScanOnlyDeclaredSubnets {
[AdditionalProperties( false )]
public record Agent {
+ public string Id {
+ get;
+ set;
+ }
+
public string Address {
get;
set;
diff --git a/src/Spec/Dtos/V1_preview/Mappers/Mapper.ToDomain.cs b/src/Spec/Dtos/V1_preview/Mappers/Mapper.ToDomain.cs
index 8d37d4a4..3f53429c 100644
--- a/src/Spec/Dtos/V1_preview/Mappers/Mapper.ToDomain.cs
+++ b/src/Spec/Dtos/V1_preview/Mappers/Mapper.ToDomain.cs
@@ -23,10 +23,9 @@ public static Inventory ToDomain( DriftSpec dto ) {
private static Domain.Agent Map( Agent dto ) {
var agent = new Domain.Agent();
+ agent.Id = dto.Id;
agent.Address = dto.Address;
- // TODO agent.Id = dto.
-
return agent;
}
From c9d8fea7ac08f6dd3fb2c21a7bd7048ddb0704ab Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Wed, 3 Dec 2025 22:38:33 +0100
Subject: [PATCH 18/31] f
---
src/Agent.PeerProtocol.Tests/AssemblyInfo.cs | 1 +
src/Cli/Commands/Scan/ScanCommand.cs | 8 +++++---
2 files changed, 6 insertions(+), 3 deletions(-)
create mode 100644 src/Agent.PeerProtocol.Tests/AssemblyInfo.cs
diff --git a/src/Agent.PeerProtocol.Tests/AssemblyInfo.cs b/src/Agent.PeerProtocol.Tests/AssemblyInfo.cs
new file mode 100644
index 00000000..5493e66a
--- /dev/null
+++ b/src/Agent.PeerProtocol.Tests/AssemblyInfo.cs
@@ -0,0 +1 @@
+[assembly: Category( "Unit" )]
\ No newline at end of file
diff --git a/src/Cli/Commands/Scan/ScanCommand.cs b/src/Cli/Commands/Scan/ScanCommand.cs
index e8722b48..b0e2977b 100644
--- a/src/Cli/Commands/Scan/ScanCommand.cs
+++ b/src/Cli/Commands/Scan/ScanCommand.cs
@@ -75,7 +75,9 @@ public async Task Invoke( ScanParameters parameters, CancellationToken canc
subnetProviders.Add( new PredefinedSubnetProvider( inventory.Network.Subnets ) );
}
- if ( inventory?.Agents.Any() ?? false ) {
+ var hasAgents = inventory?.Agents.Any() ?? false;
+
+ if ( hasAgents ) {
subnetProviders.Add(
new AgentSubnetProvider(
output.GetLogger(),
@@ -112,8 +114,8 @@ public async Task Invoke( ScanParameters parameters, CancellationToken canc
" addresses, estimated scan time is " +
scanRequest.EstimatedDuration(
subnet.Cidr ) + // TODO .Humanize( 2, CultureInfo.InvariantCulture, minUnit: TimeUnit.Second )
- ") via " +
- sourceList,
+ ")" +
+ ( hasAgents ? $" via {sourceList}" : string.Empty ),
ConsoleColor.DarkGray
);
}
From 1aaf963c2430129db7c64dc7e75c0bf32fbca56a Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Thu, 4 Dec 2025 21:53:59 +0100
Subject: [PATCH 19/31] fix test
---
src/Agent.PeerProtocol.Tests/PeerMessageHandlerTests.cs | 8 +++++++-
src/Spec/Dtos/V1_preview/DriftSpec.cs | 2 ++
.../schemas/drift-spec-v1-preview.schema.json | 7 +++++++
3 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/src/Agent.PeerProtocol.Tests/PeerMessageHandlerTests.cs b/src/Agent.PeerProtocol.Tests/PeerMessageHandlerTests.cs
index 27e11534..c5cf8960 100644
--- a/src/Agent.PeerProtocol.Tests/PeerMessageHandlerTests.cs
+++ b/src/Agent.PeerProtocol.Tests/PeerMessageHandlerTests.cs
@@ -11,6 +11,7 @@ internal sealed class PeerMessageHandlerTests {
[Test]
public void FindMessagesAndHandlersAndMessages() {
+ using var _ = Assert.EnterMultipleScope();
Assert.That( RequestTypes.ToList(), Has.Count.GreaterThan( 1 ), "No request messages found via reflection" );
Assert.That( ResponseTypes.ToList(), Has.Count.GreaterThan( 1 ), "No response messages found via reflection" );
Assert.That( HandlerTypes.ToList(), Has.Count.GreaterThan( 1 ), "No handlers found via reflection" );
@@ -99,7 +100,12 @@ private static List GetAllConcreteMessageTypes( Type baseType ) {
return ProtocolAssembly
.GetTypes()
.Where( t => t is { IsAbstract: false, IsInterface: false } )
- .Where( baseType.IsAssignableFrom )
+ .Where( t =>
+ // Generic base type
+ t.GetInterfaces().Any( i => i.IsGenericType && i.GetGenericTypeDefinition() == baseType ) ||
+ // Non-generic base type
+ baseType.IsAssignableFrom( t )
+ )
.ToList();
}
diff --git a/src/Spec/Dtos/V1_preview/DriftSpec.cs b/src/Spec/Dtos/V1_preview/DriftSpec.cs
index d786b487..1dbc1a23 100644
--- a/src/Spec/Dtos/V1_preview/DriftSpec.cs
+++ b/src/Spec/Dtos/V1_preview/DriftSpec.cs
@@ -154,11 +154,13 @@ public bool? ScanOnlyDeclaredSubnets {
[AdditionalProperties( false )]
public record Agent {
+ [Required]
public string Id {
get;
set;
}
+ [Required]
public string Address {
get;
set;
diff --git a/src/Spec/embedded_resources/schemas/drift-spec-v1-preview.schema.json b/src/Spec/embedded_resources/schemas/drift-spec-v1-preview.schema.json
index 210056a6..da2b2073 100644
--- a/src/Spec/embedded_resources/schemas/drift-spec-v1-preview.schema.json
+++ b/src/Spec/embedded_resources/schemas/drift-spec-v1-preview.schema.json
@@ -111,10 +111,17 @@
"items": {
"type": "object",
"properties": {
+ "id": {
+ "type": "string"
+ },
"address": {
"type": "string"
}
},
+ "required": [
+ "id",
+ "address"
+ ],
"additionalProperties": false
}
}
From 6559f7a6da14f6b0911ae841e721e9efa6fa9cc8 Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Thu, 4 Dec 2025 22:02:50 +0100
Subject: [PATCH 20/31] fix test
---
src/Networking.PeerStreaming.Tests/PeerStreamManagerTests.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/Networking.PeerStreaming.Tests/PeerStreamManagerTests.cs b/src/Networking.PeerStreaming.Tests/PeerStreamManagerTests.cs
index d1c3ca11..62bb8eb9 100644
--- a/src/Networking.PeerStreaming.Tests/PeerStreamManagerTests.cs
+++ b/src/Networking.PeerStreaming.Tests/PeerStreamManagerTests.cs
@@ -12,7 +12,8 @@ public async Task IncomingMessageIsDispatchedToHandler() {
var cts = new CancellationTokenSource();
var logger = new StringLogger( TestContext.Out );
var testMessageHandler = new TestMessageHandler();
- var dispatcher = new PeerMessageDispatcher( [testMessageHandler], null, null, logger );
+ var envelopeConverter = new PeerMessageEnvelopeConverter();
+ var dispatcher = new PeerMessageDispatcher( [testMessageHandler], envelopeConverter, null, logger );
var peerStreamManager = new PeerStreamManager(
logger,
null,
From 3a8f778907e14071cc0a79d0a4c7c8847d234434 Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Thu, 4 Dec 2025 22:37:09 +0100
Subject: [PATCH 21/31] fix most tests
---
.../PeerMessageHandlerTests.cs | 6 ++++--
.../Adopt/AdoptRequestHandler.cs | 6 +++---
.../Adopt/AdoptRequestPayload.cs | 2 +-
src/Agent.PeerProtocol/NullResponse.cs | 14 --------------
src/Agent.PeerProtocol/Subnets/SubnetsRequest.cs | 4 ++--
.../Subnets/SubnetsResponse.cs | 4 ++--
src/Networking.Cluster/Cluster.cs | 2 +-
src/Networking.Cluster/ICluster.cs | 2 +-
.../Empty.cs | 16 ++++++++++++++++
.../IPeerMessage.cs | 6 ++++--
.../IPeerMessageHandler.cs | 8 ++++----
.../Helpers/TestPeerMessage.cs | 2 +-
12 files changed, 39 insertions(+), 33 deletions(-)
delete mode 100644 src/Agent.PeerProtocol/NullResponse.cs
create mode 100644 src/Networking.PeerStreaming.Core.Abstractions/Empty.cs
diff --git a/src/Agent.PeerProtocol.Tests/PeerMessageHandlerTests.cs b/src/Agent.PeerProtocol.Tests/PeerMessageHandlerTests.cs
index c5cf8960..90aa24d9 100644
--- a/src/Agent.PeerProtocol.Tests/PeerMessageHandlerTests.cs
+++ b/src/Agent.PeerProtocol.Tests/PeerMessageHandlerTests.cs
@@ -5,8 +5,8 @@ namespace Drift.Agent.PeerProtocol.Tests;
internal sealed class PeerMessageHandlerTests {
private static readonly Assembly ProtocolAssembly = typeof(PeerProtocolAssemblyMarker).Assembly;
- private static readonly IEnumerable RequestTypes = GetAllConcreteMessageTypes( typeof(IPeerRequestMessage<>) );
- private static readonly IEnumerable ResponseTypes = GetAllConcreteMessageTypes( typeof(IPeerResponseMessage) );
+ private static readonly IEnumerable RequestTypes = GetAllConcreteMessageTypes( typeof(IPeerRequest<>) );
+ private static readonly IEnumerable ResponseTypes = GetAllConcreteMessageTypes( typeof(IPeerResponse) );
private static readonly IEnumerable HandlerTypes = GetAllConcreteHandlerTypes();
[Test]
@@ -80,6 +80,7 @@ public void AllResponseMessagesHaveHandlers_AndNoExtraHandlers() {
);
}
+ [Explicit( "Disabled until interface has settled" )]
[TestCaseSource( nameof(RequestTypes) )]
[TestCaseSource( nameof(ResponseTypes) )]
public void Messages_HaveValidMessageTypeAndJsonInfo( Type type ) {
@@ -99,6 +100,7 @@ public void Messages_HaveValidMessageTypeAndJsonInfo( Type type ) {
private static List GetAllConcreteMessageTypes( Type baseType ) {
return ProtocolAssembly
.GetTypes()
+ .Concat( [typeof(Empty)] )
.Where( t => t is { IsAbstract: false, IsInterface: false } )
.Where( t =>
// Generic base type
diff --git a/src/Agent.PeerProtocol/Adopt/AdoptRequestHandler.cs b/src/Agent.PeerProtocol/Adopt/AdoptRequestHandler.cs
index 4a4b8c27..6f74e318 100644
--- a/src/Agent.PeerProtocol/Adopt/AdoptRequestHandler.cs
+++ b/src/Agent.PeerProtocol/Adopt/AdoptRequestHandler.cs
@@ -3,14 +3,14 @@
namespace Drift.Agent.PeerProtocol.Adopt;
-internal sealed class AdoptRequestHandler : IPeerMessageHandler {
+internal sealed class AdoptRequestHandler : IPeerMessageHandler {
private readonly ILogger _logger; // Example: inject what you need
public string MessageType => AdoptRequestPayload.MessageType;
- public async Task HandleAsync( AdoptRequestPayload message,
+ public async Task HandleAsync( AdoptRequestPayload message,
CancellationToken cancellationToken = default ) {
_logger.LogInformation( $"[AdoptRequest] Controller: {message.ControllerId}" );
- return null;
+ return IPeerResponse.Empty;
}
}
\ No newline at end of file
diff --git a/src/Agent.PeerProtocol/Adopt/AdoptRequestPayload.cs b/src/Agent.PeerProtocol/Adopt/AdoptRequestPayload.cs
index 9854c08c..2c6b781f 100644
--- a/src/Agent.PeerProtocol/Adopt/AdoptRequestPayload.cs
+++ b/src/Agent.PeerProtocol/Adopt/AdoptRequestPayload.cs
@@ -3,7 +3,7 @@
namespace Drift.Agent.PeerProtocol.Adopt;
-internal sealed class AdoptRequestPayload : IPeerRequestMessage {
+internal sealed class AdoptRequestPayload : IPeerRequest {
public static string MessageType => "adopt-request";
public string Jwt {
diff --git a/src/Agent.PeerProtocol/NullResponse.cs b/src/Agent.PeerProtocol/NullResponse.cs
deleted file mode 100644
index 9ae4252e..00000000
--- a/src/Agent.PeerProtocol/NullResponse.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System.Text.Json.Serialization.Metadata;
-using Drift.Networking.PeerStreaming.Core.Abstractions;
-
-namespace Drift.Agent.PeerProtocol;
-
-public class NullResponse : IPeerResponseMessage {
- public static string MessageType {
- get;
- }
-
- public static JsonTypeInfo JsonInfo {
- get;
- }
-}
\ No newline at end of file
diff --git a/src/Agent.PeerProtocol/Subnets/SubnetsRequest.cs b/src/Agent.PeerProtocol/Subnets/SubnetsRequest.cs
index 291d46e8..7f0323f7 100644
--- a/src/Agent.PeerProtocol/Subnets/SubnetsRequest.cs
+++ b/src/Agent.PeerProtocol/Subnets/SubnetsRequest.cs
@@ -4,8 +4,8 @@
namespace Drift.Agent.PeerProtocol.Subnets;
-public sealed class SubnetsRequest : IPeerRequestMessage {
- public static string MessageType => "subnetsrequest";
+public sealed class SubnetsRequest : IPeerRequest {
+ public static string MessageType => "subnets-request";
public static JsonTypeInfo JsonInfo => SubnetsRequestJsonContext.Default.SubnetsRequest;
}
diff --git a/src/Agent.PeerProtocol/Subnets/SubnetsResponse.cs b/src/Agent.PeerProtocol/Subnets/SubnetsResponse.cs
index f42c26d4..f2402b6c 100644
--- a/src/Agent.PeerProtocol/Subnets/SubnetsResponse.cs
+++ b/src/Agent.PeerProtocol/Subnets/SubnetsResponse.cs
@@ -6,8 +6,8 @@
namespace Drift.Agent.PeerProtocol.Subnets;
-public sealed class SubnetsResponse : IPeerResponseMessage {
- public static string MessageType => "subnetsresponse";
+public sealed class SubnetsResponse : IPeerResponse {
+ public static string MessageType => "subnets-response";
public required IReadOnlyList Subnets {
get;
diff --git a/src/Networking.Cluster/Cluster.cs b/src/Networking.Cluster/Cluster.cs
index 760cf7f3..58df5779 100644
--- a/src/Networking.Cluster/Cluster.cs
+++ b/src/Networking.Cluster/Cluster.cs
@@ -54,7 +54,7 @@ public async Task SendAndWaitAsync(
TRequest message,
TimeSpan? timeout = null,
CancellationToken cancellationToken = default
- ) where TResponse : IPeerResponseMessage where TRequest : IPeerRequestMessage {
+ ) where TResponse : IPeerResponse where TRequest : IPeerRequest {
var correlationId = Guid.NewGuid().ToString();
var envelope = envelopeConverter.ToEnvelope( message );
envelope.CorrelationId = correlationId;
diff --git a/src/Networking.Cluster/ICluster.cs b/src/Networking.Cluster/ICluster.cs
index b64567b5..727de42b 100644
--- a/src/Networking.Cluster/ICluster.cs
+++ b/src/Networking.Cluster/ICluster.cs
@@ -10,7 +10,7 @@ Task SendAndWaitAsync(
TRequest message,
TimeSpan? timeout = null,
CancellationToken cancellationToken = default
- ) where TResponse : IPeerResponseMessage where TRequest : IPeerRequestMessage;
+ ) where TResponse : IPeerResponse where TRequest : IPeerRequest;
/*Task BroadcastAsync( PeerMessage message, CancellationToken cancellationToken = default );
Task> RequestSubnetsAsync( string peerAddress, CancellationToken cancellationToken = default );
diff --git a/src/Networking.PeerStreaming.Core.Abstractions/Empty.cs b/src/Networking.PeerStreaming.Core.Abstractions/Empty.cs
new file mode 100644
index 00000000..2c43940d
--- /dev/null
+++ b/src/Networking.PeerStreaming.Core.Abstractions/Empty.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization.Metadata;
+
+namespace Drift.Networking.PeerStreaming.Core.Abstractions;
+
+public class Empty : IPeerResponse {
+ private Empty() {
+ }
+
+ internal static Empty Instance {
+ get;
+ } = new();
+
+ public static string MessageType => "empty-response";
+
+ public static JsonTypeInfo JsonInfo => throw new NotSupportedException();
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessage.cs b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessage.cs
index bcec7853..3f8db188 100644
--- a/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessage.cs
+++ b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessage.cs
@@ -12,6 +12,8 @@ static abstract JsonTypeInfo JsonInfo {
}
}
-public interface IPeerRequestMessage : IPeerMessage where TResponse : IPeerResponseMessage;
+public interface IPeerRequest : IPeerMessage where TResponse : IPeerResponse;
-public interface IPeerResponseMessage : IPeerMessage;
\ No newline at end of file
+public interface IPeerResponse : IPeerMessage {
+ static readonly Empty Empty = Empty.Instance;
+}
\ No newline at end of file
diff --git a/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageHandler.cs b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageHandler.cs
index 7b359698..67a1d4da 100644
--- a/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageHandler.cs
+++ b/src/Networking.PeerStreaming.Core.Abstractions/IPeerMessageHandler.cs
@@ -18,9 +18,9 @@ CancellationToken cancellationToken
}
public interface IPeerMessageHandler : IPeerMessageHandler
- where TRequest : IPeerRequestMessage
- where TResponse : IPeerResponseMessage {
- Task HandleAsync( TRequest message, CancellationToken cancellationToken = default );
+ where TRequest : IPeerRequest
+ where TResponse : IPeerResponse {
+ Task HandleAsync( TRequest message, CancellationToken cancellationToken = default );
async Task IPeerMessageHandler.HandleAsync(
PeerMessage envelope,
@@ -30,7 +30,7 @@ public interface IPeerMessageHandler : IPeerMessageHandler
var response = await HandleAsync( request, cancellationToken );
- if ( response is null ) {
+ if ( response is Empty ) {
return null;
}
diff --git a/src/Networking.PeerStreaming.Tests/Helpers/TestPeerMessage.cs b/src/Networking.PeerStreaming.Tests/Helpers/TestPeerMessage.cs
index bd645c9e..16db1473 100644
--- a/src/Networking.PeerStreaming.Tests/Helpers/TestPeerMessage.cs
+++ b/src/Networking.PeerStreaming.Tests/Helpers/TestPeerMessage.cs
@@ -4,7 +4,7 @@
namespace Drift.Networking.PeerStreaming.Tests.Helpers;
-internal sealed class TestPeerMessage : IPeerRequestMessage, IPeerResponseMessage {
+internal sealed class TestPeerMessage : IPeerRequest, IPeerResponse {
public static string MessageType => "test-peer-message";
public static JsonTypeInfo JsonInfo => TestPeerMessageJsonContext.Default.TestPeerMessage;
From a228194463ea1edd6e607c20044c74b4648e8b50 Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Mon, 22 Dec 2025 13:07:48 +0100
Subject: [PATCH 22/31] fix subnet discovery via agents
---
src/Agent.Hosting/AgentHost.cs | 9 +-
.../Subnets/SubnetsRequestHandler.cs | 2 +-
src/Cli.Tests/Commands/AgentCommandTests.cs | 2 +-
src/Cli.Tests/Commands/InitCommandTests.cs | 8 +-
src/Cli.Tests/Commands/LintCommandTests.cs | 6 +-
.../ScanCommandTests.RemoteScan.verified.txt | 20 ++-
src/Cli.Tests/Commands/ScanCommandTests.cs | 122 ++++++++++--------
src/Cli.Tests/ExitCodeTests.cs | 6 +-
src/Cli.Tests/FeatureFlagTest.cs | 2 +-
src/Cli.Tests/Utils/CliCommandResult.cs | 24 ++++
src/Cli.Tests/Utils/DriftTestCli.cs | 69 ++++++++--
src/Cli.Tests/Utils/RunningCliCommand.cs | 20 +++
.../Agent/Subcommands/AgentLifetime.cs | 7 +
.../Subcommands/Start/AgentStartCommand.cs | 10 +-
src/Cli/Infrastructure/RootCommandFactory.cs | 7 +-
src/Domain/Environment.cs | 2 +-
src/Networking.Cluster/Cluster.cs | 2 +-
17 files changed, 227 insertions(+), 91 deletions(-)
create mode 100644 src/Cli.Tests/Utils/CliCommandResult.cs
create mode 100644 src/Cli.Tests/Utils/RunningCliCommand.cs
create mode 100644 src/Cli/Commands/Agent/Subcommands/AgentLifetime.cs
diff --git a/src/Agent.Hosting/AgentHost.cs b/src/Agent.Hosting/AgentHost.cs
index aee866ec..9b1444e6 100644
--- a/src/Agent.Hosting/AgentHost.cs
+++ b/src/Agent.Hosting/AgentHost.cs
@@ -16,16 +16,18 @@ public static Task Run(
ushort port,
ILogger logger,
Action? configureServices,
- CancellationToken cancellationToken
+ CancellationToken cancellationToken,
+ TaskCompletionSource? ready = null
) {
- var app = Build( port, logger, configureServices );
+ var app = Build( port, logger, configureServices, ready );
return app.RunAsync( cancellationToken );
}
private static WebApplication Build(
ushort port,
ILogger logger,
- Action? configureServices = null
+ Action? configureServices = null,
+ TaskCompletionSource? ready = null
) {
var builder = WebApplication.CreateSlimBuilder();
@@ -54,6 +56,7 @@ private static WebApplication Build(
app.Lifetime.ApplicationStarted.Register( () => {
logger.LogInformation( "Listening for incoming connections on port {Port}", port );
logger.LogInformation( "Agent started" );
+ ready?.TrySetResult();
} );
app.Lifetime.ApplicationStopping.Register( () => {
logger.LogInformation( "Agent stopping..." );
diff --git a/src/Agent.PeerProtocol/Subnets/SubnetsRequestHandler.cs b/src/Agent.PeerProtocol/Subnets/SubnetsRequestHandler.cs
index dea4933c..0b12901c 100644
--- a/src/Agent.PeerProtocol/Subnets/SubnetsRequestHandler.cs
+++ b/src/Agent.PeerProtocol/Subnets/SubnetsRequestHandler.cs
@@ -10,7 +10,7 @@ ILogger logger
) : IPeerMessageHandler {
public string MessageType => SubnetsRequest.MessageType;
- public async Task HandleAsync(
+ public async Task HandleAsync(
SubnetsRequest message,
CancellationToken cancellationToken = default
) {
diff --git a/src/Cli.Tests/Commands/AgentCommandTests.cs b/src/Cli.Tests/Commands/AgentCommandTests.cs
index aea208c9..bc3e9db8 100644
--- a/src/Cli.Tests/Commands/AgentCommandTests.cs
+++ b/src/Cli.Tests/Commands/AgentCommandTests.cs
@@ -9,7 +9,7 @@ internal sealed class AgentCommandTests {
public async Task RespectsCancellationToken() {
using var tcs = new CancellationTokenSource( TimeSpan.FromSeconds( 5 ) );
- var (exitCode, output, _) = await DriftTestCli.InvokeFromTestAsync(
+ var (exitCode, output, _) = await DriftTestCli.InvokeAsync(
"agent start --adoptable",
cancellationToken: tcs.Token
);
diff --git a/src/Cli.Tests/Commands/InitCommandTests.cs b/src/Cli.Tests/Commands/InitCommandTests.cs
index 3a9a27bb..b502d1a1 100644
--- a/src/Cli.Tests/Commands/InitCommandTests.cs
+++ b/src/Cli.Tests/Commands/InitCommandTests.cs
@@ -78,7 +78,7 @@ public void TearDown() {
[Test]
public async Task MissingNameOption() {
// Arrange / Act
- var (exitCode, output, error) = await DriftTestCli.InvokeFromTestAsync( "init --overwrite" );
+ var (exitCode, output, error) = await DriftTestCli.InvokeAsync( "init --overwrite" );
// Assert
using ( Assert.EnterMultipleScope() ) {
@@ -95,7 +95,7 @@ public async Task CancellationIsRespected() {
try {
// Act
- var (exitCode, _, _) = await DriftTestCli.InvokeFromTestAsync(
+ var (exitCode, _, _) = await DriftTestCli.InvokeAsync(
"init",
cancellationToken: cancellationTokenSource.Token
);
@@ -126,7 +126,7 @@ public async Task GenerateSpecWithDiscoverySuccess(
};
// Act
- var (exitCode, output, error) = await DriftTestCli.InvokeFromTestAsync(
+ var (exitCode, output, error) = await DriftTestCli.InvokeAsync(
$"init {SpecNameWithDiscovery} --discover {outputFormat} {verbose}",
serviceConfig
);
@@ -155,7 +155,7 @@ public async Task GenerateSpecWithoutDiscoverySuccess() {
};
// Act
- var (exitCode, output, error) = await DriftTestCli.InvokeFromTestAsync(
+ var (exitCode, output, error) = await DriftTestCli.InvokeAsync(
$"init {SpecNameWithoutDiscovery}",
serviceConfig
);
diff --git a/src/Cli.Tests/Commands/LintCommandTests.cs b/src/Cli.Tests/Commands/LintCommandTests.cs
index 96bd313e..c57fda8f 100644
--- a/src/Cli.Tests/Commands/LintCommandTests.cs
+++ b/src/Cli.Tests/Commands/LintCommandTests.cs
@@ -14,7 +14,7 @@ public async Task LintValidSpec(
var outputOption = string.IsNullOrWhiteSpace( outputFormat ) ? string.Empty : $" -o {outputFormat}";
// Act
- var (exitCode, output, error) = await DriftTestCli.InvokeFromTestAsync(
+ var (exitCode, output, error) = await DriftTestCli.InvokeAsync(
$"lint ../../../../Spec.Tests/resources/{specName}.yaml" + outputOption
);
@@ -36,7 +36,7 @@ public async Task LintInvalidSpec(
var outputOption = string.IsNullOrWhiteSpace( outputFormat ) ? string.Empty : $" -o {outputFormat}";
// Act
- var (exitCode, output, error) = await DriftTestCli.InvokeFromTestAsync(
+ var (exitCode, output, error) = await DriftTestCli.InvokeAsync(
$"lint ../../../../Spec.Tests/resources/{specName}.yaml" + outputOption
);
@@ -51,7 +51,7 @@ await Verify( output.ToString() + error )
[Test]
public async Task LintMissingSpec() {
// Arrange / Act
- var (exitCode, _, _) = await DriftTestCli.InvokeFromTestAsync( "lint" );
+ var (exitCode, _, _) = await DriftTestCli.InvokeAsync( "lint" );
// Assert
Assert.That( exitCode, Is.EqualTo( ExitCodes.GeneralError ) );
diff --git a/src/Cli.Tests/Commands/ScanCommandTests.RemoteScan.verified.txt b/src/Cli.Tests/Commands/ScanCommandTests.RemoteScan.verified.txt
index b42fe364..538f05f0 100644
--- a/src/Cli.Tests/Commands/ScanCommandTests.RemoteScan.verified.txt
+++ b/src/Cli.Tests/Commands/ScanCommandTests.RemoteScan.verified.txt
@@ -1,8 +1,18 @@
Requesting subnets from agent local1
-Received subnet(s) from agent local1: 192.168.0.0/24
-
-Scanning 1 subnet
- 192.168.0.0/24 (254 addresses, estimated scan time is 00:00:05.0800000) via local, agentid_local1
+Received subnet(s) from agent local1: 192.168.10.0/24
+Requesting subnets from agent local2
+Received subnet(s) from agent local2: 192.168.20.0/24
+Scanning 3 subnets
+ 192.168.0.0/24 (254 addresses, estimated scan time is 00:00:05.0800000) via local
+ 192.168.10.0/24 (254 addresses, estimated scan time is 00:00:05.0800000) via agentid_local1
+ 192.168.20.0/24 (254 addresses, estimated scan time is 00:00:05.0800000) via agentid_local2
192.168.0.0/24 (1 devices)
-└── 192.168.0.100 11-11-11-11-11-11 Online (unknown device)
\ No newline at end of file
+└── 192.168.0.100 11-11-11-11-11-11 Online (unknown device)
+
+192.168.10.0/24 (1 devices)
+└── 192.168.10.100 22-22-22-22-22-22 Online (unknown device)
+└── 192.168.10.101 21-21-21-21-21-21 Online (unknown device)
+
+192.168.20.0/24 (1 devices)
+└── 192.168.20.100 33-33-33-33-33-33 Online (unknown device)
\ No newline at end of file
diff --git a/src/Cli.Tests/Commands/ScanCommandTests.cs b/src/Cli.Tests/Commands/ScanCommandTests.cs
index 8835f4a4..2cf7734d 100644
--- a/src/Cli.Tests/Commands/ScanCommandTests.cs
+++ b/src/Cli.Tests/Commands/ScanCommandTests.cs
@@ -14,6 +14,7 @@
using Drift.Scanning.Subnets.Interface;
using Drift.Scanning.Tests.Utils;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
using NetworkInterface = Drift.Scanning.Subnets.Interface.NetworkInterface;
namespace Drift.Cli.Tests.Commands;
@@ -170,7 +171,7 @@ List interfaces
var serviceConfig = ConfigureServices( interfaces, discoveredDevices );
// Act
- var (exitCode, output, error) = await DriftTestCli.InvokeFromTestAsync( "scan", serviceConfig );
+ var (exitCode, output, error) = await DriftTestCli.InvokeAsync( "scan", serviceConfig );
// var exitCode = await config.InvokeAsync( $"scan -o {outputFormat}" );
// Assert
@@ -202,7 +203,7 @@ List discoveredDevices
);
// Act
- var (exitCode, output, error) = await DriftTestCli.InvokeFromTestAsync( "scan unittest", serviceConfig );
+ var (exitCode, output, error) = await DriftTestCli.InvokeAsync( "scan unittest", serviceConfig );
// Assert
using ( Assert.EnterMultipleScope() ) {
@@ -216,72 +217,72 @@ await Verify( output.ToString() + error )
[Test]
public async Task RemoteScan() {
// Arrange
- var serviceConfigScan = ConfigureServices(
- [
- new NetworkInterface {
- Description = "eth1",
- OperationalStatus = OperationalStatus.Up,
- UnicastAddress = new CidrBlock( "192.168.0.0/24" )
- }
- ],
- [
- new DiscoveredDevice { Addresses = [new IpV4Address( "192.168.0.100" ), new MacAddress( "11:11:11:11:11:11" )] }
- ],
+ var scanConfig = ConfigureServices(
+ new CidrBlock( "192.168.0.0/24" ),
+ [[new IpV4Address( "192.168.0.100" ), new MacAddress( "11:11:11:11:11:11" )]],
new Inventory {
- Network = new Network(), Agents = [new Domain.Agent { Id = "local1", Address = "http://localhost:51515" }]
+ Network = new Network(),
+ Agents = [
+ new Domain.Agent { Id = "local1", Address = "http://localhost:51515" },
+ new Domain.Agent { Id = "local2", Address = "http://localhost:51516" }
+ ]
}
);
- var serviceConfigAgent = ConfigureServices(
- [
- new NetworkInterface {
- Description = "eth1",
- OperationalStatus = OperationalStatus.Up,
- UnicastAddress = new CidrBlock( "192.168.100.0/24" )
- }
- ],
- [
- new DiscoveredDevice {
- Addresses = [new IpV4Address( "192.168.100.200" ), new MacAddress( "22:22:22:22:22:22" )]
- }
- ]
+ var tcs = new CancellationTokenSource( TimeSpan.FromMinutes( 1 ) );
+
+ await using var agent1 = await DriftTestCli.StartAgentAsync(
+ "--adoptable -v",
+ ConfigureServices(
+ interfaces: new CidrBlock( "192.168.10.0/24" ),
+ discoveredDevices: [
+ [new IpV4Address( "192.168.10.100" ), new MacAddress( "22:22:22:22:22:22" )],
+ [new IpV4Address( "192.168.10.101" ), new MacAddress( "21:21:21:21:21:21" )]
+ ]
+ ),
+ tcs.Token
);
- var cts = new CancellationTokenSource( TimeSpan.FromSeconds( 800 ) );
+ await using var agent2 = await DriftTestCli.StartAgentAsync(
+ "--adoptable -v --port 51516",
+ ConfigureServices(
+ interfaces: new CidrBlock( "192.168.20.0/24" ),
+ discoveredDevices: [[new IpV4Address( "192.168.20.100" ), new MacAddress( "33:33:33:33:33:33" )]]
+ ),
+ tcs.Token
+ );
+
+ RunningCliCommand[] agents = [agent1, agent2];
// Act
- Console.WriteLine( "Invoking agent start" );
- var agentTask = DriftTestCli.InvokeFromTestAsync(
- "agent start --adoptable -v",
- serviceConfigAgent,
- cancellationToken: cts.Token
- );
- await Task.Delay( 3000, cts.Token );
- Console.WriteLine( "Invoking scan" );
- var (scanExitCode, scanOutput, scanError) = await DriftTestCli.InvokeFromTestAsync(
+ Console.WriteLine( "Starting scan..." );
+ var (scanExitCode, scanOutput, scanError) = await DriftTestCli.InvokeAsync(
"scan unittest",
- serviceConfigScan,
- cancellationToken: cts.Token
+ scanConfig,
+ cancellationToken: tcs.Token
);
+
Console.WriteLine( "Scan finished" );
Console.WriteLine( "----------------" );
Console.WriteLine( scanOutput.ToString() + scanError );
Console.WriteLine( "----------------" );
- Console.WriteLine( "Cancelling token" );
- await cts.CancelAsync();
- cts.Dispose();
- Console.WriteLine( "Waiting for agent to shut down" );
+ Console.WriteLine( "Signalling agent cancellation..." );
+ await tcs.CancelAsync();
+ tcs.Dispose();
+ Console.WriteLine( "Waiting for agents to shut down..." );
- var (agentExitCode, agentOutput, agentError) = await agentTask;
+ foreach ( var agent in agents ) {
+ var (agentExitCode, agentOutput, agentError) = await agent.Completion;
- Console.WriteLine( "Agent finished" );
- Console.WriteLine( "----------------" );
- Console.WriteLine( agentOutput.ToString() + agentError );
- Console.WriteLine( "----------------" );
+ Console.WriteLine( "Agent finished" );
+ Console.WriteLine( "----------------" );
+ Console.WriteLine( agentOutput.ToString() + agentError );
+ Console.WriteLine( "----------------" );
+ Assert.That( agentExitCode, Is.EqualTo( ExitCodes.Success ) );
+ }
// Assert
- Assert.That( agentExitCode, Is.EqualTo( ExitCodes.Success ) );
Assert.That( scanExitCode, Is.EqualTo( ExitCodes.Success ) );
await Verify( scanOutput.ToString() + scanError );
}
@@ -289,7 +290,7 @@ public async Task RemoteScan() {
[Test]
public async Task NonExistingSpecOption() {
// Arrange / Act
- var (exitCode, output, error) = await DriftTestCli.InvokeFromTestAsync( "scan blah_spec.yaml" );
+ var (exitCode, output, error) = await DriftTestCli.InvokeAsync( "scan blah_spec.yaml" );
// Assert
using ( Assert.EnterMultipleScope() ) {
@@ -298,14 +299,31 @@ public async Task NonExistingSpecOption() {
}
}
+ private static Action ConfigureServices(
+ CidrBlock interfaces,
+ List> discoveredDevices,
+ Inventory? inventory = null
+ ) {
+ return ConfigureServices(
+ [
+ new NetworkInterface {
+ Description = "eth1", OperationalStatus = OperationalStatus.Up, UnicastAddress = interfaces
+ }
+ ],
+ discoveredDevices.Select( deviceAddresses => new DiscoveredDevice { Addresses = deviceAddresses } ).ToList(),
+ inventory
+ );
+ }
+
private static Action ConfigureServices(
List interfaces,
List discoveredDevices,
Inventory? inventory = null
) {
return services => {
- services.AddScoped( _ =>
- new PredefinedInterfaceSubnetProvider( interfaces )
+ services.Replace( ServiceDescriptor.Scoped( _ =>
+ new PredefinedInterfaceSubnetProvider( interfaces )
+ )
);
if ( inventory != null ) {
diff --git a/src/Cli.Tests/ExitCodeTests.cs b/src/Cli.Tests/ExitCodeTests.cs
index 28c4d2e1..40d5fd6e 100644
--- a/src/Cli.Tests/ExitCodeTests.cs
+++ b/src/Cli.Tests/ExitCodeTests.cs
@@ -26,7 +26,7 @@ public async Task ExitCodeIsReturnedFromCommandHandlerTest() {
// Act
var (exitCode, output, error) =
- await DriftTestCli.InvokeFromTestAsync( ExitCodeCommand, customCommands: customCommands );
+ await DriftTestCli.InvokeAsync( ExitCodeCommand, customCommands: customCommands );
// Assert
using ( Assert.EnterMultipleScope() ) {
@@ -38,7 +38,7 @@ public async Task ExitCodeIsReturnedFromCommandHandlerTest() {
[Test]
public async Task NonExistingCommand_ReturnsSystemCommandLineDefaultErrorTest() {
// Arrange
- var (exitCode, output, error) = await DriftTestCli.InvokeFromTestAsync( NonExistingCommand );
+ var (exitCode, output, error) = await DriftTestCli.InvokeAsync( NonExistingCommand );
// Assert
using ( Assert.EnterMultipleScope() ) {
@@ -62,7 +62,7 @@ public async Task UnhandledExceptionReturnsUnknownErrorTest() {
// Act
var (exitCode, output, error) =
- await DriftTestCli.InvokeFromTestAsync( ExceptionThrowingCommand, customCommands: customCommands );
+ await DriftTestCli.InvokeAsync( ExceptionThrowingCommand, customCommands: customCommands );
// Assert
using ( Assert.EnterMultipleScope() ) {
diff --git a/src/Cli.Tests/FeatureFlagTest.cs b/src/Cli.Tests/FeatureFlagTest.cs
index 8af7cae0..cfba9f51 100644
--- a/src/Cli.Tests/FeatureFlagTest.cs
+++ b/src/Cli.Tests/FeatureFlagTest.cs
@@ -37,7 +37,7 @@ public async Task SettingsControlFlag( [Values( false, true, null )] bool? featu
];
// Act
- var result = await DriftTestCli.InvokeFromTestAsync( $"{DummyCodeCommand}", customCommands: customCommands );
+ var result = await DriftTestCli.InvokeAsync( $"{DummyCodeCommand}", customCommands: customCommands );
// Assert
using ( Assert.EnterMultipleScope() ) {
diff --git a/src/Cli.Tests/Utils/CliCommandResult.cs b/src/Cli.Tests/Utils/CliCommandResult.cs
new file mode 100644
index 00000000..9665fafc
--- /dev/null
+++ b/src/Cli.Tests/Utils/CliCommandResult.cs
@@ -0,0 +1,24 @@
+namespace Drift.Cli.Tests.Utils;
+
+internal sealed class CliCommandResult {
+ internal required int ExitCode {
+ get;
+ init;
+ }
+
+ internal required TextWriter Output {
+ get;
+ init;
+ }
+
+ internal required TextWriter Error {
+ get;
+ init;
+ }
+
+ public void Deconstruct( out int exitCode, out TextWriter output, out TextWriter error ) {
+ exitCode = ExitCode;
+ output = Output;
+ error = Error;
+ }
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Utils/DriftTestCli.cs b/src/Cli.Tests/Utils/DriftTestCli.cs
index 0ab879ce..da3610dc 100644
--- a/src/Cli.Tests/Utils/DriftTestCli.cs
+++ b/src/Cli.Tests/Utils/DriftTestCli.cs
@@ -1,5 +1,6 @@
using System.CommandLine;
using System.CommandLine.Parsing;
+using Drift.Cli.Commands.Agent.Subcommands;
using Drift.Cli.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
@@ -8,7 +9,7 @@ namespace Drift.Cli.Tests.Utils;
internal static class DriftTestCli {
private static readonly TimeSpan DefaultCommandTimeout = TimeSpan.FromSeconds( 7 );
- internal static async Task<(int ExitCode, TextWriter Output, TextWriter Error )> InvokeFromTestAsync(
+ internal static async Task InvokeAsync(
string args,
Action? configureServices = null,
RootCommandFactory.CommandRegistration[]? customCommands = null,
@@ -31,22 +32,64 @@ void ConfigureInvocation( InvocationConfiguration config ) {
}
try {
- return (
- await DriftCli.InvokeAsync(
- CommandLineParser.SplitCommandLine( args ).ToArray(),
- false,
- true,
- configureServices,
- customCommands,
- ConfigureInvocation,
- token
- ),
- output,
- error
+ var exitCode = await DriftCli.InvokeAsync(
+ CommandLineParser.SplitCommandLine( args ).ToArray(),
+ false,
+ true,
+ configureServices,
+ customCommands,
+ ConfigureInvocation,
+ token
);
+
+ return new CliCommandResult { ExitCode = exitCode, Output = output, Error = error };
}
finally {
cancellationTokenSource?.Dispose();
}
}
+
+ internal static RunningCliCommand StartAsync(
+ string args,
+ Action configureServices,
+ CancellationToken testToken
+ ) {
+ var cts = CancellationTokenSource.CreateLinkedTokenSource( testToken );
+
+ var task = InvokeAsync(
+ args,
+ configureServices,
+ cancellationToken: cts.Token
+ );
+
+ return new RunningCliCommand( task, cts );
+ }
+
+ internal static async Task StartAgentAsync(
+ string args,
+ Action configureServices,
+ CancellationToken testToken
+ ) {
+ var readyTcs = new AgentLifetime();
+
+ var command = StartAsync(
+ "agent start " + args,
+ services => {
+ services.AddSingleton( readyTcs );
+ configureServices( services );
+ },
+ testToken
+ );
+
+ // Wait for either readiness or command exit
+ var completed = await Task.WhenAny( readyTcs.Ready.Task, command.Completion );
+
+ if ( completed == command.Completion ) {
+ throw new InvalidOperationException( "Command exited before agent was started" );
+ }
+
+ return command;
+ }
+
+
}
\ No newline at end of file
diff --git a/src/Cli.Tests/Utils/RunningCliCommand.cs b/src/Cli.Tests/Utils/RunningCliCommand.cs
new file mode 100644
index 00000000..00fa3eba
--- /dev/null
+++ b/src/Cli.Tests/Utils/RunningCliCommand.cs
@@ -0,0 +1,20 @@
+namespace Drift.Cli.Tests.Utils;
+
+internal sealed class RunningCliCommand : IAsyncDisposable {
+ private readonly CancellationTokenSource _cts;
+
+ internal RunningCliCommand( Task task, CancellationTokenSource cts ) {
+ Completion = task;
+ _cts = cts;
+ }
+
+ public Task Completion {
+ get;
+ }
+
+ public async ValueTask DisposeAsync() {
+ await _cts.CancelAsync();
+ await Completion;
+ _cts.Dispose();
+ }
+}
\ No newline at end of file
diff --git a/src/Cli/Commands/Agent/Subcommands/AgentLifetime.cs b/src/Cli/Commands/Agent/Subcommands/AgentLifetime.cs
new file mode 100644
index 00000000..416ee801
--- /dev/null
+++ b/src/Cli/Commands/Agent/Subcommands/AgentLifetime.cs
@@ -0,0 +1,7 @@
+namespace Drift.Cli.Commands.Agent.Subcommands;
+
+internal sealed class AgentLifetime {
+ public TaskCompletionSource Ready {
+ get;
+ } = new();
+}
\ No newline at end of file
diff --git a/src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs b/src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs
index 1ee0b8f6..061db09c 100644
--- a/src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs
+++ b/src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs
@@ -23,7 +23,12 @@ protected override AgentStartParameters CreateParameters( ParseResult result ) {
}
}
-internal class AgentStartCommandHandler( IOutputManager output ) : ICommandHandler {
+internal class AgentStartCommandHandler(
+ IOutputManager output,
+ AgentLifetime? agentLifetime,
+ Action? configureServicesOverride
+)
+ : ICommandHandler {
public async Task Invoke( AgentStartParameters parameters, CancellationToken cancellationToken ) {
output.Log.LogDebug( "Running 'agent start' command" );
var logger = output.GetLogger();
@@ -52,7 +57,7 @@ public async Task Invoke( AgentStartParameters parameters, CancellationToke
output.Log.LogDebug( "Starting agent..." );
- await AgentHost.Run( parameters.Port, logger, ConfigureServices, cancellationToken );
+ await AgentHost.Run( parameters.Port, logger, ConfigureServices, cancellationToken, agentLifetime?.Ready );
output.Log.LogDebug( "Completed 'agent start' command" );
@@ -61,6 +66,7 @@ public async Task Invoke( AgentStartParameters parameters, CancellationToke
void ConfigureServices( IServiceCollection services ) {
RootCommandFactory.ConfigureSubnetProvider( services );
services.AddPeerProtocol();
+ configureServicesOverride?.Invoke( services );
}
}
diff --git a/src/Cli/Infrastructure/RootCommandFactory.cs b/src/Cli/Infrastructure/RootCommandFactory.cs
index a7d46500..5687deaa 100644
--- a/src/Cli/Infrastructure/RootCommandFactory.cs
+++ b/src/Cli/Infrastructure/RootCommandFactory.cs
@@ -54,7 +54,12 @@ internal static RootCommand Create(
ConfigureDefaults( services, toConsole, plainConsole );
ConfigureBuiltInCommandHandlers( services );
ConfigureDynamicCommands( services, customCommands ?? [] );
- configureServices?.Invoke( services );
+
+ if ( configureServices != null ) {
+ configureServices.Invoke( services );
+ // Allow agent host to override it's services with the same configuration
+ services.AddScoped>( _ => configureServices );
+ }
var provider = services.BuildServiceProvider();
var rootCommand = CreateRootCommand( provider );
diff --git a/src/Domain/Environment.cs b/src/Domain/Environment.cs
index 4a0a52a6..e56727f2 100644
--- a/src/Domain/Environment.cs
+++ b/src/Domain/Environment.cs
@@ -28,7 +28,7 @@ public List Agents {
}
public record Agent {
- public string Id {
+ public string Id { // TODO use AgentId???? or should that only be for internal use
get;
set;
}
diff --git a/src/Networking.Cluster/Cluster.cs b/src/Networking.Cluster/Cluster.cs
index 58df5779..f9576699 100644
--- a/src/Networking.Cluster/Cluster.cs
+++ b/src/Networking.Cluster/Cluster.cs
@@ -67,7 +67,7 @@ public async Task SendAndWaitAsync(
);
// Request
- var connection = peerStreamManager.GetOrCreate( new Uri( agent.Address ), "agentid_local1" );
+ var connection = peerStreamManager.GetOrCreate( new Uri( agent.Address ), "agentid_" + agent.Id );
await connection.SendAsync( envelope );
// Response
From fa18f25dc1596d34383dae97ac2a054da2b1ea2f Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Mon, 22 Dec 2025 13:16:10 +0100
Subject: [PATCH 23/31] f
---
...ommandTests.Remote.RemoteScan.verified.txt | 18 +++++
.../Commands/ScanCommandTests.Remote.cs | 81 +++++++++++++++++++
src/Cli.Tests/Commands/ScanCommandTests.cs | 75 +----------------
3 files changed, 100 insertions(+), 74 deletions(-)
create mode 100644 src/Cli.Tests/Commands/ScanCommandTests.Remote.RemoteScan.verified.txt
create mode 100644 src/Cli.Tests/Commands/ScanCommandTests.Remote.cs
diff --git a/src/Cli.Tests/Commands/ScanCommandTests.Remote.RemoteScan.verified.txt b/src/Cli.Tests/Commands/ScanCommandTests.Remote.RemoteScan.verified.txt
new file mode 100644
index 00000000..538f05f0
--- /dev/null
+++ b/src/Cli.Tests/Commands/ScanCommandTests.Remote.RemoteScan.verified.txt
@@ -0,0 +1,18 @@
+Requesting subnets from agent local1
+Received subnet(s) from agent local1: 192.168.10.0/24
+Requesting subnets from agent local2
+Received subnet(s) from agent local2: 192.168.20.0/24
+Scanning 3 subnets
+ 192.168.0.0/24 (254 addresses, estimated scan time is 00:00:05.0800000) via local
+ 192.168.10.0/24 (254 addresses, estimated scan time is 00:00:05.0800000) via agentid_local1
+ 192.168.20.0/24 (254 addresses, estimated scan time is 00:00:05.0800000) via agentid_local2
+
+192.168.0.0/24 (1 devices)
+└── 192.168.0.100 11-11-11-11-11-11 Online (unknown device)
+
+192.168.10.0/24 (1 devices)
+└── 192.168.10.100 22-22-22-22-22-22 Online (unknown device)
+└── 192.168.10.101 21-21-21-21-21-21 Online (unknown device)
+
+192.168.20.0/24 (1 devices)
+└── 192.168.20.100 33-33-33-33-33-33 Online (unknown device)
\ No newline at end of file
diff --git a/src/Cli.Tests/Commands/ScanCommandTests.Remote.cs b/src/Cli.Tests/Commands/ScanCommandTests.Remote.cs
new file mode 100644
index 00000000..95fda5be
--- /dev/null
+++ b/src/Cli.Tests/Commands/ScanCommandTests.Remote.cs
@@ -0,0 +1,81 @@
+using Drift.Cli.Abstractions;
+using Drift.Cli.Tests.Utils;
+using Drift.Domain;
+using Drift.Domain.Device.Addresses;
+
+namespace Drift.Cli.Tests.Commands;
+
+internal sealed partial class ScanCommandTests {
+ [Test]
+ public async Task RemoteScan() {
+ // Arrange
+ var scanConfig = ConfigureServices(
+ new CidrBlock( "192.168.0.0/24" ),
+ [[new IpV4Address( "192.168.0.100" ), new MacAddress( "11:11:11:11:11:11" )]],
+ new Inventory {
+ Network = new Network(),
+ Agents = [
+ new Domain.Agent { Id = "local1", Address = "http://localhost:51515" },
+ new Domain.Agent { Id = "local2", Address = "http://localhost:51516" }
+ ]
+ }
+ );
+
+ using var tcs = new CancellationTokenSource( TimeSpan.FromMinutes( 1 ) );
+
+ Console.WriteLine( "Starting agents..." );
+ RunningCliCommand[] agents = [
+ await DriftTestCli.StartAgentAsync(
+ "--adoptable -v",
+ ConfigureServices(
+ interfaces: new CidrBlock( "192.168.10.0/24" ),
+ discoveredDevices: [
+ [new IpV4Address( "192.168.10.100" ), new MacAddress( "22:22:22:22:22:22" )],
+ [new IpV4Address( "192.168.10.101" ), new MacAddress( "21:21:21:21:21:21" )]
+ ]
+ ),
+ tcs.Token
+ ),
+ await DriftTestCli.StartAgentAsync(
+ "--adoptable -v --port 51516",
+ ConfigureServices(
+ interfaces: new CidrBlock( "192.168.20.0/24" ),
+ discoveredDevices: [[new IpV4Address( "192.168.20.100" ), new MacAddress( "33:33:33:33:33:33" )]]
+ ),
+ tcs.Token
+ )
+ ];
+
+ // Act
+ Console.WriteLine( "Starting scan..." );
+ var (scanExitCode, scanOutput, scanError) = await DriftTestCli.InvokeAsync(
+ "scan unittest",
+ scanConfig,
+ cancellationToken: tcs.Token
+ );
+
+ Console.WriteLine( "Scan finished" );
+ Console.WriteLine( "----------------" );
+ Console.WriteLine( scanOutput.ToString() + scanError );
+ Console.WriteLine( "----------------" );
+
+ Console.WriteLine( "Signalling agent cancellation..." );
+ await tcs.CancelAsync();
+ Console.WriteLine( "Waiting for agents to shut down..." );
+
+ foreach ( var agent in agents ) {
+ var (agentExitCode, agentOutput, agentError) = await agent.Completion;
+
+ Console.WriteLine( "Agent finished" );
+ Console.WriteLine( "----------------" );
+ Console.WriteLine( agentOutput.ToString() + agentError );
+ Console.WriteLine( "----------------" );
+
+ Assert.That( agentExitCode, Is.EqualTo( ExitCodes.Success ) );
+ }
+
+ // Assert
+ Assert.That( scanExitCode, Is.EqualTo( ExitCodes.Success ) );
+ await Verify( scanOutput.ToString() + scanError );
+ }
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Commands/ScanCommandTests.cs b/src/Cli.Tests/Commands/ScanCommandTests.cs
index 2cf7734d..df871478 100644
--- a/src/Cli.Tests/Commands/ScanCommandTests.cs
+++ b/src/Cli.Tests/Commands/ScanCommandTests.cs
@@ -19,7 +19,7 @@
namespace Drift.Cli.Tests.Commands;
-internal sealed class ScanCommandTests {
+internal sealed partial class ScanCommandTests {
private static readonly INetworkInterface DefaultInterface = new NetworkInterface {
Description = "eth0", OperationalStatus = OperationalStatus.Up, UnicastAddress = new CidrBlock( "192.168.0.0/24" )
};
@@ -214,79 +214,6 @@ await Verify( output.ToString() + error )
}
}
- [Test]
- public async Task RemoteScan() {
- // Arrange
- var scanConfig = ConfigureServices(
- new CidrBlock( "192.168.0.0/24" ),
- [[new IpV4Address( "192.168.0.100" ), new MacAddress( "11:11:11:11:11:11" )]],
- new Inventory {
- Network = new Network(),
- Agents = [
- new Domain.Agent { Id = "local1", Address = "http://localhost:51515" },
- new Domain.Agent { Id = "local2", Address = "http://localhost:51516" }
- ]
- }
- );
-
- var tcs = new CancellationTokenSource( TimeSpan.FromMinutes( 1 ) );
-
- await using var agent1 = await DriftTestCli.StartAgentAsync(
- "--adoptable -v",
- ConfigureServices(
- interfaces: new CidrBlock( "192.168.10.0/24" ),
- discoveredDevices: [
- [new IpV4Address( "192.168.10.100" ), new MacAddress( "22:22:22:22:22:22" )],
- [new IpV4Address( "192.168.10.101" ), new MacAddress( "21:21:21:21:21:21" )]
- ]
- ),
- tcs.Token
- );
-
- await using var agent2 = await DriftTestCli.StartAgentAsync(
- "--adoptable -v --port 51516",
- ConfigureServices(
- interfaces: new CidrBlock( "192.168.20.0/24" ),
- discoveredDevices: [[new IpV4Address( "192.168.20.100" ), new MacAddress( "33:33:33:33:33:33" )]]
- ),
- tcs.Token
- );
-
- RunningCliCommand[] agents = [agent1, agent2];
-
- // Act
- Console.WriteLine( "Starting scan..." );
- var (scanExitCode, scanOutput, scanError) = await DriftTestCli.InvokeAsync(
- "scan unittest",
- scanConfig,
- cancellationToken: tcs.Token
- );
-
- Console.WriteLine( "Scan finished" );
- Console.WriteLine( "----------------" );
- Console.WriteLine( scanOutput.ToString() + scanError );
- Console.WriteLine( "----------------" );
-
- Console.WriteLine( "Signalling agent cancellation..." );
- await tcs.CancelAsync();
- tcs.Dispose();
- Console.WriteLine( "Waiting for agents to shut down..." );
-
- foreach ( var agent in agents ) {
- var (agentExitCode, agentOutput, agentError) = await agent.Completion;
-
- Console.WriteLine( "Agent finished" );
- Console.WriteLine( "----------------" );
- Console.WriteLine( agentOutput.ToString() + agentError );
- Console.WriteLine( "----------------" );
- Assert.That( agentExitCode, Is.EqualTo( ExitCodes.Success ) );
- }
-
- // Assert
- Assert.That( scanExitCode, Is.EqualTo( ExitCodes.Success ) );
- await Verify( scanOutput.ToString() + scanError );
- }
-
[Test]
public async Task NonExistingSpecOption() {
// Arrange / Act
From b8c33f190f9be436ea892c80ddf39c20b90e319e Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Mon, 22 Dec 2025 13:18:53 +0100
Subject: [PATCH 24/31] f
---
src/Cli.Tests/Commands/ScanCommandTests.cs | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/Cli.Tests/Commands/ScanCommandTests.cs b/src/Cli.Tests/Commands/ScanCommandTests.cs
index df871478..f06af80e 100644
--- a/src/Cli.Tests/Commands/ScanCommandTests.cs
+++ b/src/Cli.Tests/Commands/ScanCommandTests.cs
@@ -20,6 +20,8 @@
namespace Drift.Cli.Tests.Commands;
internal sealed partial class ScanCommandTests {
+ private const string SpecName = "unittest";
+
private static readonly INetworkInterface DefaultInterface = new NetworkInterface {
Description = "eth0", OperationalStatus = OperationalStatus.Up, UnicastAddress = new CidrBlock( "192.168.0.0/24" )
};
@@ -203,7 +205,7 @@ List discoveredDevices
);
// Act
- var (exitCode, output, error) = await DriftTestCli.InvokeAsync( "scan unittest", serviceConfig );
+ var (exitCode, output, error) = await DriftTestCli.InvokeAsync( $"scan {SpecName}", serviceConfig );
// Assert
using ( Assert.EnterMultipleScope() ) {
@@ -255,7 +257,7 @@ private static Action ConfigureServices(
if ( inventory != null ) {
services.AddScoped( _ =>
- new PredefinedSpecProvider( new Dictionary { { "unittest", inventory } } )
+ new PredefinedSpecProvider( new Dictionary { { SpecName, inventory } } )
);
}
From 3377c73b54fdf665cdd161f7359bfc4d839a2016 Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Mon, 22 Dec 2025 13:21:45 +0100
Subject: [PATCH 25/31] f
---
src/Cli.Tests/Utils/DriftTestCli.cs | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/src/Cli.Tests/Utils/DriftTestCli.cs b/src/Cli.Tests/Utils/DriftTestCli.cs
index da3610dc..e21dff3a 100644
--- a/src/Cli.Tests/Utils/DriftTestCli.cs
+++ b/src/Cli.Tests/Utils/DriftTestCli.cs
@@ -52,9 +52,9 @@ void ConfigureInvocation( InvocationConfiguration config ) {
internal static RunningCliCommand StartAsync(
string args,
Action configureServices,
- CancellationToken testToken
+ CancellationToken cancellationToken
) {
- var cts = CancellationTokenSource.CreateLinkedTokenSource( testToken );
+ var cts = CancellationTokenSource.CreateLinkedTokenSource( cancellationToken );
var task = InvokeAsync(
args,
@@ -65,10 +65,13 @@ CancellationToken testToken
return new RunningCliCommand( task, cts );
}
+ ///
+ /// Starts a new agent asynchronously and waits for it to be ready.
+ ///
internal static async Task StartAgentAsync(
string args,
Action configureServices,
- CancellationToken testToken
+ CancellationToken cancellationToken
) {
var readyTcs = new AgentLifetime();
@@ -78,7 +81,7 @@ CancellationToken testToken
services.AddSingleton( readyTcs );
configureServices( services );
},
- testToken
+ cancellationToken
);
// Wait for either readiness or command exit
@@ -90,6 +93,4 @@ CancellationToken testToken
return command;
}
-
-
}
\ No newline at end of file
From 000c668da2dcca021879a74b51092d57d96fa28b Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Mon, 22 Dec 2025 13:41:31 +0100
Subject: [PATCH 26/31] f
---
.../AgentCommandTests.MissingOption.verified.txt | 1 +
src/Cli.Tests/Commands/AgentCommandTests.cs | 12 ++++++++++++
src/Cli.Tests/Commands/ScanCommandTests.Remote.cs | 12 ++++++------
.../Agent/Subcommands/Start/AgentStartCommand.cs | 7 +++++--
src/Networking.PeerStreaming.Core/PeerStream.cs | 2 +-
5 files changed, 25 insertions(+), 9 deletions(-)
create mode 100644 src/Cli.Tests/Commands/AgentCommandTests.MissingOption.verified.txt
diff --git a/src/Cli.Tests/Commands/AgentCommandTests.MissingOption.verified.txt b/src/Cli.Tests/Commands/AgentCommandTests.MissingOption.verified.txt
new file mode 100644
index 00000000..e8825647
--- /dev/null
+++ b/src/Cli.Tests/Commands/AgentCommandTests.MissingOption.verified.txt
@@ -0,0 +1 @@
+✗ Either --adoptable or --join must be specified.
\ No newline at end of file
diff --git a/src/Cli.Tests/Commands/AgentCommandTests.cs b/src/Cli.Tests/Commands/AgentCommandTests.cs
index bc3e9db8..688075a9 100644
--- a/src/Cli.Tests/Commands/AgentCommandTests.cs
+++ b/src/Cli.Tests/Commands/AgentCommandTests.cs
@@ -18,4 +18,16 @@ public async Task RespectsCancellationToken() {
Assert.That( exitCode, Is.EqualTo( ExitCodes.Success ) );
}
+
+ [Test]
+ public async Task MissingOption() {
+ var (exitCode, output, error) = await DriftTestCli.InvokeAsync( "agent start" );
+
+ Console.WriteLine( output.ToString() + error );
+
+ await Assert.MultipleAsync( async () => {
+ Assert.That( exitCode, Is.EqualTo( ExitCodes.GeneralError ) );
+ await Verify( output.ToString() + error );
+ } );
+ }
}
\ No newline at end of file
diff --git a/src/Cli.Tests/Commands/ScanCommandTests.Remote.cs b/src/Cli.Tests/Commands/ScanCommandTests.Remote.cs
index 95fda5be..afdeaee4 100644
--- a/src/Cli.Tests/Commands/ScanCommandTests.Remote.cs
+++ b/src/Cli.Tests/Commands/ScanCommandTests.Remote.cs
@@ -26,7 +26,7 @@ public async Task RemoteScan() {
Console.WriteLine( "Starting agents..." );
RunningCliCommand[] agents = [
await DriftTestCli.StartAgentAsync(
- "--adoptable -v",
+ "--adoptable",
ConfigureServices(
interfaces: new CidrBlock( "192.168.10.0/24" ),
discoveredDevices: [
@@ -37,7 +37,7 @@ await DriftTestCli.StartAgentAsync(
tcs.Token
),
await DriftTestCli.StartAgentAsync(
- "--adoptable -v --port 51516",
+ "--adoptable --port 51516",
ConfigureServices(
interfaces: new CidrBlock( "192.168.20.0/24" ),
discoveredDevices: [[new IpV4Address( "192.168.20.100" ), new MacAddress( "33:33:33:33:33:33" )]]
@@ -54,10 +54,10 @@ await DriftTestCli.StartAgentAsync(
cancellationToken: tcs.Token
);
- Console.WriteLine( "Scan finished" );
+ Console.WriteLine( "\nScan finished" );
Console.WriteLine( "----------------" );
Console.WriteLine( scanOutput.ToString() + scanError );
- Console.WriteLine( "----------------" );
+ Console.WriteLine( "----------------\n" );
Console.WriteLine( "Signalling agent cancellation..." );
await tcs.CancelAsync();
@@ -66,10 +66,10 @@ await DriftTestCli.StartAgentAsync(
foreach ( var agent in agents ) {
var (agentExitCode, agentOutput, agentError) = await agent.Completion;
- Console.WriteLine( "Agent finished" );
+ Console.WriteLine( "\nAgent finished" );
Console.WriteLine( "----------------" );
Console.WriteLine( agentOutput.ToString() + agentError );
- Console.WriteLine( "----------------" );
+ Console.WriteLine( "----------------\n" );
Assert.That( agentExitCode, Is.EqualTo( ExitCodes.Success ) );
}
diff --git a/src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs b/src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs
index 061db09c..2319d23c 100644
--- a/src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs
+++ b/src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs
@@ -25,14 +25,17 @@ protected override AgentStartParameters CreateParameters( ParseResult result ) {
internal class AgentStartCommandHandler(
IOutputManager output,
- AgentLifetime? agentLifetime,
- Action? configureServicesOverride
+ AgentLifetime? agentLifetime = null,
+ Action? configureServicesOverride = null
)
: ICommandHandler {
public async Task Invoke( AgentStartParameters parameters, CancellationToken cancellationToken ) {
output.Log.LogDebug( "Running 'agent start' command" );
+
var logger = output.GetLogger();
+ logger.LogInformation( "Agent starting" );
+
var identity = LoadAgentIdentity();
if ( identity == null ) {
diff --git a/src/Networking.PeerStreaming.Core/PeerStream.cs b/src/Networking.PeerStreaming.Core/PeerStream.cs
index aa6484d7..a8846e9d 100644
--- a/src/Networking.PeerStreaming.Core/PeerStream.cs
+++ b/src/Networking.PeerStreaming.Core/PeerStream.cs
@@ -8,7 +8,7 @@
namespace Drift.Networking.PeerStreaming.Core;
public sealed class PeerStream : IPeerStream {
- private static int _instanceCounter;
+ private static int _instanceCounter; // Being static is not ideal for testing with multiple instances
private readonly IAsyncStreamReader _reader;
private readonly IAsyncStreamWriter _writer;
private readonly PeerMessageDispatcher _dispatcher;
From 09ae92276fa08aafdcfa6f430205276e2ce1c4fb Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Mon, 29 Dec 2025 22:03:48 +0100
Subject: [PATCH 27/31] f
---
...ommandTests.SuccessfulStartup.verified.txt | 6 ++++
src/Cli.Tests/Commands/AgentCommandTests.cs | 30 +++++++++++++++----
.../Commands/ScanCommandTests.Remote.cs | 8 ++---
src/Cli.Tests/Utils/DriftTestCli.cs | 11 +++----
src/Cli/Commands/Agent/AgentCommand.cs | 2 +-
.../Subcommands/Start/AgentStartCommand.cs | 2 +-
6 files changed, 42 insertions(+), 17 deletions(-)
create mode 100644 src/Cli.Tests/Commands/AgentCommandTests.SuccessfulStartup.verified.txt
diff --git a/src/Cli.Tests/Commands/AgentCommandTests.SuccessfulStartup.verified.txt b/src/Cli.Tests/Commands/AgentCommandTests.SuccessfulStartup.verified.txt
new file mode 100644
index 00000000..1cab4e3a
--- /dev/null
+++ b/src/Cli.Tests/Commands/AgentCommandTests.SuccessfulStartup.verified.txt
@@ -0,0 +1,6 @@
+Agent starting..
+Agent cluster enrollment method is Adoption
+Listening for incoming connections on port 51515
+Agent started
+Agent stopping...
+Agent stopped
diff --git a/src/Cli.Tests/Commands/AgentCommandTests.cs b/src/Cli.Tests/Commands/AgentCommandTests.cs
index 688075a9..b8c1274f 100644
--- a/src/Cli.Tests/Commands/AgentCommandTests.cs
+++ b/src/Cli.Tests/Commands/AgentCommandTests.cs
@@ -4,10 +4,10 @@
namespace Drift.Cli.Tests.Commands;
internal sealed class AgentCommandTests {
- [CancelAfter( 10000 )]
+ [CancelAfter( 3000 )]
[Test]
public async Task RespectsCancellationToken() {
- using var tcs = new CancellationTokenSource( TimeSpan.FromSeconds( 5 ) );
+ using var tcs = new CancellationTokenSource( TimeSpan.FromSeconds( 2000 ) );
var (exitCode, output, _) = await DriftTestCli.InvokeAsync(
"agent start --adoptable",
@@ -19,15 +19,33 @@ public async Task RespectsCancellationToken() {
Assert.That( exitCode, Is.EqualTo( ExitCodes.Success ) );
}
+ [Test]
+ public async Task SuccessfulStartup() {
+ using var tcs = new CancellationTokenSource();
+
+ var runningCommand = await DriftTestCli.StartAgentAsync(
+ "--adoptable",
+ cancellationToken: tcs.Token
+ );
+
+ await tcs.CancelAsync();
+
+ var (exitCode, output, error) = await runningCommand.Completion;
+
+ using ( Assert.EnterMultipleScope() ) {
+ Assert.That( exitCode, Is.EqualTo( ExitCodes.Success ) );
+ await Verify( output.ToString() );
+ Assert.That( error.ToString(), Is.Empty );
+ }
+ }
+
[Test]
public async Task MissingOption() {
var (exitCode, output, error) = await DriftTestCli.InvokeAsync( "agent start" );
- Console.WriteLine( output.ToString() + error );
-
- await Assert.MultipleAsync( async () => {
+ using ( Assert.EnterMultipleScope() ) {
Assert.That( exitCode, Is.EqualTo( ExitCodes.GeneralError ) );
await Verify( output.ToString() + error );
- } );
+ }
}
}
\ No newline at end of file
diff --git a/src/Cli.Tests/Commands/ScanCommandTests.Remote.cs b/src/Cli.Tests/Commands/ScanCommandTests.Remote.cs
index afdeaee4..edf40259 100644
--- a/src/Cli.Tests/Commands/ScanCommandTests.Remote.cs
+++ b/src/Cli.Tests/Commands/ScanCommandTests.Remote.cs
@@ -27,22 +27,22 @@ public async Task RemoteScan() {
RunningCliCommand[] agents = [
await DriftTestCli.StartAgentAsync(
"--adoptable",
+ tcs.Token,
ConfigureServices(
interfaces: new CidrBlock( "192.168.10.0/24" ),
discoveredDevices: [
[new IpV4Address( "192.168.10.100" ), new MacAddress( "22:22:22:22:22:22" )],
[new IpV4Address( "192.168.10.101" ), new MacAddress( "21:21:21:21:21:21" )]
]
- ),
- tcs.Token
+ )
),
await DriftTestCli.StartAgentAsync(
"--adoptable --port 51516",
+ tcs.Token,
ConfigureServices(
interfaces: new CidrBlock( "192.168.20.0/24" ),
discoveredDevices: [[new IpV4Address( "192.168.20.100" ), new MacAddress( "33:33:33:33:33:33" )]]
- ),
- tcs.Token
+ )
)
];
diff --git a/src/Cli.Tests/Utils/DriftTestCli.cs b/src/Cli.Tests/Utils/DriftTestCli.cs
index e21dff3a..d33141d8 100644
--- a/src/Cli.Tests/Utils/DriftTestCli.cs
+++ b/src/Cli.Tests/Utils/DriftTestCli.cs
@@ -66,12 +66,12 @@ CancellationToken cancellationToken
}
///
- /// Starts a new agent asynchronously and waits for it to be ready.
+ /// Starts a new agent asynchronously and returns tasks that complete when it has started.
///
internal static async Task StartAgentAsync(
string args,
- Action configureServices,
- CancellationToken cancellationToken
+ CancellationToken cancellationToken,
+ Action? configureServices = null
) {
var readyTcs = new AgentLifetime();
@@ -79,7 +79,7 @@ CancellationToken cancellationToken
"agent start " + args,
services => {
services.AddSingleton( readyTcs );
- configureServices( services );
+ configureServices?.Invoke( services );
},
cancellationToken
);
@@ -88,7 +88,8 @@ CancellationToken cancellationToken
var completed = await Task.WhenAny( readyTcs.Ready.Task, command.Completion );
if ( completed == command.Completion ) {
- throw new InvalidOperationException( "Command exited before agent was started" );
+ var com = await command.Completion;
+ throw new InvalidOperationException( "Command exited before agent was started. Details:\n" + com.Error );
}
return command;
diff --git a/src/Cli/Commands/Agent/AgentCommand.cs b/src/Cli/Commands/Agent/AgentCommand.cs
index 09eb43d3..b7d42b85 100644
--- a/src/Cli/Commands/Agent/AgentCommand.cs
+++ b/src/Cli/Commands/Agent/AgentCommand.cs
@@ -4,7 +4,7 @@
namespace Drift.Cli.Commands.Agent;
internal class AgentCommand : ContainerCommandBase {
- internal AgentCommand( IServiceProvider provider ) : base( "agent", "Manage the local Drift agent (PREVIEW)" ) {
+ internal AgentCommand( IServiceProvider provider ) : base( "agent", "Manage the local Drift agent" ) {
Subcommands.Add( new AgentStartCommand( provider ) );
// Subcommands.Add( new AgentServiceCommand( provider ) );
diff --git a/src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs b/src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs
index 2319d23c..8b25b754 100644
--- a/src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs
+++ b/src/Cli/Commands/Agent/Subcommands/Start/AgentStartCommand.cs
@@ -34,7 +34,7 @@ public async Task Invoke( AgentStartParameters parameters, CancellationToke
var logger = output.GetLogger();
- logger.LogInformation( "Agent starting" );
+ logger.LogInformation( "Agent starting.." );
var identity = LoadAgentIdentity();
From 557903873cfaa0c15cc44619d732ef52c80a5656 Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Mon, 29 Dec 2025 22:05:59 +0100
Subject: [PATCH 28/31] f
---
src/Cli.Tests/Commands/AgentCommandTests.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Cli.Tests/Commands/AgentCommandTests.cs b/src/Cli.Tests/Commands/AgentCommandTests.cs
index b8c1274f..18b2a018 100644
--- a/src/Cli.Tests/Commands/AgentCommandTests.cs
+++ b/src/Cli.Tests/Commands/AgentCommandTests.cs
@@ -7,7 +7,7 @@ internal sealed class AgentCommandTests {
[CancelAfter( 3000 )]
[Test]
public async Task RespectsCancellationToken() {
- using var tcs = new CancellationTokenSource( TimeSpan.FromSeconds( 2000 ) );
+ using var tcs = new CancellationTokenSource( TimeSpan.FromMilliseconds( 2000 ) );
var (exitCode, output, _) = await DriftTestCli.InvokeAsync(
"agent start --adoptable",
From 655845c553a795ba5b77dc35cfc451aa99b7fea7 Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Mon, 29 Dec 2025 22:46:20 +0100
Subject: [PATCH 29/31] remove legacy Grpc.Core references
use Grpc.Core.Api instead
---
Directory.Packages.props | 8 ++++----
.../Build.Utilities.Tests.csproj | 4 ++--
build/_build.csproj | 9 +++++----
src/Common/Common.csproj | 4 ++--
.../FeatureFlagsDELETE.Tests.csproj | 8 ++++----
.../Networking.PeerStreaming.Client.csproj | 1 -
.../Networking.PeerStreaming.Core.csproj | 4 ----
.../Networking.PeerStreaming.Grpc.csproj | 2 +-
.../Networking.PeerStreaming.Server.csproj | 4 ++--
.../Networking.PeerStreaming.Tests.csproj | 12 ++++++------
10 files changed, 26 insertions(+), 30 deletions(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 59f3b0ef..3e868eb6 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -5,10 +5,10 @@
true
-
-
-
-
+
+
+
+
diff --git a/build-utils/Build.Utilities.Tests/Build.Utilities.Tests.csproj b/build-utils/Build.Utilities.Tests/Build.Utilities.Tests.csproj
index 2a3d630e..56d8698c 100644
--- a/build-utils/Build.Utilities.Tests/Build.Utilities.Tests.csproj
+++ b/build-utils/Build.Utilities.Tests/Build.Utilities.Tests.csproj
@@ -1,11 +1,11 @@
-
+
-
+
diff --git a/build/_build.csproj b/build/_build.csproj
index 6b7e5a43..fc36a1e0 100644
--- a/build/_build.csproj
+++ b/build/_build.csproj
@@ -13,14 +13,15 @@
+
+
+
+
+
-
-
-
-
diff --git a/src/Common/Common.csproj b/src/Common/Common.csproj
index 2bdadc5e..a49980c3 100644
--- a/src/Common/Common.csproj
+++ b/src/Common/Common.csproj
@@ -1,11 +1,11 @@
-
+
-
+
\ No newline at end of file
diff --git a/src/FeatureFlagsDELETE.Tests/FeatureFlagsDELETE.Tests.csproj b/src/FeatureFlagsDELETE.Tests/FeatureFlagsDELETE.Tests.csproj
index 21e05e69..4a73a6ec 100644
--- a/src/FeatureFlagsDELETE.Tests/FeatureFlagsDELETE.Tests.csproj
+++ b/src/FeatureFlagsDELETE.Tests/FeatureFlagsDELETE.Tests.csproj
@@ -4,6 +4,10 @@
+
+
+
+
@@ -12,8 +16,4 @@
-
-
-
-
diff --git a/src/Networking.PeerStreaming.Client/Networking.PeerStreaming.Client.csproj b/src/Networking.PeerStreaming.Client/Networking.PeerStreaming.Client.csproj
index e6584c54..20592b5f 100644
--- a/src/Networking.PeerStreaming.Client/Networking.PeerStreaming.Client.csproj
+++ b/src/Networking.PeerStreaming.Client/Networking.PeerStreaming.Client.csproj
@@ -1,7 +1,6 @@
-
diff --git a/src/Networking.PeerStreaming.Core/Networking.PeerStreaming.Core.csproj b/src/Networking.PeerStreaming.Core/Networking.PeerStreaming.Core.csproj
index d0e60a0d..9d441dfb 100644
--- a/src/Networking.PeerStreaming.Core/Networking.PeerStreaming.Core.csproj
+++ b/src/Networking.PeerStreaming.Core/Networking.PeerStreaming.Core.csproj
@@ -11,8 +11,4 @@
-
-
-
-
diff --git a/src/Networking.PeerStreaming.Grpc/Networking.PeerStreaming.Grpc.csproj b/src/Networking.PeerStreaming.Grpc/Networking.PeerStreaming.Grpc.csproj
index 5253f1c8..92ab3e5b 100644
--- a/src/Networking.PeerStreaming.Grpc/Networking.PeerStreaming.Grpc.csproj
+++ b/src/Networking.PeerStreaming.Grpc/Networking.PeerStreaming.Grpc.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/src/Networking.PeerStreaming.Server/Networking.PeerStreaming.Server.csproj b/src/Networking.PeerStreaming.Server/Networking.PeerStreaming.Server.csproj
index f0d05b8c..76db19ff 100644
--- a/src/Networking.PeerStreaming.Server/Networking.PeerStreaming.Server.csproj
+++ b/src/Networking.PeerStreaming.Server/Networking.PeerStreaming.Server.csproj
@@ -5,11 +5,11 @@
-
+
-
+
diff --git a/src/Networking.PeerStreaming.Tests/Networking.PeerStreaming.Tests.csproj b/src/Networking.PeerStreaming.Tests/Networking.PeerStreaming.Tests.csproj
index c4ba5682..42b50c1e 100644
--- a/src/Networking.PeerStreaming.Tests/Networking.PeerStreaming.Tests.csproj
+++ b/src/Networking.PeerStreaming.Tests/Networking.PeerStreaming.Tests.csproj
@@ -4,6 +4,12 @@
+
+
+
+
+
+
@@ -11,10 +17,4 @@
-
-
-
-
-
-
From f829f1c2838e3dc4aeed6e31ee6c35542dfaf9f2 Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Mon, 29 Dec 2025 23:01:42 +0100
Subject: [PATCH 30/31] f
---
src/Cli/Commands/Scan/ScanCommand.cs | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/Cli/Commands/Scan/ScanCommand.cs b/src/Cli/Commands/Scan/ScanCommand.cs
index b0e2977b..c8b353ca 100644
--- a/src/Cli/Commands/Scan/ScanCommand.cs
+++ b/src/Cli/Commands/Scan/ScanCommand.cs
@@ -115,7 +115,9 @@ public async Task Invoke( ScanParameters parameters, CancellationToken canc
scanRequest.EstimatedDuration(
subnet.Cidr ) + // TODO .Humanize( 2, CultureInfo.InvariantCulture, minUnit: TimeUnit.Second )
")" +
- ( hasAgents ? $" via {sourceList}" : string.Empty ),
+ ( hasAgents
+ ? $" via {sourceList}"
+ : string.Empty ), // TODO print without agentid_ prefix (internal technicality)
ConsoleColor.DarkGray
);
}
From 5294ed8915d8c27f733b4db4efa16280a83dc932 Mon Sep 17 00:00:00 2001
From: hojmark <1203136+hojmark@users.noreply.github.com>
Date: Wed, 18 Feb 2026 13:46:59 +0100
Subject: [PATCH 31/31] bump packages
---
Directory.Packages.props | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 3e868eb6..9228f6e5 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -5,11 +5,11 @@
true
-
+
-
+