mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2025-10-18 12:39:36 +02:00
Compare commits
5 Commits
main
...
9887860f3d
Author | SHA1 | Date | |
---|---|---|---|
9887860f3d
|
|||
f660cf440b
|
|||
42de35dfe6
|
|||
4210619d54
|
|||
089cfb203a
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -192,7 +192,6 @@ ClientBin/
|
|||||||
*.dbmdl
|
*.dbmdl
|
||||||
*.dbproj.schemaview
|
*.dbproj.schemaview
|
||||||
*.jfm
|
*.jfm
|
||||||
*.pfx
|
|
||||||
*.publishsettings
|
*.publishsettings
|
||||||
orleans.codegen.cs
|
orleans.codegen.cs
|
||||||
|
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent1" />
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent1" />
|
||||||
<option name="PASS_PARENT_ENVS" value="1" />
|
<option name="PASS_PARENT_ENVS" value="1" />
|
||||||
<envs>
|
<envs>
|
||||||
<env name="AGENT_KEY" value="JXBQQYG5T267RQS75MXWBTCJZY5CKTCCGQY22MCZPHSQQSJYCHH2NG2TCNXQY6TBSXM9NQDRS2CMX" />
|
<env name="AGENT_KEY" value="K5ZRZYYJ9GWM2FS6XH5N5QQ7WZRPNDGHYMN5QP7RP6PPY27KRPMSYGCN" />
|
||||||
<env name="AGENT_NAME" value="Agent 1" />
|
<env name="AGENT_NAME" value="Agent 1" />
|
||||||
<env name="ALLOWED_RCON_PORTS" value="25575,27000,27001" />
|
<env name="ALLOWED_RCON_PORTS" value="25575,27000,27001" />
|
||||||
<env name="ALLOWED_SERVER_PORTS" value="25565,26000,26001" />
|
<env name="ALLOWED_SERVER_PORTS" value="25565,26000,26001" />
|
||||||
@@ -15,14 +15,19 @@
|
|||||||
<env name="MAX_MEMORY" value="12G" />
|
<env name="MAX_MEMORY" value="12G" />
|
||||||
</envs>
|
</envs>
|
||||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||||
|
<option name="ENV_FILE_PATHS" value="" />
|
||||||
|
<option name="REDIRECT_INPUT_PATH" value="" />
|
||||||
|
<option name="PTY_MODE" value="Auto" />
|
||||||
<option name="USE_MONO" value="0" />
|
<option name="USE_MONO" value="0" />
|
||||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
|
<option name="AUTO_ATTACH_CHILDREN" value="0" />
|
||||||
|
<option name="MIXED_MODE_DEBUG" value="0" />
|
||||||
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Agent/Phantom.Agent/Phantom.Agent.csproj" />
|
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Agent/Phantom.Agent/Phantom.Agent.csproj" />
|
||||||
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
||||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
|
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
|
||||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||||
<option name="PROJECT_TFM" value="net8.0" />
|
<option name="PROJECT_TFM" value="net9.0" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Build" />
|
<option name="Build" />
|
||||||
</method>
|
</method>
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent2" />
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent2" />
|
||||||
<option name="PASS_PARENT_ENVS" value="1" />
|
<option name="PASS_PARENT_ENVS" value="1" />
|
||||||
<envs>
|
<envs>
|
||||||
<env name="AGENT_KEY" value="JXBQQYG5T267RQS75MXWBTCJZY5CKTCCGQY22MCZPHSQQSJYCHH2NG2TCNXQY6TBSXM9NQDRS2CMX" />
|
<env name="AGENT_KEY" value="K5ZRZYYJ9GWM2FS6XH5N5QQ7WZRPNDGHYMN5QP7RP6PPY27KRPMSYGCN" />
|
||||||
<env name="AGENT_NAME" value="Agent 2" />
|
<env name="AGENT_NAME" value="Agent 2" />
|
||||||
<env name="ALLOWED_RCON_PORTS" value="27002-27006" />
|
<env name="ALLOWED_RCON_PORTS" value="27002-27006" />
|
||||||
<env name="ALLOWED_SERVER_PORTS" value="26002-26006" />
|
<env name="ALLOWED_SERVER_PORTS" value="26002-26006" />
|
||||||
@@ -15,14 +15,19 @@
|
|||||||
<env name="MAX_MEMORY" value="10G" />
|
<env name="MAX_MEMORY" value="10G" />
|
||||||
</envs>
|
</envs>
|
||||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||||
|
<option name="ENV_FILE_PATHS" value="" />
|
||||||
|
<option name="REDIRECT_INPUT_PATH" value="" />
|
||||||
|
<option name="PTY_MODE" value="Auto" />
|
||||||
<option name="USE_MONO" value="0" />
|
<option name="USE_MONO" value="0" />
|
||||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
|
<option name="AUTO_ATTACH_CHILDREN" value="0" />
|
||||||
|
<option name="MIXED_MODE_DEBUG" value="0" />
|
||||||
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Agent/Phantom.Agent/Phantom.Agent.csproj" />
|
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Agent/Phantom.Agent/Phantom.Agent.csproj" />
|
||||||
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
||||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
|
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
|
||||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||||
<option name="PROJECT_TFM" value="net8.0" />
|
<option name="PROJECT_TFM" value="net9.0" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Build" />
|
<option name="Build" />
|
||||||
</method>
|
</method>
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent3" />
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent3" />
|
||||||
<option name="PASS_PARENT_ENVS" value="1" />
|
<option name="PASS_PARENT_ENVS" value="1" />
|
||||||
<envs>
|
<envs>
|
||||||
<env name="AGENT_KEY" value="JXBQQYG5T267RQS75MXWBTCJZY5CKTCCGQY22MCZPHSQQSJYCHH2NG2TCNXQY6TBSXM9NQDRS2CMX" />
|
<env name="AGENT_KEY" value="K5ZRZYYJ9GWM2FS6XH5N5QQ7WZRPNDGHYMN5QP7RP6PPY27KRPMSYGCN" />
|
||||||
<env name="AGENT_NAME" value="Agent 3" />
|
<env name="AGENT_NAME" value="Agent 3" />
|
||||||
<env name="ALLOWED_RCON_PORTS" value="27007" />
|
<env name="ALLOWED_RCON_PORTS" value="27007" />
|
||||||
<env name="ALLOWED_SERVER_PORTS" value="26007" />
|
<env name="ALLOWED_SERVER_PORTS" value="26007" />
|
||||||
@@ -15,14 +15,19 @@
|
|||||||
<env name="MAX_MEMORY" value="2560M" />
|
<env name="MAX_MEMORY" value="2560M" />
|
||||||
</envs>
|
</envs>
|
||||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||||
|
<option name="ENV_FILE_PATHS" value="" />
|
||||||
|
<option name="REDIRECT_INPUT_PATH" value="" />
|
||||||
|
<option name="PTY_MODE" value="Auto" />
|
||||||
<option name="USE_MONO" value="0" />
|
<option name="USE_MONO" value="0" />
|
||||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
|
<option name="AUTO_ATTACH_CHILDREN" value="0" />
|
||||||
|
<option name="MIXED_MODE_DEBUG" value="0" />
|
||||||
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Agent/Phantom.Agent/Phantom.Agent.csproj" />
|
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Agent/Phantom.Agent/Phantom.Agent.csproj" />
|
||||||
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
||||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
|
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
|
||||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||||
<option name="PROJECT_TFM" value="net8.0" />
|
<option name="PROJECT_TFM" value="net9.0" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Build" />
|
<option name="Build" />
|
||||||
</method>
|
</method>
|
||||||
|
@@ -14,14 +14,19 @@
|
|||||||
<env name="WEB_RPC_SERVER_HOST" value="localhost" />
|
<env name="WEB_RPC_SERVER_HOST" value="localhost" />
|
||||||
</envs>
|
</envs>
|
||||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||||
|
<option name="ENV_FILE_PATHS" value="" />
|
||||||
|
<option name="REDIRECT_INPUT_PATH" value="" />
|
||||||
|
<option name="PTY_MODE" value="Auto" />
|
||||||
<option name="USE_MONO" value="0" />
|
<option name="USE_MONO" value="0" />
|
||||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
|
<option name="AUTO_ATTACH_CHILDREN" value="0" />
|
||||||
|
<option name="MIXED_MODE_DEBUG" value="0" />
|
||||||
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Controller/Phantom.Controller/Phantom.Controller.csproj" />
|
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Controller/Phantom.Controller/Phantom.Controller.csproj" />
|
||||||
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
||||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
|
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
|
||||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||||
<option name="PROJECT_TFM" value="net8.0" />
|
<option name="PROJECT_TFM" value="net9.0" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Build" />
|
<option name="Build" />
|
||||||
</method>
|
</method>
|
||||||
|
@@ -7,18 +7,23 @@
|
|||||||
<envs>
|
<envs>
|
||||||
<env name="ASPNETCORE_ENVIRONMENT" value="Development" />
|
<env name="ASPNETCORE_ENVIRONMENT" value="Development" />
|
||||||
<env name="CONTROLLER_HOST" value="localhost" />
|
<env name="CONTROLLER_HOST" value="localhost" />
|
||||||
<env name="WEB_KEY" value="BMNHM9RRPMCBBY29D9XHS6KBKZSRY7F5XFN27YZX96XXWJC2NM2D6YRHM9PZN9JGQGCSJ6FMB2GGZ" />
|
<env name="WEB_KEY" value="T5Y722D2GZBXT2H27QS95P2YQRFB2GCTKHSWT5CZFDTFKW52TCM9GDRW" />
|
||||||
<env name="WEB_SERVER_HOST" value="localhost" />
|
<env name="WEB_SERVER_HOST" value="localhost" />
|
||||||
</envs>
|
</envs>
|
||||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||||
|
<option name="ENV_FILE_PATHS" value="" />
|
||||||
|
<option name="REDIRECT_INPUT_PATH" value="" />
|
||||||
|
<option name="PTY_MODE" value="Auto" />
|
||||||
<option name="USE_MONO" value="0" />
|
<option name="USE_MONO" value="0" />
|
||||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
|
<option name="AUTO_ATTACH_CHILDREN" value="0" />
|
||||||
|
<option name="MIXED_MODE_DEBUG" value="0" />
|
||||||
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Web/Phantom.Web/Phantom.Web.csproj" />
|
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Web/Phantom.Web/Phantom.Web.csproj" />
|
||||||
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
||||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
|
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
|
||||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||||
<option name="PROJECT_TFM" value="net8.0" />
|
<option name="PROJECT_TFM" value="net9.0" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Build" />
|
<option name="Build" />
|
||||||
</method>
|
</method>
|
||||||
|
1
.workdir/Controller/secrets/agent.auth
Normal file
1
.workdir/Controller/secrets/agent.auth
Normal file
@@ -0,0 +1 @@
|
|||||||
|
满<EFBFBD>H<EFBFBD>c<EFBFBD>og<EFBFBD>
|
Binary file not shown.
BIN
.workdir/Controller/secrets/agent.pfx
Normal file
BIN
.workdir/Controller/secrets/agent.pfx
Normal file
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
<EFBFBD>Z<EFBFBD>t<>MPI<49>GMZ<4D><5A><EFBFBD><EFBFBD>kN<6B>VF1X<><58>p
|
|
1
.workdir/Controller/secrets/web.auth
Normal file
1
.workdir/Controller/secrets/web.auth
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<07>U<EFBFBD>/<2F><04><><EFBFBD>q
|
@@ -1,2 +0,0 @@
|
|||||||
<EFBFBD><EFBFBD>h?Ο<05>Bx
|
|
||||||
<02>
|
|
BIN
.workdir/Controller/secrets/web.pfx
Normal file
BIN
.workdir/Controller/secrets/web.pfx
Normal file
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
T<EFBFBD>./g<11><>N<EFBFBD><4E>t<EFBFBD>$<24>!<21>(<28><>#<23>~<7E><>}<14><:
|
|
@@ -1,5 +1,7 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using Phantom.Common.Data.Java;
|
using Phantom.Common.Data.Java;
|
||||||
|
using Phantom.Utils.Collections;
|
||||||
using Phantom.Utils.IO;
|
using Phantom.Utils.IO;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
@@ -19,13 +21,14 @@ public sealed class JavaRuntimeDiscovery {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IAsyncEnumerable<JavaRuntimeExecutable> Scan(string folderPath) {
|
public static async Task<JavaRuntimeRepository> Scan(string folderPath, CancellationToken cancellationToken) {
|
||||||
return new JavaRuntimeDiscovery().ScanInternal(folderPath);
|
var runtimes = await new JavaRuntimeDiscovery().ScanInternal(folderPath, cancellationToken).ToImmutableArrayAsync(cancellationToken);
|
||||||
|
return new JavaRuntimeRepository(runtimes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly Dictionary<string, int> duplicateDisplayNames = new ();
|
private readonly Dictionary<string, int> duplicateDisplayNames = new ();
|
||||||
|
|
||||||
private async IAsyncEnumerable<JavaRuntimeExecutable> ScanInternal(string folderPath) {
|
private async IAsyncEnumerable<JavaRuntimeExecutable> ScanInternal(string folderPath, [EnumeratorCancellation] CancellationToken cancellationToken) {
|
||||||
Logger.Information("Starting Java runtime scan in: {FolderPath}", folderPath);
|
Logger.Information("Starting Java runtime scan in: {FolderPath}", folderPath);
|
||||||
|
|
||||||
string javaExecutableName = OperatingSystem.IsWindows() ? "java.exe" : "java";
|
string javaExecutableName = OperatingSystem.IsWindows() ? "java.exe" : "java";
|
||||||
@@ -37,6 +40,8 @@ public sealed class JavaRuntimeDiscovery {
|
|||||||
IgnoreInaccessible = true,
|
IgnoreInaccessible = true,
|
||||||
AttributesToSkip = FileAttributes.Hidden | FileAttributes.ReparsePoint | FileAttributes.System
|
AttributesToSkip = FileAttributes.Hidden | FileAttributes.ReparsePoint | FileAttributes.System
|
||||||
}).Order()) {
|
}).Order()) {
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var javaExecutablePath = Paths.NormalizeSlashes(Path.Combine(binFolderPath, javaExecutableName));
|
var javaExecutablePath = Paths.NormalizeSlashes(Path.Combine(binFolderPath, javaExecutableName));
|
||||||
|
|
||||||
FileAttributes javaExecutableAttributes;
|
FileAttributes javaExecutableAttributes;
|
||||||
@@ -54,7 +59,7 @@ public sealed class JavaRuntimeDiscovery {
|
|||||||
|
|
||||||
JavaRuntime? foundRuntime;
|
JavaRuntime? foundRuntime;
|
||||||
try {
|
try {
|
||||||
foundRuntime = await TryReadJavaRuntimeInformationFromProcess(javaExecutablePath);
|
foundRuntime = await TryReadJavaRuntimeInformationFromProcess(javaExecutablePath, cancellationToken);
|
||||||
} catch (OperationCanceledException) {
|
} catch (OperationCanceledException) {
|
||||||
Logger.Error("Java process did not exit in time.");
|
Logger.Error("Java process did not exit in time.");
|
||||||
continue;
|
continue;
|
||||||
@@ -73,7 +78,7 @@ public sealed class JavaRuntimeDiscovery {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<JavaRuntime?> TryReadJavaRuntimeInformationFromProcess(string javaExecutablePath) {
|
private async Task<JavaRuntime?> TryReadJavaRuntimeInformationFromProcess(string javaExecutablePath, CancellationToken cancellationToken) {
|
||||||
var startInfo = new ProcessStartInfo {
|
var startInfo = new ProcessStartInfo {
|
||||||
FileName = javaExecutablePath,
|
FileName = javaExecutablePath,
|
||||||
WorkingDirectory = Path.GetDirectoryName(javaExecutablePath),
|
WorkingDirectory = Path.GetDirectoryName(javaExecutablePath),
|
||||||
@@ -81,32 +86,29 @@ public sealed class JavaRuntimeDiscovery {
|
|||||||
RedirectStandardInput = false,
|
RedirectStandardInput = false,
|
||||||
RedirectStandardOutput = false,
|
RedirectStandardOutput = false,
|
||||||
RedirectStandardError = true,
|
RedirectStandardError = true,
|
||||||
UseShellExecute = false
|
UseShellExecute = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
var process = new Process { StartInfo = startInfo };
|
using var timeoutCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||||
var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
using var combinedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken);
|
||||||
|
|
||||||
try {
|
using var process = new Process();
|
||||||
process.Start();
|
process.StartInfo = startInfo;
|
||||||
|
process.Start();
|
||||||
|
|
||||||
|
JavaRuntimeBuilder runtimeBuilder = new ();
|
||||||
|
|
||||||
|
while (await process.StandardError.ReadLineAsync(combinedCancellationTokenSource.Token) is {} line) {
|
||||||
|
ExtractJavaVersionPropertiesFromLine(line, runtimeBuilder);
|
||||||
|
|
||||||
JavaRuntimeBuilder runtimeBuilder = new ();
|
JavaRuntime? runtime = runtimeBuilder.TryBuild(duplicateDisplayNames);
|
||||||
|
if (runtime != null) {
|
||||||
while (await process.StandardError.ReadLineAsync(cancellationTokenSource.Token) is {} line) {
|
return runtime;
|
||||||
ExtractJavaVersionPropertiesFromLine(line, runtimeBuilder);
|
|
||||||
|
|
||||||
JavaRuntime? runtime = runtimeBuilder.TryBuild(duplicateDisplayNames);
|
|
||||||
if (runtime != null) {
|
|
||||||
return runtime;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await process.WaitForExitAsync(cancellationTokenSource.Token);
|
|
||||||
return null;
|
|
||||||
} finally {
|
|
||||||
process.Dispose();
|
|
||||||
cancellationTokenSource.Dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await process.WaitForExitAsync(combinedCancellationTokenSource.Token);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ExtractJavaVersionPropertiesFromLine(ReadOnlySpan<char> line, JavaRuntimeBuilder runtimeBuilder) {
|
private static void ExtractJavaVersionPropertiesFromLine(ReadOnlySpan<char> line, JavaRuntimeBuilder runtimeBuilder) {
|
||||||
|
@@ -6,41 +6,28 @@ using Phantom.Utils.Cryptography;
|
|||||||
namespace Phantom.Agent.Minecraft.Java;
|
namespace Phantom.Agent.Minecraft.Java;
|
||||||
|
|
||||||
public sealed class JavaRuntimeRepository {
|
public sealed class JavaRuntimeRepository {
|
||||||
private readonly Dictionary<string, Guid> guidsByPath = new ();
|
private readonly ImmutableDictionary<Guid, JavaRuntimeExecutable> runtimesByGuid;
|
||||||
private readonly Dictionary<Guid, JavaRuntimeExecutable> runtimesByGuid = new ();
|
|
||||||
private readonly ReaderWriterLockSlim rwLock = new (LockRecursionPolicy.NoRecursion);
|
internal JavaRuntimeRepository(ImmutableArray<JavaRuntimeExecutable> runtimes) {
|
||||||
|
var runtimesByGuidBuilder = ImmutableDictionary.CreateBuilder<Guid, JavaRuntimeExecutable>();
|
||||||
|
|
||||||
|
foreach (JavaRuntimeExecutable runtime in runtimes) {
|
||||||
|
runtimesByGuidBuilder.Add(GenerateStableGuid(runtime.ExecutablePath), runtime);
|
||||||
|
}
|
||||||
|
|
||||||
|
runtimesByGuid = runtimesByGuidBuilder.ToImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
public ImmutableArray<TaggedJavaRuntime> All {
|
public ImmutableArray<TaggedJavaRuntime> All {
|
||||||
get {
|
get {
|
||||||
rwLock.EnterReadLock();
|
return runtimesByGuid.Select(static kvp => new TaggedJavaRuntime(kvp.Key, kvp.Value.Runtime))
|
||||||
try {
|
.OrderBy(static taggedRuntime => taggedRuntime.Runtime)
|
||||||
return runtimesByGuid.Select(static kvp => new TaggedJavaRuntime(kvp.Key, kvp.Value.Runtime)).OrderBy(static taggedRuntime => taggedRuntime.Runtime).ToImmutableArray();
|
.ToImmutableArray();
|
||||||
} finally {
|
|
||||||
rwLock.ExitReadLock();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Include(JavaRuntimeExecutable runtime) {
|
internal bool TryGetByGuid(Guid guid, [MaybeNullWhen(false)] out JavaRuntimeExecutable runtime) {
|
||||||
rwLock.EnterWriteLock();
|
return runtimesByGuid.TryGetValue(guid, out runtime);
|
||||||
try {
|
|
||||||
if (!guidsByPath.TryGetValue(runtime.ExecutablePath, out var guid)) {
|
|
||||||
guidsByPath[runtime.ExecutablePath] = guid = GenerateStableGuid(runtime.ExecutablePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
runtimesByGuid[guid] = runtime;
|
|
||||||
} finally {
|
|
||||||
rwLock.ExitWriteLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryGetByGuid(Guid guid, [MaybeNullWhen(false)] out JavaRuntimeExecutable runtime) {
|
|
||||||
rwLock.EnterReadLock();
|
|
||||||
try {
|
|
||||||
return runtimesByGuid.TryGetValue(guid, out runtime);
|
|
||||||
} finally {
|
|
||||||
rwLock.ExitReadLock();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Guid GenerateStableGuid(string executablePath) {
|
private static Guid GenerateStableGuid(string executablePath) {
|
||||||
|
@@ -1,21 +0,0 @@
|
|||||||
using Phantom.Common.Messages.Agent;
|
|
||||||
using Phantom.Utils.Logging;
|
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
namespace Phantom.Agent.Rpc;
|
|
||||||
|
|
||||||
public sealed class ControllerConnection {
|
|
||||||
private static readonly ILogger Logger = PhantomLogger.Create(nameof(ControllerConnection));
|
|
||||||
|
|
||||||
private readonly RpcConnectionToServer<IMessageToController> connection;
|
|
||||||
|
|
||||||
public ControllerConnection(RpcConnectionToServer<IMessageToController> connection) {
|
|
||||||
this.connection = connection;
|
|
||||||
Logger.Information("Connection ready.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Send<TMessage>(TMessage message) where TMessage : IMessageToController {
|
|
||||||
return connection.Send(message);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,44 +0,0 @@
|
|||||||
using Phantom.Common.Messages.Agent;
|
|
||||||
using Phantom.Common.Messages.Agent.ToController;
|
|
||||||
using Phantom.Utils.Logging;
|
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
namespace Phantom.Agent.Rpc;
|
|
||||||
|
|
||||||
sealed class KeepAliveLoop {
|
|
||||||
private static readonly ILogger Logger = PhantomLogger.Create<KeepAliveLoop>();
|
|
||||||
|
|
||||||
private static readonly TimeSpan KeepAliveInterval = TimeSpan.FromSeconds(10);
|
|
||||||
|
|
||||||
private readonly RpcConnectionToServer<IMessageToController> connection;
|
|
||||||
private readonly CancellationTokenSource cancellationTokenSource = new ();
|
|
||||||
|
|
||||||
public KeepAliveLoop(RpcConnectionToServer<IMessageToController> connection) {
|
|
||||||
this.connection = connection;
|
|
||||||
Task.Run(Run);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task Run() {
|
|
||||||
var cancellationToken = cancellationTokenSource.Token;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await connection.IsReady.WaitAsync(cancellationToken);
|
|
||||||
Logger.Information("Started keep-alive loop.");
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
await Task.Delay(KeepAliveInterval, cancellationToken);
|
|
||||||
await connection.Send(new AgentIsAliveMessage()).WaitAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
} catch (OperationCanceledException) {
|
|
||||||
// Ignore.
|
|
||||||
} finally {
|
|
||||||
cancellationTokenSource.Dispose();
|
|
||||||
Logger.Information("Stopped keep-alive loop.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Cancel() {
|
|
||||||
cancellationTokenSource.Cancel();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,12 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\Common\Phantom.Common.Messages.Agent\Phantom.Common.Messages.Agent.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@@ -1,37 +0,0 @@
|
|||||||
using NetMQ;
|
|
||||||
using NetMQ.Sockets;
|
|
||||||
using Phantom.Common.Messages.Agent;
|
|
||||||
using Phantom.Common.Messages.Agent.BiDirectional;
|
|
||||||
using Phantom.Common.Messages.Agent.ToController;
|
|
||||||
using Phantom.Utils.Actor;
|
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
|
||||||
using Phantom.Utils.Rpc.Sockets;
|
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
namespace Phantom.Agent.Rpc;
|
|
||||||
|
|
||||||
public sealed class RpcClientRuntime : RpcClientRuntime<IMessageToAgent, IMessageToController, ReplyMessage> {
|
|
||||||
public static Task Launch(RpcClientSocket<IMessageToAgent, IMessageToController, ReplyMessage> socket, ActorRef<IMessageToAgent> handlerActorRef, SemaphoreSlim disconnectSemaphore, CancellationToken receiveCancellationToken) {
|
|
||||||
return new RpcClientRuntime(socket, handlerActorRef, disconnectSemaphore, receiveCancellationToken).Launch();
|
|
||||||
}
|
|
||||||
|
|
||||||
private RpcClientRuntime(RpcClientSocket<IMessageToAgent, IMessageToController, ReplyMessage> socket, ActorRef<IMessageToAgent> handlerActor, SemaphoreSlim disconnectSemaphore, CancellationToken receiveCancellationToken) : base(socket, handlerActor, disconnectSemaphore, receiveCancellationToken) {}
|
|
||||||
|
|
||||||
protected override async Task RunWithConnection(ClientSocket socket, RpcConnectionToServer<IMessageToController> connection) {
|
|
||||||
var keepAliveLoop = new KeepAliveLoop(connection);
|
|
||||||
try {
|
|
||||||
await base.RunWithConnection(socket, connection);
|
|
||||||
} finally {
|
|
||||||
keepAliveLoop.Cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task SendDisconnectMessage(ClientSocket socket, ILogger logger) {
|
|
||||||
var unregisterMessageBytes = AgentMessageRegistries.ToController.Write(new UnregisterAgentMessage()).ToArray();
|
|
||||||
try {
|
|
||||||
await socket.SendAsync(unregisterMessageBytes).AsTask().WaitAsync(TimeSpan.FromSeconds(5), CancellationToken.None);
|
|
||||||
} catch (TimeoutException) {
|
|
||||||
logger.Error("Timed out communicating agent shutdown with the controller.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,9 +1,12 @@
|
|||||||
using Akka.Actor;
|
using System.Collections.Immutable;
|
||||||
|
using Akka.Actor;
|
||||||
using Phantom.Agent.Minecraft.Java;
|
using Phantom.Agent.Minecraft.Java;
|
||||||
using Phantom.Agent.Rpc;
|
|
||||||
using Phantom.Agent.Services.Backups;
|
using Phantom.Agent.Services.Backups;
|
||||||
using Phantom.Agent.Services.Instances;
|
using Phantom.Agent.Services.Instances;
|
||||||
|
using Phantom.Agent.Services.Rpc;
|
||||||
using Phantom.Common.Data.Agent;
|
using Phantom.Common.Data.Agent;
|
||||||
|
using Phantom.Common.Data.Replies;
|
||||||
|
using Phantom.Common.Messages.Agent.ToAgent;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
@@ -15,7 +18,6 @@ public sealed class AgentServices {
|
|||||||
|
|
||||||
public ActorSystem ActorSystem { get; }
|
public ActorSystem ActorSystem { get; }
|
||||||
|
|
||||||
private AgentFolders AgentFolders { get; }
|
|
||||||
private AgentState AgentState { get; }
|
private AgentState AgentState { get; }
|
||||||
private BackupManager BackupManager { get; }
|
private BackupManager BackupManager { get; }
|
||||||
|
|
||||||
@@ -23,30 +25,51 @@ public sealed class AgentServices {
|
|||||||
internal InstanceTicketManager InstanceTicketManager { get; }
|
internal InstanceTicketManager InstanceTicketManager { get; }
|
||||||
internal ActorRef<InstanceManagerActor.ICommand> InstanceManager { get; }
|
internal ActorRef<InstanceManagerActor.ICommand> InstanceManager { get; }
|
||||||
|
|
||||||
public AgentServices(AgentInfo agentInfo, AgentFolders agentFolders, AgentServiceConfiguration serviceConfiguration, ControllerConnection controllerConnection) {
|
public AgentServices(AgentInfo agentInfo, AgentFolders agentFolders, AgentServiceConfiguration serviceConfiguration, ControllerConnection controllerConnection, JavaRuntimeRepository javaRuntimeRepository) {
|
||||||
this.ActorSystem = ActorSystemFactory.Create("Agent");
|
this.ActorSystem = ActorSystemFactory.Create("Agent");
|
||||||
|
|
||||||
this.AgentFolders = agentFolders;
|
|
||||||
this.AgentState = new AgentState();
|
this.AgentState = new AgentState();
|
||||||
this.BackupManager = new BackupManager(agentFolders, serviceConfiguration.MaxConcurrentCompressionTasks);
|
this.BackupManager = new BackupManager(agentFolders, serviceConfiguration.MaxConcurrentCompressionTasks);
|
||||||
|
|
||||||
this.JavaRuntimeRepository = new JavaRuntimeRepository();
|
this.JavaRuntimeRepository = javaRuntimeRepository;
|
||||||
this.InstanceTicketManager = new InstanceTicketManager(agentInfo, controllerConnection);
|
this.InstanceTicketManager = new InstanceTicketManager(agentInfo, controllerConnection);
|
||||||
|
|
||||||
var instanceManagerInit = new InstanceManagerActor.Init(controllerConnection, agentFolders, AgentState, JavaRuntimeRepository, InstanceTicketManager, BackupManager);
|
var instanceManagerInit = new InstanceManagerActor.Init(controllerConnection, agentFolders, AgentState, JavaRuntimeRepository, InstanceTicketManager, BackupManager);
|
||||||
this.InstanceManager = ActorSystem.ActorOf(InstanceManagerActor.Factory(instanceManagerInit), "InstanceManager");
|
this.InstanceManager = ActorSystem.ActorOf(InstanceManagerActor.Factory(instanceManagerInit), "InstanceManager");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Initialize() {
|
public async Task<bool> Register(ControllerHandshake handshake, CancellationToken cancellationToken) {
|
||||||
await foreach (var runtime in JavaRuntimeDiscovery.Scan(AgentFolders.JavaSearchFolderPath)) {
|
ImmutableArray<ConfigureInstanceMessage> configureInstanceMessages = handshake.Response;
|
||||||
JavaRuntimeRepository.Include(runtime);
|
if (configureInstanceMessages.IsDefault) {
|
||||||
|
Logger.Fatal("Handshake failed.");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var configureInstanceMessage in configureInstanceMessages) {
|
||||||
|
var configureInstanceCommand = new InstanceManagerActor.ConfigureInstanceCommand(
|
||||||
|
configureInstanceMessage.InstanceGuid,
|
||||||
|
configureInstanceMessage.Configuration,
|
||||||
|
configureInstanceMessage.LaunchProperties,
|
||||||
|
configureInstanceMessage.LaunchNow,
|
||||||
|
AlwaysReportStatus: true
|
||||||
|
);
|
||||||
|
|
||||||
|
var configureInstanceResult = await InstanceManager.Request(configureInstanceCommand, cancellationToken);
|
||||||
|
if (!configureInstanceResult.Is(ConfigureInstanceResult.Success)) {
|
||||||
|
Logger.Fatal("Unable to configure instance \"{Name}\" (GUID {Guid}), shutting down.", configureInstanceMessage.Configuration.InstanceName, configureInstanceMessage.InstanceGuid);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InstanceTicketManager.RefreshAgentStatus();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Shutdown() {
|
public async Task Shutdown() {
|
||||||
Logger.Information("Stopping services...");
|
Logger.Information("Stopping services...");
|
||||||
|
|
||||||
await InstanceManager.Stop(new InstanceManagerActor.ShutdownCommand());
|
await InstanceManager.Stop(new InstanceManagerActor.ShutdownCommand());
|
||||||
|
await InstanceTicketManager.Shutdown();
|
||||||
|
|
||||||
BackupManager.Dispose();
|
BackupManager.Dispose();
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
using Phantom.Agent.Minecraft.Launcher;
|
using Phantom.Agent.Minecraft.Launcher;
|
||||||
using Phantom.Agent.Services.Backups;
|
using Phantom.Agent.Services.Backups;
|
||||||
using Phantom.Agent.Services.Instances.State;
|
using Phantom.Agent.Services.Instances.State;
|
||||||
|
using Phantom.Agent.Services.Rpc;
|
||||||
using Phantom.Common.Data.Backups;
|
using Phantom.Common.Data.Backups;
|
||||||
using Phantom.Common.Data.Instance;
|
using Phantom.Common.Data.Instance;
|
||||||
using Phantom.Common.Data.Minecraft;
|
using Phantom.Common.Data.Minecraft;
|
||||||
@@ -23,24 +24,30 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
|
|||||||
private readonly CancellationToken shutdownCancellationToken;
|
private readonly CancellationToken shutdownCancellationToken;
|
||||||
|
|
||||||
private readonly Guid instanceGuid;
|
private readonly Guid instanceGuid;
|
||||||
private readonly InstanceServices instanceServices;
|
|
||||||
private readonly InstanceTicketManager instanceTicketManager;
|
private readonly InstanceTicketManager instanceTicketManager;
|
||||||
private readonly InstanceContext context;
|
private readonly InstanceContext context;
|
||||||
|
|
||||||
|
private readonly ControllerSendQueue<ReportInstanceStatusMessage> reportStatusQueue;
|
||||||
|
private readonly ControllerSendQueue<ReportInstanceEventMessage> reportEventsQueue;
|
||||||
|
|
||||||
private readonly CancellationTokenSource actorCancellationTokenSource = new ();
|
private readonly CancellationTokenSource actorCancellationTokenSource = new ();
|
||||||
|
|
||||||
private IInstanceStatus currentStatus = InstanceStatus.NotRunning;
|
private IInstanceStatus currentStatus = InstanceStatus.NotRunning;
|
||||||
private InstanceRunningState? runningState = null;
|
private InstanceRunningState? runningState = null;
|
||||||
|
|
||||||
private InstanceActor(Init init) {
|
private InstanceActor(Init init) {
|
||||||
|
InstanceServices services = init.InstanceServices;
|
||||||
|
|
||||||
this.agentState = init.AgentState;
|
this.agentState = init.AgentState;
|
||||||
this.instanceGuid = init.InstanceGuid;
|
this.instanceGuid = init.InstanceGuid;
|
||||||
this.instanceServices = init.InstanceServices;
|
|
||||||
this.instanceTicketManager = init.InstanceTicketManager;
|
this.instanceTicketManager = init.InstanceTicketManager;
|
||||||
this.shutdownCancellationToken = init.ShutdownCancellationToken;
|
this.shutdownCancellationToken = init.ShutdownCancellationToken;
|
||||||
|
|
||||||
|
this.reportStatusQueue = new ControllerSendQueue<ReportInstanceStatusMessage>(services.ControllerConnection, init.ShortName + "-Status", capacity: 1, singleWriter: true);
|
||||||
|
this.reportEventsQueue = new ControllerSendQueue<ReportInstanceEventMessage>(services.ControllerConnection, init.ShortName + "-Events", capacity: 1000, singleWriter: true);
|
||||||
|
|
||||||
var logger = PhantomLogger.Create<InstanceActor>(init.ShortName);
|
var logger = PhantomLogger.Create<InstanceActor>(init.ShortName);
|
||||||
this.context = new InstanceContext(instanceGuid, init.ShortName, logger, instanceServices, SelfTyped, actorCancellationTokenSource.Token);
|
this.context = new InstanceContext(instanceGuid, init.ShortName, logger, services, reportEventsQueue, SelfTyped, actorCancellationTokenSource.Token);
|
||||||
|
|
||||||
Receive<ReportInstanceStatusCommand>(ReportInstanceStatus);
|
Receive<ReportInstanceStatusCommand>(ReportInstanceStatus);
|
||||||
ReceiveAsync<LaunchInstanceCommand>(LaunchInstance);
|
ReceiveAsync<LaunchInstanceCommand>(LaunchInstance);
|
||||||
@@ -58,7 +65,7 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
|
|||||||
|
|
||||||
private void ReportCurrentStatus() {
|
private void ReportCurrentStatus() {
|
||||||
agentState.UpdateInstance(new Instance(instanceGuid, currentStatus));
|
agentState.UpdateInstance(new Instance(instanceGuid, currentStatus));
|
||||||
instanceServices.ControllerConnection.Send(new ReportInstanceStatusMessage(instanceGuid, currentStatus));
|
reportStatusQueue.Enqueue(new ReportInstanceStatusMessage(instanceGuid, currentStatus));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TransitionState(InstanceRunningState? newState) {
|
private void TransitionState(InstanceRunningState? newState) {
|
||||||
@@ -71,7 +78,7 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
|
|||||||
runningState?.Initialize();
|
runningState?.Initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface ICommand {}
|
public interface ICommand;
|
||||||
|
|
||||||
public sealed record ReportInstanceStatusCommand : ICommand;
|
public sealed record ReportInstanceStatusCommand : ICommand;
|
||||||
|
|
||||||
@@ -156,6 +163,12 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
|
|||||||
private async Task Shutdown(ShutdownCommand command) {
|
private async Task Shutdown(ShutdownCommand command) {
|
||||||
await StopInstance(new StopInstanceCommand(MinecraftStopStrategy.Instant));
|
await StopInstance(new StopInstanceCommand(MinecraftStopStrategy.Instant));
|
||||||
await actorCancellationTokenSource.CancelAsync();
|
await actorCancellationTokenSource.CancelAsync();
|
||||||
|
|
||||||
|
await Task.WhenAll(
|
||||||
|
reportStatusQueue.Shutdown(TimeSpan.FromSeconds(5)),
|
||||||
|
reportEventsQueue.Shutdown(TimeSpan.FromSeconds(5))
|
||||||
|
);
|
||||||
|
|
||||||
Context.Stop(Self);
|
Context.Stop(Self);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,21 @@
|
|||||||
using Phantom.Common.Data.Instance;
|
using Phantom.Agent.Services.Rpc;
|
||||||
|
using Phantom.Common.Data.Instance;
|
||||||
using Phantom.Common.Messages.Agent.ToController;
|
using Phantom.Common.Messages.Agent.ToController;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
namespace Phantom.Agent.Services.Instances;
|
namespace Phantom.Agent.Services.Instances;
|
||||||
|
|
||||||
sealed record InstanceContext(Guid InstanceGuid, string ShortName, ILogger Logger, InstanceServices Services, ActorRef<InstanceActor.ICommand> Actor, CancellationToken ActorCancellationToken) {
|
sealed record InstanceContext(
|
||||||
|
Guid InstanceGuid,
|
||||||
|
string ShortName,
|
||||||
|
ILogger Logger,
|
||||||
|
InstanceServices Services,
|
||||||
|
ControllerSendQueue<ReportInstanceEventMessage> ReportEventQueue,
|
||||||
|
ActorRef<InstanceActor.ICommand> Actor,
|
||||||
|
CancellationToken ActorCancellationToken
|
||||||
|
) {
|
||||||
public void ReportEvent(IInstanceEvent instanceEvent) {
|
public void ReportEvent(IInstanceEvent instanceEvent) {
|
||||||
Services.ControllerConnection.Send(new ReportInstanceEventMessage(Guid.NewGuid(), DateTime.UtcNow, InstanceGuid, instanceEvent));
|
ReportEventQueue.Enqueue(new ReportInstanceEventMessage(Guid.NewGuid(), DateTime.UtcNow, InstanceGuid, instanceEvent));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,8 +4,8 @@ using Phantom.Agent.Minecraft.Launcher;
|
|||||||
using Phantom.Agent.Minecraft.Launcher.Types;
|
using Phantom.Agent.Minecraft.Launcher.Types;
|
||||||
using Phantom.Agent.Minecraft.Properties;
|
using Phantom.Agent.Minecraft.Properties;
|
||||||
using Phantom.Agent.Minecraft.Server;
|
using Phantom.Agent.Minecraft.Server;
|
||||||
using Phantom.Agent.Rpc;
|
|
||||||
using Phantom.Agent.Services.Backups;
|
using Phantom.Agent.Services.Backups;
|
||||||
|
using Phantom.Agent.Services.Rpc;
|
||||||
using Phantom.Common.Data;
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Instance;
|
using Phantom.Common.Data.Instance;
|
||||||
using Phantom.Common.Data.Minecraft;
|
using Phantom.Common.Data.Minecraft;
|
||||||
@@ -56,11 +56,6 @@ sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand>
|
|||||||
ReceiveAsync<ShutdownCommand>(Shutdown);
|
ReceiveAsync<ShutdownCommand>(Shutdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetInstanceLoggerName(Guid guid) {
|
|
||||||
var prefix = guid.ToString();
|
|
||||||
return prefix[..prefix.IndexOf('-')] + "/" + Interlocked.Increment(ref instanceLoggerSequenceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed record InstanceInfo(ActorRef<InstanceActor.ICommand> Actor, InstanceConfiguration Configuration, IServerLauncher Launcher);
|
private sealed record InstanceInfo(ActorRef<InstanceActor.ICommand> Actor, InstanceConfiguration Configuration, IServerLauncher Launcher);
|
||||||
|
|
||||||
public interface ICommand {}
|
public interface ICommand {}
|
||||||
@@ -118,7 +113,8 @@ sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var instanceInit = new InstanceActor.Init(agentState, instanceGuid, GetInstanceLoggerName(instanceGuid), instanceServices, instanceTicketManager, shutdownCancellationToken);
|
var instanceLoggerName = PhantomLogger.ShortenGuid(instanceGuid) + "/" + Interlocked.Increment(ref instanceLoggerSequenceId);
|
||||||
|
var instanceInit = new InstanceActor.Init(agentState, instanceGuid, instanceLoggerName, instanceServices, instanceTicketManager, shutdownCancellationToken);
|
||||||
instances[instanceGuid] = instance = new InstanceInfo(Context.ActorOf(InstanceActor.Factory(instanceInit), "Instance-" + instanceGuid), configuration, launcher);
|
instances[instanceGuid] = instance = new InstanceInfo(Context.ActorOf(InstanceActor.Factory(instanceInit), "Instance-" + instanceGuid), configuration, launcher);
|
||||||
|
|
||||||
Logger.Information("Created instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, instanceGuid);
|
Logger.Information("Created instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, instanceGuid);
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
using Phantom.Agent.Minecraft.Launcher;
|
using Phantom.Agent.Minecraft.Launcher;
|
||||||
using Phantom.Agent.Rpc;
|
|
||||||
using Phantom.Agent.Services.Backups;
|
using Phantom.Agent.Services.Backups;
|
||||||
|
using Phantom.Agent.Services.Rpc;
|
||||||
|
|
||||||
namespace Phantom.Agent.Services.Instances;
|
namespace Phantom.Agent.Services.Instances;
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
using Phantom.Agent.Rpc;
|
using Phantom.Agent.Services.Rpc;
|
||||||
using Phantom.Common.Data;
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Agent;
|
using Phantom.Common.Data.Agent;
|
||||||
using Phantom.Common.Data.Instance;
|
using Phantom.Common.Data.Instance;
|
||||||
@@ -9,21 +9,15 @@ using Serilog;
|
|||||||
|
|
||||||
namespace Phantom.Agent.Services.Instances;
|
namespace Phantom.Agent.Services.Instances;
|
||||||
|
|
||||||
sealed class InstanceTicketManager {
|
sealed class InstanceTicketManager(AgentInfo agentInfo, ControllerConnection controllerConnection) {
|
||||||
private static readonly ILogger Logger = PhantomLogger.Create<InstanceTicketManager>();
|
private static readonly ILogger Logger = PhantomLogger.Create<InstanceTicketManager>();
|
||||||
|
|
||||||
private readonly AgentInfo agentInfo;
|
private readonly ControllerSendQueue<ReportAgentStatusMessage> reportStatusQueue = new (controllerConnection, nameof(InstanceTicketManager), capacity: 1, singleWriter: true);
|
||||||
private readonly ControllerConnection controllerConnection;
|
|
||||||
|
|
||||||
private readonly HashSet<Guid> activeTicketGuids = new ();
|
private readonly HashSet<Guid> activeTicketGuids = [];
|
||||||
private readonly HashSet<ushort> usedPorts = new ();
|
private readonly HashSet<ushort> usedPorts = [];
|
||||||
private RamAllocationUnits usedMemory = new ();
|
private RamAllocationUnits usedMemory = new ();
|
||||||
|
|
||||||
public InstanceTicketManager(AgentInfo agentInfo, ControllerConnection controllerConnection) {
|
|
||||||
this.agentInfo = agentInfo;
|
|
||||||
this.controllerConnection = controllerConnection;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Result<Ticket, LaunchInstanceResult> Reserve(InstanceConfiguration configuration) {
|
public Result<Ticket, LaunchInstanceResult> Reserve(InstanceConfiguration configuration) {
|
||||||
var memoryAllocation = configuration.MemoryAllocation;
|
var memoryAllocation = configuration.MemoryAllocation;
|
||||||
var serverPort = configuration.ServerPort;
|
var serverPort = configuration.ServerPort;
|
||||||
@@ -91,9 +85,13 @@ sealed class InstanceTicketManager {
|
|||||||
|
|
||||||
public void RefreshAgentStatus() {
|
public void RefreshAgentStatus() {
|
||||||
lock (this) {
|
lock (this) {
|
||||||
controllerConnection.Send(new ReportAgentStatusMessage(activeTicketGuids.Count, usedMemory));
|
reportStatusQueue.Enqueue(new ReportAgentStatusMessage(activeTicketGuids.Count, usedMemory));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task Shutdown() {
|
||||||
|
await reportStatusQueue.Shutdown(TimeSpan.FromSeconds(5));
|
||||||
|
}
|
||||||
|
|
||||||
public sealed record Ticket(Guid TicketGuid, RamAllocationUnits MemoryAllocation, ushort ServerPort, ushort RconPort);
|
public sealed record Ticket(Guid TicketGuid, RamAllocationUnits MemoryAllocation, ushort ServerPort, ushort RconPort);
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
using Phantom.Agent.Rpc;
|
using Phantom.Agent.Services.Rpc;
|
||||||
using Phantom.Common.Messages.Agent.ToController;
|
using Phantom.Common.Messages.Agent.ToController;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
using Phantom.Utils.Tasks;
|
using Phantom.Utils.Tasks;
|
||||||
@@ -8,10 +9,10 @@ using Phantom.Utils.Tasks;
|
|||||||
namespace Phantom.Agent.Services.Instances.State;
|
namespace Phantom.Agent.Services.Instances.State;
|
||||||
|
|
||||||
sealed class InstanceLogSender : CancellableBackgroundTask {
|
sealed class InstanceLogSender : CancellableBackgroundTask {
|
||||||
private static readonly BoundedChannelOptions BufferOptions = new (capacity: 100) {
|
private static readonly BoundedChannelOptions BufferOptions = new (capacity: 200) {
|
||||||
SingleReader = true,
|
SingleReader = true,
|
||||||
SingleWriter = true,
|
SingleWriter = true,
|
||||||
FullMode = BoundedChannelFullMode.DropNewest
|
FullMode = BoundedChannelFullMode.DropNewest,
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly TimeSpan SendDelay = TimeSpan.FromMilliseconds(200);
|
private static readonly TimeSpan SendDelay = TimeSpan.FromMilliseconds(200);
|
||||||
@@ -33,17 +34,29 @@ sealed class InstanceLogSender : CancellableBackgroundTask {
|
|||||||
var lineReader = outputChannel.Reader;
|
var lineReader = outputChannel.Reader;
|
||||||
var lineBuilder = ImmutableArray.CreateBuilder<string>();
|
var lineBuilder = ImmutableArray.CreateBuilder<string>();
|
||||||
|
|
||||||
|
using var sendOutputCancellationTokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
await using var sendOutputCancellationRegistration = CancellationToken.Register([SuppressMessage("ReSharper", "AccessToDisposedClosure")]() => {
|
||||||
|
sendOutputCancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(10));
|
||||||
|
});
|
||||||
|
|
||||||
|
var sendOutputCancellationToken = sendOutputCancellationTokenSource.Token;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (await lineReader.WaitToReadAsync(CancellationToken)) {
|
while (await lineReader.WaitToReadAsync(CancellationToken)) {
|
||||||
await Task.Delay(SendDelay, CancellationToken);
|
await Task.Delay(SendDelay, CancellationToken);
|
||||||
SendOutputToServer(ReadLinesFromChannel(lineReader, lineBuilder));
|
await SendOutputToServer(ReadLinesFromChannel(lineReader, lineBuilder), sendOutputCancellationToken);
|
||||||
}
|
}
|
||||||
} catch (OperationCanceledException) {
|
} catch (OperationCanceledException) {
|
||||||
// Ignore.
|
// Ignore.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush remaining lines.
|
// Flush remaining lines.
|
||||||
SendOutputToServer(ReadLinesFromChannel(lineReader, lineBuilder));
|
try {
|
||||||
|
await SendOutputToServer(ReadLinesFromChannel(lineReader, lineBuilder), sendOutputCancellationToken);
|
||||||
|
} catch (OperationCanceledException) {
|
||||||
|
// Ignore.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ImmutableArray<string> ReadLinesFromChannel(ChannelReader<string> reader, ImmutableArray<string>.Builder builder) {
|
private ImmutableArray<string> ReadLinesFromChannel(ChannelReader<string> reader, ImmutableArray<string>.Builder builder) {
|
||||||
@@ -53,7 +66,7 @@ sealed class InstanceLogSender : CancellableBackgroundTask {
|
|||||||
builder.Add(line);
|
builder.Add(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
int droppedLines = Interlocked.Exchange(ref droppedLinesSinceLastSend, 0);
|
int droppedLines = Interlocked.Exchange(ref droppedLinesSinceLastSend, value: 0);
|
||||||
if (droppedLines > 0) {
|
if (droppedLines > 0) {
|
||||||
builder.Add($"Dropped {droppedLines} {(droppedLines == 1 ? "line" : "lines")} due to buffer overflow.");
|
builder.Add($"Dropped {droppedLines} {(droppedLines == 1 ? "line" : "lines")} due to buffer overflow.");
|
||||||
}
|
}
|
||||||
@@ -61,9 +74,12 @@ sealed class InstanceLogSender : CancellableBackgroundTask {
|
|||||||
return builder.ToImmutable();
|
return builder.ToImmutable();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SendOutputToServer(ImmutableArray<string> lines) {
|
private ValueTask SendOutputToServer(ImmutableArray<string> lines, CancellationToken cancellationToken) {
|
||||||
if (!lines.IsEmpty) {
|
if (lines.IsEmpty) {
|
||||||
controllerConnection.Send(new InstanceOutputMessage(instanceGuid, lines));
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return controllerConnection.Send(new InstanceOutputMessage(instanceGuid, lines), cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
using Phantom.Agent.Minecraft.Instance;
|
using Phantom.Agent.Minecraft.Instance;
|
||||||
using Phantom.Agent.Minecraft.Server;
|
using Phantom.Agent.Minecraft.Server;
|
||||||
using Phantom.Agent.Rpc;
|
using Phantom.Agent.Services.Rpc;
|
||||||
using Phantom.Common.Data.Instance;
|
using Phantom.Common.Data.Instance;
|
||||||
using Phantom.Common.Messages.Agent.ToController;
|
using Phantom.Common.Messages.Agent.ToController;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
@@ -19,29 +19,6 @@ sealed class InstancePlayerCountTracker : CancellableBackgroundTask {
|
|||||||
private readonly ManualResetEventSlim serverOutputEvent = new ();
|
private readonly ManualResetEventSlim serverOutputEvent = new ();
|
||||||
|
|
||||||
private InstancePlayerCounts? playerCounts;
|
private InstancePlayerCounts? playerCounts;
|
||||||
|
|
||||||
public InstancePlayerCounts? PlayerCounts {
|
|
||||||
get {
|
|
||||||
lock (this) {
|
|
||||||
return playerCounts;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private set {
|
|
||||||
EventHandler<int?>? onlinePlayerCountChanged;
|
|
||||||
lock (this) {
|
|
||||||
if (playerCounts == value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
playerCounts = value;
|
|
||||||
onlinePlayerCountChanged = OnlinePlayerCountChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
onlinePlayerCountChanged?.Invoke(this, value?.Online);
|
|
||||||
controllerConnection.Send(new ReportInstancePlayerCountsMessage(instanceGuid, value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private event EventHandler<int?>? OnlinePlayerCountChanged;
|
private event EventHandler<int?>? OnlinePlayerCountChanged;
|
||||||
|
|
||||||
private bool isDisposed = false;
|
private bool isDisposed = false;
|
||||||
@@ -64,7 +41,7 @@ sealed class InstancePlayerCountTracker : CancellableBackgroundTask {
|
|||||||
while (!CancellationToken.IsCancellationRequested) {
|
while (!CancellationToken.IsCancellationRequested) {
|
||||||
serverOutputEvent.Reset();
|
serverOutputEvent.Reset();
|
||||||
|
|
||||||
PlayerCounts = await TryGetPlayerCounts();
|
await UpdatePlayerCounts(await TryGetPlayerCounts());
|
||||||
|
|
||||||
if (!firstDetection.Task.IsCompleted) {
|
if (!firstDetection.Task.IsCompleted) {
|
||||||
firstDetection.SetResult();
|
firstDetection.SetResult();
|
||||||
@@ -82,7 +59,7 @@ sealed class InstancePlayerCountTracker : CancellableBackgroundTask {
|
|||||||
Logger.Debug("Detected {OnlinePlayerCount} / {MaximumPlayerCount} online player(s).", result.Online, result.Maximum);
|
Logger.Debug("Detected {OnlinePlayerCount} / {MaximumPlayerCount} online player(s).", result.Online, result.Maximum);
|
||||||
return result;
|
return result;
|
||||||
} catch (ServerStatusProtocol.ProtocolException e) {
|
} catch (ServerStatusProtocol.ProtocolException e) {
|
||||||
Logger.Error(e.Message);
|
Logger.Error("{Message}", e.Message);
|
||||||
return null;
|
return null;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Logger.Error(e, "Caught exception while checking online player count.");
|
Logger.Error(e, "Caught exception while checking online player count.");
|
||||||
@@ -90,6 +67,21 @@ sealed class InstancePlayerCountTracker : CancellableBackgroundTask {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task UpdatePlayerCounts(InstancePlayerCounts? value) {
|
||||||
|
EventHandler<int?>? onlinePlayerCountChanged;
|
||||||
|
lock (this) {
|
||||||
|
if (playerCounts == value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
playerCounts = value;
|
||||||
|
onlinePlayerCountChanged = OnlinePlayerCountChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
onlinePlayerCountChanged?.Invoke(this, value?.Online);
|
||||||
|
await controllerConnection.Send(new ReportInstancePlayerCountsMessage(instanceGuid, value), CancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task WaitForOnlinePlayers(CancellationToken cancellationToken) {
|
public async Task WaitForOnlinePlayers(CancellationToken cancellationToken) {
|
||||||
await firstDetection.Task.WaitAsync(cancellationToken);
|
await firstDetection.Task.WaitAsync(cancellationToken);
|
||||||
|
|
||||||
|
@@ -8,7 +8,6 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Common\Phantom.Common.Messages.Agent\Phantom.Common.Messages.Agent.csproj" />
|
<ProjectReference Include="..\..\Common\Phantom.Common.Messages.Agent\Phantom.Common.Messages.Agent.csproj" />
|
||||||
<ProjectReference Include="..\Phantom.Agent.Minecraft\Phantom.Agent.Minecraft.csproj" />
|
<ProjectReference Include="..\Phantom.Agent.Minecraft\Phantom.Agent.Minecraft.csproj" />
|
||||||
<ProjectReference Include="..\Phantom.Agent.Rpc\Phantom.Agent.Rpc.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
19
Agent/Phantom.Agent.Services/Rpc/ControllerConnection.cs
Normal file
19
Agent/Phantom.Agent.Services/Rpc/ControllerConnection.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using Phantom.Common.Messages.Agent;
|
||||||
|
using Phantom.Utils.Actor;
|
||||||
|
using Phantom.Utils.Rpc.Message;
|
||||||
|
|
||||||
|
namespace Phantom.Agent.Services.Rpc;
|
||||||
|
|
||||||
|
public sealed class ControllerConnection(MessageSender<IMessageToController> sender) {
|
||||||
|
internal bool TrySend<TMessage>(TMessage message) where TMessage : IMessageToController {
|
||||||
|
return sender.TrySend(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal ValueTask Send<TMessage>(TMessage message, CancellationToken cancellationToken) where TMessage : IMessageToController {
|
||||||
|
return sender.Send(message, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Task<TReply> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken cancellationToken) where TMessage : IMessageToController, ICanReply<TReply> {
|
||||||
|
return sender.Send<TMessage, TReply>(message, waitForReplyTime, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
65
Agent/Phantom.Agent.Services/Rpc/ControllerHandshake.cs
Normal file
65
Agent/Phantom.Agent.Services/Rpc/ControllerHandshake.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
using System.Collections.Immutable;
|
||||||
|
using Phantom.Common.Messages.Agent.Handshake;
|
||||||
|
using Phantom.Common.Messages.Agent.ToAgent;
|
||||||
|
using Phantom.Utils.Logging;
|
||||||
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
|
using Phantom.Utils.Rpc.Runtime.Client;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace Phantom.Agent.Services.Rpc;
|
||||||
|
|
||||||
|
public sealed class ControllerHandshake(AgentRegistration registration) : IRpcClientHandshake {
|
||||||
|
private const int MaxInstances = 100_000;
|
||||||
|
private const int MaxMessageBytes = 1024 * 1024 * 8;
|
||||||
|
|
||||||
|
private readonly ILogger logger = PhantomLogger.Create<ControllerHandshake>();
|
||||||
|
|
||||||
|
private readonly Lock responseLock = new ();
|
||||||
|
private ImmutableArray<ConfigureInstanceMessage> response;
|
||||||
|
|
||||||
|
internal ImmutableArray<ConfigureInstanceMessage> Response {
|
||||||
|
get {
|
||||||
|
lock (responseLock) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private set {
|
||||||
|
lock (responseLock) {
|
||||||
|
response = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Perform(Stream stream, CancellationToken cancellationToken) {
|
||||||
|
logger.Information("Registering with the controller...");
|
||||||
|
|
||||||
|
ReadOnlyMemory<byte> serializedRegistration = RpcSerialization.Serialize(registration);
|
||||||
|
await RpcSerialization.WriteSignedInt(serializedRegistration.Length, stream, cancellationToken);
|
||||||
|
await stream.WriteAsync(serializedRegistration, cancellationToken);
|
||||||
|
|
||||||
|
if (await RpcSerialization.ReadByte(stream, cancellationToken) == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint configureInstanceMessageCount = await RpcSerialization.ReadUnsignedInt(stream, cancellationToken);
|
||||||
|
if (configureInstanceMessageCount > MaxInstances) {
|
||||||
|
throw new InvalidOperationException("Trying to configure too many instances (" + configureInstanceMessageCount + " > " + MaxInstances + ").");
|
||||||
|
}
|
||||||
|
|
||||||
|
var configureInstanceMessages = ImmutableArray.CreateBuilder<ConfigureInstanceMessage>();
|
||||||
|
|
||||||
|
for (int index = 0; index < configureInstanceMessageCount; index++) {
|
||||||
|
int serializedMessageLength = await RpcSerialization.ReadSignedInt(stream, cancellationToken);
|
||||||
|
if (serializedMessageLength is < 0 or > MaxMessageBytes) {
|
||||||
|
throw new InvalidOperationException("Message must be between 0 and " + MaxMessageBytes + " bytes.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var serializedMessage = await RpcSerialization.ReadBytes(serializedMessageLength, stream, cancellationToken);
|
||||||
|
configureInstanceMessages.Add(RpcSerialization.Deserialize<ConfigureInstanceMessage>(serializedMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO handle response, only if new session
|
||||||
|
Response = configureInstanceMessages.ToImmutable();
|
||||||
|
logger.Information("Registration complete.");
|
||||||
|
}
|
||||||
|
}
|
@@ -1,86 +1,32 @@
|
|||||||
using Phantom.Agent.Services.Instances;
|
using Phantom.Agent.Services.Instances;
|
||||||
using Phantom.Common.Data;
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Instance;
|
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
using Phantom.Common.Messages.Agent;
|
using Phantom.Common.Messages.Agent;
|
||||||
using Phantom.Common.Messages.Agent.BiDirectional;
|
|
||||||
using Phantom.Common.Messages.Agent.ToAgent;
|
using Phantom.Common.Messages.Agent.ToAgent;
|
||||||
using Phantom.Common.Messages.Agent.ToController;
|
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
using Phantom.Utils.Logging;
|
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
namespace Phantom.Agent.Services.Rpc;
|
namespace Phantom.Agent.Services.Rpc;
|
||||||
|
|
||||||
public sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToAgent> {
|
public sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToAgent> {
|
||||||
private static ILogger Logger { get; } = PhantomLogger.Create<ControllerMessageHandlerActor>();
|
public readonly record struct Init(AgentServices Agent);
|
||||||
|
|
||||||
public readonly record struct Init(RpcConnectionToServer<IMessageToController> Connection, AgentServices Agent, CancellationTokenSource ShutdownTokenSource);
|
|
||||||
|
|
||||||
public static Props<IMessageToAgent> Factory(Init init) {
|
public static Props<IMessageToAgent> Factory(Init init) {
|
||||||
return Props<IMessageToAgent>.Create(() => new ControllerMessageHandlerActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume });
|
return Props<IMessageToAgent>.Create(() => new ControllerMessageHandlerActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume });
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly RpcConnectionToServer<IMessageToController> connection;
|
|
||||||
private readonly AgentServices agent;
|
private readonly AgentServices agent;
|
||||||
private readonly CancellationTokenSource shutdownTokenSource;
|
|
||||||
|
|
||||||
private ControllerMessageHandlerActor(Init init) {
|
private ControllerMessageHandlerActor(Init init) {
|
||||||
this.connection = init.Connection;
|
|
||||||
this.agent = init.Agent;
|
this.agent = init.Agent;
|
||||||
this.shutdownTokenSource = init.ShutdownTokenSource;
|
|
||||||
|
|
||||||
ReceiveAsync<RegisterAgentSuccessMessage>(HandleRegisterAgentSuccess);
|
|
||||||
Receive<RegisterAgentFailureMessage>(HandleRegisterAgentFailure);
|
|
||||||
ReceiveAndReplyLater<ConfigureInstanceMessage, Result<ConfigureInstanceResult, InstanceActionFailure>>(HandleConfigureInstance);
|
ReceiveAndReplyLater<ConfigureInstanceMessage, Result<ConfigureInstanceResult, InstanceActionFailure>>(HandleConfigureInstance);
|
||||||
ReceiveAndReplyLater<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(HandleLaunchInstance);
|
ReceiveAndReplyLater<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(HandleLaunchInstance);
|
||||||
ReceiveAndReplyLater<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(HandleStopInstance);
|
ReceiveAndReplyLater<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(HandleStopInstance);
|
||||||
ReceiveAndReplyLater<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(HandleSendCommandToInstance);
|
ReceiveAndReplyLater<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(HandleSendCommandToInstance);
|
||||||
Receive<ReplyMessage>(HandleReply);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleRegisterAgentSuccess(RegisterAgentSuccessMessage message) {
|
|
||||||
Logger.Information("Agent authentication successful.");
|
|
||||||
|
|
||||||
void ShutdownAfterConfigurationFailed(Guid instanceGuid, InstanceConfiguration configuration) {
|
|
||||||
Logger.Fatal("Unable to configure instance \"{Name}\" (GUID {Guid}), shutting down.", configuration.InstanceName, instanceGuid);
|
|
||||||
shutdownTokenSource.Cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var configureInstanceMessage in message.InitialInstanceConfigurations) {
|
|
||||||
var result = await HandleConfigureInstance(configureInstanceMessage, alwaysReportStatus: true);
|
|
||||||
if (!result.Is(ConfigureInstanceResult.Success)) {
|
|
||||||
ShutdownAfterConfigurationFailed(configureInstanceMessage.InstanceGuid, configureInstanceMessage.Configuration);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connection.SetIsReady();
|
|
||||||
|
|
||||||
await connection.Send(new AdvertiseJavaRuntimesMessage(agent.JavaRuntimeRepository.All));
|
|
||||||
agent.InstanceTicketManager.RefreshAgentStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleRegisterAgentFailure(RegisterAgentFailureMessage message) {
|
|
||||||
string errorMessage = message.FailureKind switch {
|
|
||||||
RegisterAgentFailure.ConnectionAlreadyHasAnAgent => "This connection already has an associated agent.",
|
|
||||||
RegisterAgentFailure.InvalidToken => "Invalid token.",
|
|
||||||
_ => "Unknown error " + (byte) message.FailureKind + "."
|
|
||||||
};
|
|
||||||
|
|
||||||
Logger.Fatal("Agent authentication failed: {Error}", errorMessage);
|
|
||||||
|
|
||||||
PhantomLogger.Dispose();
|
|
||||||
Environment.Exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task<Result<ConfigureInstanceResult, InstanceActionFailure>> HandleConfigureInstance(ConfigureInstanceMessage message, bool alwaysReportStatus) {
|
|
||||||
return agent.InstanceManager.Request(new InstanceManagerActor.ConfigureInstanceCommand(message.InstanceGuid, message.Configuration, message.LaunchProperties, message.LaunchNow, alwaysReportStatus));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Result<ConfigureInstanceResult, InstanceActionFailure>> HandleConfigureInstance(ConfigureInstanceMessage message) {
|
private async Task<Result<ConfigureInstanceResult, InstanceActionFailure>> HandleConfigureInstance(ConfigureInstanceMessage message) {
|
||||||
return await HandleConfigureInstance(message, alwaysReportStatus: false);
|
return await agent.InstanceManager.Request(new InstanceManagerActor.ConfigureInstanceCommand(message.InstanceGuid, message.Configuration, message.LaunchProperties, message.LaunchNow, AlwaysReportStatus: false));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Result<LaunchInstanceResult, InstanceActionFailure>> HandleLaunchInstance(LaunchInstanceMessage message) {
|
private async Task<Result<LaunchInstanceResult, InstanceActionFailure>> HandleLaunchInstance(LaunchInstanceMessage message) {
|
||||||
@@ -94,8 +40,4 @@ public sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToAgent
|
|||||||
private async Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) {
|
private async Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) {
|
||||||
return await agent.InstanceManager.Request(new InstanceManagerActor.SendCommandToInstanceCommand(message.InstanceGuid, message.Command));
|
return await agent.InstanceManager.Request(new InstanceManagerActor.SendCommandToInstanceCommand(message.InstanceGuid, message.Command));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleReply(ReplyMessage message) {
|
|
||||||
connection.Receive(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
53
Agent/Phantom.Agent.Services/Rpc/ControllerSendQueue.cs
Normal file
53
Agent/Phantom.Agent.Services/Rpc/ControllerSendQueue.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using System.Threading.Channels;
|
||||||
|
using Phantom.Common.Messages.Agent;
|
||||||
|
using Phantom.Utils.Logging;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace Phantom.Agent.Services.Rpc;
|
||||||
|
|
||||||
|
sealed class ControllerSendQueue<TMessage> where TMessage : IMessageToController {
|
||||||
|
private readonly ILogger logger;
|
||||||
|
private readonly Channel<TMessage> channel;
|
||||||
|
private readonly Task sendTask;
|
||||||
|
private readonly CancellationTokenSource shutdownTokenSource = new ();
|
||||||
|
|
||||||
|
public ControllerSendQueue(ControllerConnection controllerConnection, string loggerName, int capacity, bool singleWriter) {
|
||||||
|
this.logger = PhantomLogger.Create<ControllerSendQueue<TMessage>>(loggerName);
|
||||||
|
|
||||||
|
this.channel = Channel.CreateBounded<TMessage>(new BoundedChannelOptions(capacity) {
|
||||||
|
AllowSynchronousContinuations = false,
|
||||||
|
FullMode = BoundedChannelFullMode.DropOldest,
|
||||||
|
SingleReader = true,
|
||||||
|
SingleWriter = singleWriter,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.sendTask = Send(controllerConnection, shutdownTokenSource.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Send(ControllerConnection controllerConnection, CancellationToken cancellationToken) {
|
||||||
|
await foreach (var message in channel.Reader.ReadAllAsync(cancellationToken)) {
|
||||||
|
await controllerConnection.Send(message, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Enqueue(TMessage message) {
|
||||||
|
channel.Writer.TryWrite(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Shutdown(TimeSpan gracefulTimeout) {
|
||||||
|
channel.Writer.TryComplete();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendTask.WaitAsync(gracefulTimeout);
|
||||||
|
} catch (TimeoutException) {
|
||||||
|
logger.Warning("Timed out waiting for queue to finish processing.");
|
||||||
|
} catch (Exception) {
|
||||||
|
// Ignore.
|
||||||
|
}
|
||||||
|
|
||||||
|
await shutdownTokenSource.CancelAsync();
|
||||||
|
await sendTask.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
|
||||||
|
|
||||||
|
shutdownTokenSource.Dispose();
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
using NetMQ;
|
using System.Text;
|
||||||
using Phantom.Common.Data;
|
using Phantom.Common.Data;
|
||||||
using Phantom.Utils.Cryptography;
|
using Phantom.Utils.Cryptography;
|
||||||
using Phantom.Utils.IO;
|
using Phantom.Utils.IO;
|
||||||
@@ -10,7 +10,7 @@ namespace Phantom.Agent;
|
|||||||
static class AgentKey {
|
static class AgentKey {
|
||||||
private static ILogger Logger { get; } = PhantomLogger.Create(nameof(AgentKey));
|
private static ILogger Logger { get; } = PhantomLogger.Create(nameof(AgentKey));
|
||||||
|
|
||||||
public static Task<(NetMQCertificate, AuthToken)?> Load(string? agentKeyToken, string? agentKeyFilePath) {
|
public static Task<ConnectionKey?> Load(string? agentKeyToken, string? agentKeyFilePath) {
|
||||||
if (agentKeyFilePath != null) {
|
if (agentKeyFilePath != null) {
|
||||||
return LoadFromFile(agentKeyFilePath);
|
return LoadFromFile(agentKeyFilePath);
|
||||||
}
|
}
|
||||||
@@ -22,18 +22,19 @@ static class AgentKey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<(NetMQCertificate, AuthToken)?> LoadFromFile(string agentKeyFilePath) {
|
private static async Task<ConnectionKey?> LoadFromFile(string agentKeyFilePath) {
|
||||||
if (!File.Exists(agentKeyFilePath)) {
|
if (!File.Exists(agentKeyFilePath)) {
|
||||||
Logger.Fatal("Missing agent key file: {AgentKeyFilePath}", agentKeyFilePath);
|
Logger.Fatal("Missing agent key file: {AgentKeyFilePath}", agentKeyFilePath);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Files.RequireMaximumFileSize(agentKeyFilePath, 64);
|
Files.RequireMaximumFileSize(agentKeyFilePath, maximumBytes: 64);
|
||||||
return LoadFromBytes(await File.ReadAllBytesAsync(agentKeyFilePath));
|
string[] lines = await File.ReadAllLinesAsync(agentKeyFilePath, Encoding.UTF8);
|
||||||
|
return LoadFromToken(lines[0]);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Logger.Fatal("Error loading agent key from file: {AgentKeyFilePath}", agentKeyFilePath);
|
Logger.Fatal("Error loading agent key from file: {AgentKeyFilePath}", agentKeyFilePath);
|
||||||
Logger.Fatal(e.Message);
|
Logger.Fatal("{Message}", e.Message);
|
||||||
return null;
|
return null;
|
||||||
} catch (Exception) {
|
} catch (Exception) {
|
||||||
Logger.Fatal("File does not contain a valid agent key: {AgentKeyFilePath}", agentKeyFilePath);
|
Logger.Fatal("File does not contain a valid agent key: {AgentKeyFilePath}", agentKeyFilePath);
|
||||||
@@ -41,7 +42,7 @@ static class AgentKey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (NetMQCertificate, AuthToken)? LoadFromToken(string agentKey) {
|
private static ConnectionKey? LoadFromToken(string agentKey) {
|
||||||
try {
|
try {
|
||||||
return LoadFromBytes(TokenGenerator.DecodeBytes(agentKey));
|
return LoadFromBytes(TokenGenerator.DecodeBytes(agentKey));
|
||||||
} catch (Exception) {
|
} catch (Exception) {
|
||||||
@@ -50,11 +51,9 @@ static class AgentKey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (NetMQCertificate, AuthToken)? LoadFromBytes(byte[] agentKey) {
|
private static ConnectionKey? LoadFromBytes(byte[] agentKey) {
|
||||||
var (publicKey, agentToken) = ConnectionCommonKey.FromBytes(agentKey);
|
var connectionKey = ConnectionKey.FromBytes(agentKey);
|
||||||
var controllerCertificate = NetMQCertificate.FromPublicKey(publicKey);
|
|
||||||
|
|
||||||
Logger.Information("Loaded agent key.");
|
Logger.Information("Loaded agent key.");
|
||||||
return (controllerCertificate, agentToken);
|
return connectionKey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,17 +1,19 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using NetMQ;
|
|
||||||
using Phantom.Agent;
|
using Phantom.Agent;
|
||||||
using Phantom.Agent.Rpc;
|
using Phantom.Agent.Minecraft.Java;
|
||||||
using Phantom.Agent.Services;
|
using Phantom.Agent.Services;
|
||||||
using Phantom.Agent.Services.Rpc;
|
using Phantom.Agent.Services.Rpc;
|
||||||
using Phantom.Common.Data.Agent;
|
using Phantom.Common.Data.Agent;
|
||||||
using Phantom.Common.Messages.Agent;
|
using Phantom.Common.Messages.Agent;
|
||||||
|
using Phantom.Common.Messages.Agent.Handshake;
|
||||||
using Phantom.Common.Messages.Agent.ToController;
|
using Phantom.Common.Messages.Agent.ToController;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
using Phantom.Utils.Rpc;
|
using Phantom.Utils.Rpc.Message;
|
||||||
using Phantom.Utils.Rpc.Sockets;
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
|
using Phantom.Utils.Rpc.Runtime.Client;
|
||||||
using Phantom.Utils.Runtime;
|
using Phantom.Utils.Runtime;
|
||||||
|
using Phantom.Utils.Threading;
|
||||||
|
|
||||||
const int ProtocolVersion = 1;
|
const int ProtocolVersion = 1;
|
||||||
|
|
||||||
@@ -48,33 +50,64 @@ try {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
var (controllerCertificate, agentToken) = agentKey.Value;
|
|
||||||
var agentInfo = new AgentInfo(agentGuid.Value, agentName, ProtocolVersion, fullVersion, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts);
|
var agentInfo = new AgentInfo(agentGuid.Value, agentName, ProtocolVersion, fullVersion, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts);
|
||||||
|
var javaRuntimeRepository = await JavaRuntimeDiscovery.Scan(folders.JavaSearchFolderPath, shutdownCancellationToken);
|
||||||
|
var controllerHandshake = new ControllerHandshake(new AgentRegistration(agentInfo, javaRuntimeRepository.All));
|
||||||
|
|
||||||
PhantomLogger.Root.InformationHeading("Launching Phantom Panel agent...");
|
var rpcClientConnectionParameters = new RpcClientConnectionParameters(
|
||||||
|
Host: controllerHost,
|
||||||
|
Port: controllerPort,
|
||||||
|
DistinguishedName: "phantom-controller",
|
||||||
|
CertificateThumbprint: agentKey.Value.CertificateThumbprint,
|
||||||
|
AuthToken: agentKey.Value.AuthToken,
|
||||||
|
Handshake: controllerHandshake,
|
||||||
|
CommonParameters: new RpcCommonConnectionParameters(
|
||||||
|
MessageQueueCapacity: 250,
|
||||||
|
FrameQueueCapacity: 500,
|
||||||
|
MaxConcurrentlyHandledMessages: 50,
|
||||||
|
PingInterval: TimeSpan.FromSeconds(10)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
var rpcConfiguration = new RpcConfiguration("Agent", controllerHost, controllerPort, controllerCertificate);
|
using var rpcClient = await RpcClient<IMessageToController, IMessageToAgent>.Connect("Controller", rpcClientConnectionParameters, AgentMessageRegistries.Definitions, shutdownCancellationToken);
|
||||||
var rpcSocket = RpcClientSocket.Connect(rpcConfiguration, AgentMessageRegistries.Definitions, new RegisterAgentMessage(agentToken, agentInfo));
|
if (rpcClient == null) {
|
||||||
|
PhantomLogger.Root.Fatal("Could not connect to Phantom Controller, shutting down.");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
var agentServices = new AgentServices(agentInfo, folders, new AgentServiceConfiguration(maxConcurrentBackupCompressionTasks), new ControllerConnection(rpcSocket.Connection));
|
Task? rpcClientListener = null;
|
||||||
await agentServices.Initialize();
|
|
||||||
|
|
||||||
var rpcMessageHandlerInit = new ControllerMessageHandlerActor.Init(rpcSocket.Connection, agentServices, shutdownCancellationTokenSource);
|
|
||||||
var rpcMessageHandlerActor = agentServices.ActorSystem.ActorOf(ControllerMessageHandlerActor.Factory(rpcMessageHandlerInit), "ControllerMessageHandler");
|
|
||||||
|
|
||||||
var rpcDisconnectSemaphore = new SemaphoreSlim(0, 1);
|
|
||||||
var rpcTask = RpcClientRuntime.Launch(rpcSocket, rpcMessageHandlerActor, rpcDisconnectSemaphore, shutdownCancellationToken);
|
|
||||||
try {
|
try {
|
||||||
await rpcTask.WaitAsync(shutdownCancellationToken);
|
PhantomLogger.Root.InformationHeading("Launching Phantom Panel agent...");
|
||||||
} finally {
|
|
||||||
shutdownCancellationTokenSource.Cancel();
|
var agentServices = new AgentServices(agentInfo, folders, new AgentServiceConfiguration(maxConcurrentBackupCompressionTasks), new ControllerConnection(rpcClient.MessageSender), javaRuntimeRepository);
|
||||||
|
|
||||||
|
var rpcMessageHandlerInit = new ControllerMessageHandlerActor.Init(agentServices);
|
||||||
|
var rpcMessageHandlerActor = agentServices.ActorSystem.ActorOf(ControllerMessageHandlerActor.Factory(rpcMessageHandlerInit), "ControllerMessageHandler");
|
||||||
|
|
||||||
|
rpcClientListener = rpcClient.Listen(new IMessageReceiver<IMessageToAgent>.Actor(rpcMessageHandlerActor));
|
||||||
|
|
||||||
|
if (await agentServices.Register(controllerHandshake, shutdownCancellationToken)) {
|
||||||
|
PhantomLogger.Root.Information("Phantom Panel agent is ready.");
|
||||||
|
await shutdownCancellationToken.WaitHandle.WaitOneAsync();
|
||||||
|
}
|
||||||
|
|
||||||
await agentServices.Shutdown();
|
await agentServices.Shutdown();
|
||||||
|
} finally {
|
||||||
rpcDisconnectSemaphore.Release();
|
PhantomLogger.Root.Information("Unregistering agent...");
|
||||||
await rpcTask;
|
try {
|
||||||
rpcDisconnectSemaphore.Dispose();
|
using var unregisterCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||||
|
await rpcClient.MessageSender.Send(new UnregisterAgentMessage(), unregisterCancellationTokenSource.Token);
|
||||||
NetMQConfig.Cleanup();
|
} catch (OperationCanceledException) {
|
||||||
|
PhantomLogger.Root.Warning("Could not unregister agent after shutdown.");
|
||||||
|
} catch (Exception e) {
|
||||||
|
PhantomLogger.Root.Warning(e, "Could not unregister agent during shutdown.");
|
||||||
|
} finally {
|
||||||
|
await rpcClient.Shutdown();
|
||||||
|
|
||||||
|
if (rpcClientListener != null) {
|
||||||
|
await rpcClientListener;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
@@ -1,37 +0,0 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using MemoryPack;
|
|
||||||
|
|
||||||
namespace Phantom.Common.Data;
|
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
|
||||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
|
|
||||||
public sealed partial class AuthToken {
|
|
||||||
internal const int Length = 12;
|
|
||||||
|
|
||||||
[MemoryPackOrder(0)]
|
|
||||||
[MemoryPackInclude]
|
|
||||||
private readonly byte[] bytes;
|
|
||||||
|
|
||||||
internal AuthToken(byte[]? bytes) {
|
|
||||||
ArgumentNullException.ThrowIfNull(bytes);
|
|
||||||
|
|
||||||
if (bytes.Length != Length) {
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(bytes), "Invalid token length: " + bytes.Length + ". Token length must be exactly " + Length + " bytes.");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.bytes = bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool FixedTimeEquals(AuthToken providedAuthToken) {
|
|
||||||
return CryptographicOperations.FixedTimeEquals(bytes, providedAuthToken.bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void WriteTo(Span<byte> span) {
|
|
||||||
bytes.CopyTo(span);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static AuthToken Generate() {
|
|
||||||
return new AuthToken(RandomNumberGenerator.GetBytes(Length));
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,18 +0,0 @@
|
|||||||
namespace Phantom.Common.Data;
|
|
||||||
|
|
||||||
public readonly record struct ConnectionCommonKey(byte[] CertificatePublicKey, AuthToken AuthToken) {
|
|
||||||
private const byte TokenLength = AuthToken.Length;
|
|
||||||
|
|
||||||
public byte[] ToBytes() {
|
|
||||||
Span<byte> result = stackalloc byte[TokenLength + CertificatePublicKey.Length];
|
|
||||||
AuthToken.WriteTo(result[..TokenLength]);
|
|
||||||
CertificatePublicKey.CopyTo(result[TokenLength..]);
|
|
||||||
return result.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ConnectionCommonKey FromBytes(byte[] agentKey) {
|
|
||||||
var authToken = new AuthToken(agentKey[..TokenLength]);
|
|
||||||
var certificatePublicKey = agentKey[TokenLength..];
|
|
||||||
return new ConnectionCommonKey(certificatePublicKey, authToken);
|
|
||||||
}
|
|
||||||
}
|
|
21
Common/Phantom.Common.Data/ConnectionKey.cs
Normal file
21
Common/Phantom.Common.Data/ConnectionKey.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using Phantom.Utils.Rpc.Handshake;
|
||||||
|
using Phantom.Utils.Rpc.Runtime.Tls;
|
||||||
|
|
||||||
|
namespace Phantom.Common.Data;
|
||||||
|
|
||||||
|
public readonly record struct ConnectionKey(RpcCertificateThumbprint CertificateThumbprint, AuthToken AuthToken) {
|
||||||
|
private const byte TokenLength = AuthToken.Length;
|
||||||
|
|
||||||
|
public byte[] ToBytes() {
|
||||||
|
Span<byte> result = stackalloc byte[TokenLength + CertificateThumbprint.Bytes.Length];
|
||||||
|
AuthToken.Bytes.CopyTo(result[..TokenLength]);
|
||||||
|
CertificateThumbprint.Bytes.CopyTo(result[TokenLength..]);
|
||||||
|
return result.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ConnectionKey FromBytes(ReadOnlySpan<byte> data) {
|
||||||
|
var authToken = new AuthToken([..data[..TokenLength]]);
|
||||||
|
var certificateThumbprint = RpcCertificateThumbprint.From(data[TokenLength..]);
|
||||||
|
return new ConnectionKey(certificateThumbprint, authToken);
|
||||||
|
}
|
||||||
|
}
|
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Utils\Phantom.Utils\Phantom.Utils.csproj" />
|
<ProjectReference Include="..\..\Utils\Phantom.Utils\Phantom.Utils.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Utils\Phantom.Utils.Rpc\Phantom.Utils.Rpc.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@@ -1,6 +0,0 @@
|
|||||||
namespace Phantom.Common.Data.Replies;
|
|
||||||
|
|
||||||
public enum RegisterAgentFailure : byte {
|
|
||||||
ConnectionAlreadyHasAnAgent,
|
|
||||||
InvalidToken
|
|
||||||
}
|
|
@@ -1,5 +1,6 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using MemoryPack;
|
using MemoryPack;
|
||||||
|
using Phantom.Utils.Monads;
|
||||||
using Phantom.Utils.Result;
|
using Phantom.Utils.Result;
|
||||||
|
|
||||||
namespace Phantom.Common.Data;
|
namespace Phantom.Common.Data;
|
||||||
@@ -24,6 +25,9 @@ public sealed partial class Result<TValue, TError> {
|
|||||||
[MemoryPackIgnore]
|
[MemoryPackIgnore]
|
||||||
public TError Error => !hasValue ? error! : throw new InvalidOperationException("Attempted to get error from a success result.");
|
public TError Error => !hasValue ? error! : throw new InvalidOperationException("Attempted to get error from a success result.");
|
||||||
|
|
||||||
|
[MemoryPackIgnore]
|
||||||
|
public Either<TValue, TError> AsEither => hasValue ? Either.Left(value!) : Either.Right(error!);
|
||||||
|
|
||||||
private Result(bool hasValue, TValue? value, TError? error) {
|
private Result(bool hasValue, TValue? value, TError? error) {
|
||||||
this.hasValue = hasValue;
|
this.hasValue = hasValue;
|
||||||
this.value = value;
|
this.value = value;
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
using Phantom.Common.Data;
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
using Phantom.Common.Messages.Agent.BiDirectional;
|
|
||||||
using Phantom.Common.Messages.Agent.ToAgent;
|
using Phantom.Common.Messages.Agent.ToAgent;
|
||||||
using Phantom.Common.Messages.Agent.ToController;
|
using Phantom.Common.Messages.Agent.ToController;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
@@ -12,35 +11,24 @@ public static class AgentMessageRegistries {
|
|||||||
public static MessageRegistry<IMessageToAgent> ToAgent { get; } = new (PhantomLogger.Create("MessageRegistry", nameof(ToAgent)));
|
public static MessageRegistry<IMessageToAgent> ToAgent { get; } = new (PhantomLogger.Create("MessageRegistry", nameof(ToAgent)));
|
||||||
public static MessageRegistry<IMessageToController> ToController { get; } = new (PhantomLogger.Create("MessageRegistry", nameof(ToController)));
|
public static MessageRegistry<IMessageToController> ToController { get; } = new (PhantomLogger.Create("MessageRegistry", nameof(ToController)));
|
||||||
|
|
||||||
public static IMessageDefinitions<IMessageToAgent, IMessageToController, ReplyMessage> Definitions { get; } = new MessageDefinitions();
|
public static IMessageDefinitions<IMessageToController, IMessageToAgent> Definitions { get; } = new MessageDefinitions();
|
||||||
|
|
||||||
static AgentMessageRegistries() {
|
static AgentMessageRegistries() {
|
||||||
ToAgent.Add<RegisterAgentSuccessMessage>(0);
|
ToAgent.Add<ConfigureInstanceMessage, Result<ConfigureInstanceResult, InstanceActionFailure>>(1);
|
||||||
ToAgent.Add<RegisterAgentFailureMessage>(1);
|
ToAgent.Add<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(2);
|
||||||
ToAgent.Add<ConfigureInstanceMessage, Result<ConfigureInstanceResult, InstanceActionFailure>>(2);
|
ToAgent.Add<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(3);
|
||||||
ToAgent.Add<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(3);
|
ToAgent.Add<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(4);
|
||||||
ToAgent.Add<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(4);
|
|
||||||
ToAgent.Add<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(5);
|
|
||||||
ToAgent.Add<ReplyMessage>(127);
|
|
||||||
|
|
||||||
ToController.Add<RegisterAgentMessage>(0);
|
|
||||||
ToController.Add<UnregisterAgentMessage>(1);
|
ToController.Add<UnregisterAgentMessage>(1);
|
||||||
ToController.Add<AgentIsAliveMessage>(2);
|
ToController.Add<ReportInstanceStatusMessage>(2);
|
||||||
ToController.Add<AdvertiseJavaRuntimesMessage>(3);
|
ToController.Add<InstanceOutputMessage>(3);
|
||||||
ToController.Add<ReportInstanceStatusMessage>(4);
|
ToController.Add<ReportAgentStatusMessage>(4);
|
||||||
ToController.Add<InstanceOutputMessage>(5);
|
ToController.Add<ReportInstanceEventMessage>(5);
|
||||||
ToController.Add<ReportAgentStatusMessage>(6);
|
ToController.Add<ReportInstancePlayerCountsMessage>(6);
|
||||||
ToController.Add<ReportInstanceEventMessage>(7);
|
|
||||||
ToController.Add<ReportInstancePlayerCountsMessage>(8);
|
|
||||||
ToController.Add<ReplyMessage>(127);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class MessageDefinitions : IMessageDefinitions<IMessageToAgent, IMessageToController, ReplyMessage> {
|
private sealed class MessageDefinitions : IMessageDefinitions<IMessageToController, IMessageToAgent> {
|
||||||
public MessageRegistry<IMessageToAgent> ToClient => ToAgent;
|
public MessageRegistry<IMessageToAgent> ToClient => ToAgent;
|
||||||
public MessageRegistry<IMessageToController> ToServer => ToController;
|
public MessageRegistry<IMessageToController> ToServer => ToController;
|
||||||
|
|
||||||
public ReplyMessage CreateReplyMessage(uint sequenceId, byte[] serializedReply) {
|
|
||||||
return new ReplyMessage(sequenceId, serializedReply);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +0,0 @@
|
|||||||
using MemoryPack;
|
|
||||||
using Phantom.Utils.Rpc.Message;
|
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Agent.BiDirectional;
|
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
|
||||||
public sealed partial record ReplyMessage(
|
|
||||||
[property: MemoryPackOrder(0)] uint SequenceId,
|
|
||||||
[property: MemoryPackOrder(1)] byte[] SerializedReply
|
|
||||||
) : IMessageToController, IMessageToAgent, IReply;
|
|
@@ -0,0 +1,12 @@
|
|||||||
|
using System.Collections.Immutable;
|
||||||
|
using MemoryPack;
|
||||||
|
using Phantom.Common.Data.Agent;
|
||||||
|
using Phantom.Common.Data.Java;
|
||||||
|
|
||||||
|
namespace Phantom.Common.Messages.Agent.Handshake;
|
||||||
|
|
||||||
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
|
public sealed partial record AgentRegistration(
|
||||||
|
[property: MemoryPackOrder(0)] AgentInfo AgentInfo,
|
||||||
|
[property: MemoryPackOrder(1)] ImmutableArray<TaggedJavaRuntime> JavaRuntimes
|
||||||
|
);
|
@@ -1,9 +0,0 @@
|
|||||||
using MemoryPack;
|
|
||||||
using Phantom.Common.Data.Replies;
|
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Agent.ToAgent;
|
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
|
||||||
public sealed partial record RegisterAgentFailureMessage(
|
|
||||||
[property: MemoryPackOrder(0)] RegisterAgentFailure FailureKind
|
|
||||||
) : IMessageToAgent;
|
|
@@ -1,9 +0,0 @@
|
|||||||
using System.Collections.Immutable;
|
|
||||||
using MemoryPack;
|
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Agent.ToAgent;
|
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
|
||||||
public sealed partial record RegisterAgentSuccessMessage(
|
|
||||||
[property: MemoryPackOrder(0)] ImmutableArray<ConfigureInstanceMessage> InitialInstanceConfigurations
|
|
||||||
) : IMessageToAgent;
|
|
@@ -1,10 +0,0 @@
|
|||||||
using System.Collections.Immutable;
|
|
||||||
using MemoryPack;
|
|
||||||
using Phantom.Common.Data.Java;
|
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Agent.ToController;
|
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
|
||||||
public sealed partial record AdvertiseJavaRuntimesMessage(
|
|
||||||
[property: MemoryPackOrder(0)] ImmutableArray<TaggedJavaRuntime> Runtimes
|
|
||||||
) : IMessageToController;
|
|
@@ -1,6 +0,0 @@
|
|||||||
using MemoryPack;
|
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Agent.ToController;
|
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
|
||||||
public sealed partial record AgentIsAliveMessage : IMessageToController;
|
|
@@ -1,11 +0,0 @@
|
|||||||
using MemoryPack;
|
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Data.Agent;
|
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Agent.ToController;
|
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
|
||||||
public sealed partial record RegisterAgentMessage(
|
|
||||||
[property: MemoryPackOrder(0)] AuthToken AuthToken,
|
|
||||||
[property: MemoryPackOrder(1)] AgentInfo AgentInfo
|
|
||||||
) : IMessageToController;
|
|
@@ -1,10 +0,0 @@
|
|||||||
using MemoryPack;
|
|
||||||
using Phantom.Utils.Rpc.Message;
|
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Web.BiDirectional;
|
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
|
||||||
public sealed partial record ReplyMessage(
|
|
||||||
[property: MemoryPackOrder(0)] uint SequenceId,
|
|
||||||
[property: MemoryPackOrder(1)] byte[] SerializedReply
|
|
||||||
) : IMessageToController, IMessageToWeb, IReply;
|
|
@@ -1,9 +0,0 @@
|
|||||||
using MemoryPack;
|
|
||||||
using Phantom.Common.Data;
|
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Web.ToController;
|
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
|
||||||
public sealed partial record RegisterWebMessage(
|
|
||||||
[property: MemoryPackOrder(0)] AuthToken AuthToken
|
|
||||||
) : IMessageToController;
|
|
@@ -1,8 +0,0 @@
|
|||||||
using MemoryPack;
|
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Web.ToWeb;
|
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
|
||||||
public sealed partial record RegisterWebResultMessage(
|
|
||||||
[property: MemoryPackOrder(0)] bool Success
|
|
||||||
) : IMessageToWeb;
|
|
@@ -7,7 +7,6 @@ using Phantom.Common.Data.Web.AuditLog;
|
|||||||
using Phantom.Common.Data.Web.EventLog;
|
using Phantom.Common.Data.Web.EventLog;
|
||||||
using Phantom.Common.Data.Web.Instance;
|
using Phantom.Common.Data.Web.Instance;
|
||||||
using Phantom.Common.Data.Web.Users;
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Common.Messages.Web.BiDirectional;
|
|
||||||
using Phantom.Common.Messages.Web.ToController;
|
using Phantom.Common.Messages.Web.ToController;
|
||||||
using Phantom.Common.Messages.Web.ToWeb;
|
using Phantom.Common.Messages.Web.ToWeb;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
@@ -19,10 +18,9 @@ public static class WebMessageRegistries {
|
|||||||
public static MessageRegistry<IMessageToController> ToController { get; } = new (PhantomLogger.Create("MessageRegistry", nameof(ToController)));
|
public static MessageRegistry<IMessageToController> ToController { get; } = new (PhantomLogger.Create("MessageRegistry", nameof(ToController)));
|
||||||
public static MessageRegistry<IMessageToWeb> ToWeb { get; } = new (PhantomLogger.Create("MessageRegistry", nameof(ToWeb)));
|
public static MessageRegistry<IMessageToWeb> ToWeb { get; } = new (PhantomLogger.Create("MessageRegistry", nameof(ToWeb)));
|
||||||
|
|
||||||
public static IMessageDefinitions<IMessageToWeb, IMessageToController, ReplyMessage> Definitions { get; } = new MessageDefinitions();
|
public static IMessageDefinitions<IMessageToController, IMessageToWeb> Definitions { get; } = new MessageDefinitions();
|
||||||
|
|
||||||
static WebMessageRegistries() {
|
static WebMessageRegistries() {
|
||||||
ToController.Add<RegisterWebMessage>(0);
|
|
||||||
ToController.Add<UnregisterWebMessage>(1);
|
ToController.Add<UnregisterWebMessage>(1);
|
||||||
ToController.Add<LogInMessage, Optional<LogInSuccess>>(2);
|
ToController.Add<LogInMessage, Optional<LogInSuccess>>(2);
|
||||||
ToController.Add<LogOutMessage>(3);
|
ToController.Add<LogOutMessage>(3);
|
||||||
@@ -42,22 +40,15 @@ public static class WebMessageRegistries {
|
|||||||
ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(17);
|
ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(17);
|
||||||
ToController.Add<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(18);
|
ToController.Add<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(18);
|
||||||
ToController.Add<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(19);
|
ToController.Add<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(19);
|
||||||
ToController.Add<ReplyMessage>(127);
|
|
||||||
|
|
||||||
ToWeb.Add<RegisterWebResultMessage>(0);
|
|
||||||
ToWeb.Add<RefreshAgentsMessage>(1);
|
ToWeb.Add<RefreshAgentsMessage>(1);
|
||||||
ToWeb.Add<RefreshInstancesMessage>(2);
|
ToWeb.Add<RefreshInstancesMessage>(2);
|
||||||
ToWeb.Add<InstanceOutputMessage>(3);
|
ToWeb.Add<InstanceOutputMessage>(3);
|
||||||
ToWeb.Add<RefreshUserSessionMessage>(4);
|
ToWeb.Add<RefreshUserSessionMessage>(4);
|
||||||
ToWeb.Add<ReplyMessage>(127);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class MessageDefinitions : IMessageDefinitions<IMessageToWeb, IMessageToController, ReplyMessage> {
|
private sealed class MessageDefinitions : IMessageDefinitions<IMessageToController, IMessageToWeb> {
|
||||||
public MessageRegistry<IMessageToWeb> ToClient => ToWeb;
|
public MessageRegistry<IMessageToWeb> ToClient => ToWeb;
|
||||||
public MessageRegistry<IMessageToController> ToServer => ToController;
|
public MessageRegistry<IMessageToController> ToServer => ToController;
|
||||||
|
|
||||||
public ReplyMessage CreateReplyMessage(uint sequenceId, byte[] serializedReply) {
|
|
||||||
return new ReplyMessage(sequenceId, serializedReply);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -21,7 +21,7 @@ using Phantom.Utils.Actor.Mailbox;
|
|||||||
using Phantom.Utils.Actor.Tasks;
|
using Phantom.Utils.Actor.Tasks;
|
||||||
using Phantom.Utils.Collections;
|
using Phantom.Utils.Collections;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
using Phantom.Utils.Rpc.Runtime.Server;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
namespace Phantom.Controller.Services.Agents;
|
namespace Phantom.Controller.Services.Agents;
|
||||||
@@ -89,11 +89,11 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
|||||||
|
|
||||||
ReceiveAsync<InitializeCommand>(Initialize);
|
ReceiveAsync<InitializeCommand>(Initialize);
|
||||||
ReceiveAsyncAndReply<RegisterCommand, ImmutableArray<ConfigureInstanceMessage>>(Register);
|
ReceiveAsyncAndReply<RegisterCommand, ImmutableArray<ConfigureInstanceMessage>>(Register);
|
||||||
|
Receive<SetConnectionCommand>(SetConnection);
|
||||||
Receive<UnregisterCommand>(Unregister);
|
Receive<UnregisterCommand>(Unregister);
|
||||||
Receive<RefreshConnectionStatusCommand>(RefreshConnectionStatus);
|
Receive<RefreshConnectionStatusCommand>(RefreshConnectionStatus);
|
||||||
Receive<NotifyIsAliveCommand>(NotifyIsAlive);
|
Receive<NotifyIsAliveCommand>(NotifyIsAlive);
|
||||||
Receive<UpdateStatsCommand>(UpdateStats);
|
Receive<UpdateStatsCommand>(UpdateStats);
|
||||||
Receive<UpdateJavaRuntimesCommand>(UpdateJavaRuntimes);
|
|
||||||
ReceiveAndReplyLater<CreateOrUpdateInstanceCommand, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstance);
|
ReceiveAndReplyLater<CreateOrUpdateInstanceCommand, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstance);
|
||||||
Receive<UpdateInstanceStatusCommand>(UpdateInstanceStatus);
|
Receive<UpdateInstanceStatusCommand>(UpdateInstanceStatus);
|
||||||
Receive<UpdateInstancePlayerCountsCommand>(UpdateInstancePlayerCounts);
|
Receive<UpdateInstancePlayerCountsCommand>(UpdateInstancePlayerCounts);
|
||||||
@@ -168,13 +168,15 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
|||||||
return configurationMessages.ToImmutable();
|
return configurationMessages.ToImmutable();
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface ICommand {}
|
public interface ICommand;
|
||||||
|
|
||||||
private sealed record InitializeCommand : ICommand;
|
private sealed record InitializeCommand : ICommand;
|
||||||
|
|
||||||
public sealed record RegisterCommand(AgentConfiguration Configuration, RpcConnectionToClient<IMessageToAgent> Connection) : ICommand, ICanReply<ImmutableArray<ConfigureInstanceMessage>>;
|
public sealed record RegisterCommand(AgentConfiguration Configuration, ImmutableArray<TaggedJavaRuntime> JavaRuntimes) : ICommand, ICanReply<ImmutableArray<ConfigureInstanceMessage>>;
|
||||||
|
|
||||||
public sealed record UnregisterCommand(RpcConnectionToClient<IMessageToAgent> Connection) : ICommand;
|
public sealed record SetConnectionCommand(RpcServerToClientConnection<IMessageToController, IMessageToAgent> Connection) : ICommand;
|
||||||
|
|
||||||
|
public sealed record UnregisterCommand(RpcServerToClientConnection<IMessageToController, IMessageToAgent> Connection) : ICommand;
|
||||||
|
|
||||||
private sealed record RefreshConnectionStatusCommand : ICommand;
|
private sealed record RefreshConnectionStatusCommand : ICommand;
|
||||||
|
|
||||||
@@ -182,8 +184,6 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
|||||||
|
|
||||||
public sealed record UpdateStatsCommand(int RunningInstanceCount, RamAllocationUnits RunningInstanceMemory) : ICommand;
|
public sealed record UpdateStatsCommand(int RunningInstanceCount, RamAllocationUnits RunningInstanceMemory) : ICommand;
|
||||||
|
|
||||||
public sealed record UpdateJavaRuntimesCommand(ImmutableArray<TaggedJavaRuntime> JavaRuntimes) : ICommand;
|
|
||||||
|
|
||||||
public sealed record CreateOrUpdateInstanceCommand(Guid LoggedInUserGuid, Guid InstanceGuid, InstanceConfiguration Configuration) : ICommand, ICanReply<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>;
|
public sealed record CreateOrUpdateInstanceCommand(Guid LoggedInUserGuid, Guid InstanceGuid, InstanceConfiguration Configuration) : ICommand, ICanReply<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>;
|
||||||
|
|
||||||
public sealed record UpdateInstanceStatusCommand(Guid InstanceGuid, IInstanceStatus Status) : ICommand;
|
public sealed record UpdateInstanceStatusCommand(Guid InstanceGuid, IInstanceStatus Status) : ICommand;
|
||||||
@@ -229,7 +229,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
|||||||
var configurationMessages = await PrepareInitialConfigurationMessages();
|
var configurationMessages = await PrepareInitialConfigurationMessages();
|
||||||
|
|
||||||
configuration = command.Configuration;
|
configuration = command.Configuration;
|
||||||
connection.UpdateConnection(command.Connection, configuration.AgentName);
|
connection.SetAgentName(configuration.AgentName);
|
||||||
|
|
||||||
lastPingTime = DateTimeOffset.Now;
|
lastPingTime = DateTimeOffset.Now;
|
||||||
isOnline = true;
|
isOnline = true;
|
||||||
@@ -239,9 +239,16 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
|||||||
|
|
||||||
databaseStorageActor.Tell(new AgentDatabaseStorageActor.StoreAgentConfigurationCommand(configuration));
|
databaseStorageActor.Tell(new AgentDatabaseStorageActor.StoreAgentConfigurationCommand(configuration));
|
||||||
|
|
||||||
|
javaRuntimes = command.JavaRuntimes;
|
||||||
|
controllerState.UpdateAgentJavaRuntimes(agentGuid, javaRuntimes);
|
||||||
|
|
||||||
return configurationMessages;
|
return configurationMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SetConnection(SetConnectionCommand command) {
|
||||||
|
connection.UpdateConnection(command.Connection);
|
||||||
|
}
|
||||||
|
|
||||||
private void Unregister(UnregisterCommand command) {
|
private void Unregister(UnregisterCommand command) {
|
||||||
if (connection.CloseIfSame(command.Connection)) {
|
if (connection.CloseIfSame(command.Connection)) {
|
||||||
stats = null;
|
stats = null;
|
||||||
@@ -278,11 +285,6 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
|||||||
NotifyAgentUpdated();
|
NotifyAgentUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateJavaRuntimes(UpdateJavaRuntimesCommand command) {
|
|
||||||
javaRuntimes = command.JavaRuntimes;
|
|
||||||
controllerState.UpdateAgentJavaRuntimes(agentGuid, javaRuntimes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>> CreateOrUpdateInstance(CreateOrUpdateInstanceCommand command) {
|
private Task<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>> CreateOrUpdateInstance(CreateOrUpdateInstanceCommand command) {
|
||||||
var instanceConfiguration = command.Configuration;
|
var instanceConfiguration = command.Configuration;
|
||||||
|
|
||||||
|
@@ -1,66 +1,66 @@
|
|||||||
using Phantom.Common.Messages.Agent;
|
using Phantom.Common.Messages.Agent;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
using Phantom.Utils.Rpc.Runtime.Server;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
namespace Phantom.Controller.Services.Agents;
|
namespace Phantom.Controller.Services.Agents;
|
||||||
|
|
||||||
sealed class AgentConnection {
|
sealed class AgentConnection(Guid agentGuid, string agentName) {
|
||||||
private static readonly ILogger Logger = PhantomLogger.Create<AgentConnection>();
|
private static readonly ILogger Logger = PhantomLogger.Create<AgentConnection>();
|
||||||
|
|
||||||
private readonly Guid agentGuid;
|
private string agentName = agentName;
|
||||||
private string agentName;
|
private RpcServerToClientConnection<IMessageToController, IMessageToAgent>? connection;
|
||||||
|
|
||||||
private RpcConnectionToClient<IMessageToAgent>? connection;
|
public void SetAgentName(string newAgentName) {
|
||||||
|
Volatile.Write(ref agentName, newAgentName);
|
||||||
public AgentConnection(Guid agentGuid, string agentName) {
|
|
||||||
this.agentName = agentName;
|
|
||||||
this.agentGuid = agentGuid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateConnection(RpcConnectionToClient<IMessageToAgent> newConnection, string newAgentName) {
|
public void UpdateConnection(RpcServerToClientConnection<IMessageToController, IMessageToAgent> newConnection) {
|
||||||
lock (this) {
|
lock (this) {
|
||||||
connection?.Close();
|
if (connection != null) {
|
||||||
|
Task.Run(connection.ClientClosedSession);
|
||||||
|
}
|
||||||
|
|
||||||
connection = newConnection;
|
connection = newConnection;
|
||||||
agentName = newAgentName;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CloseIfSame(RpcConnectionToClient<IMessageToAgent> expected) {
|
public bool CloseIfSame(RpcServerToClientConnection<IMessageToController, IMessageToAgent> expected) {
|
||||||
lock (this) {
|
lock (this) {
|
||||||
if (connection != null && connection.IsSame(expected)) {
|
if (connection != null && ReferenceEquals(connection, expected)) {
|
||||||
connection.Close();
|
Task.Run(connection.ClientClosedSession);
|
||||||
|
connection = null;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask Send<TMessage>(TMessage message) where TMessage : IMessageToAgent {
|
||||||
|
lock (this) {
|
||||||
|
if (connection != null) {
|
||||||
|
return connection.MessageSender.Send(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
LogAgentOffline();
|
||||||
}
|
return ValueTask.CompletedTask;
|
||||||
|
|
||||||
public Task Send<TMessage>(TMessage message) where TMessage : IMessageToAgent {
|
|
||||||
lock (this) {
|
|
||||||
if (connection == null) {
|
|
||||||
LogAgentOffline();
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
return connection.Send(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken) where TMessage : IMessageToAgent, ICanReply<TReply> where TReply : class {
|
public Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken) where TMessage : IMessageToAgent, ICanReply<TReply> where TReply : class {
|
||||||
lock (this) {
|
lock (this) {
|
||||||
if (connection == null) {
|
if (connection != null) {
|
||||||
LogAgentOffline();
|
return connection.MessageSender.Send<TMessage, TReply>(message, waitForReplyTime, waitForReplyCancellationToken)!;
|
||||||
return Task.FromResult<TReply?>(default);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return connection.Send<TMessage, TReply>(message, waitForReplyTime, waitForReplyCancellationToken)!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LogAgentOffline();
|
||||||
|
return Task.FromResult<TReply?>(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LogAgentOffline() {
|
private void LogAgentOffline() {
|
||||||
Logger.Error("Could not send message to offline agent \"{Name}\" (GUID {Guid}).", agentName, agentGuid);
|
Logger.Error("Could not send message to offline agent \"{Name}\" (GUID {Guid}).", Volatile.Read(ref agentName), agentGuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,18 +2,16 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using Akka.Actor;
|
using Akka.Actor;
|
||||||
using Phantom.Common.Data;
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Agent;
|
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
using Phantom.Common.Data.Web.Agent;
|
using Phantom.Common.Data.Web.Agent;
|
||||||
using Phantom.Common.Data.Web.Users;
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Common.Messages.Agent;
|
using Phantom.Common.Messages.Agent.Handshake;
|
||||||
using Phantom.Common.Messages.Agent.ToAgent;
|
using Phantom.Common.Messages.Agent.ToAgent;
|
||||||
using Phantom.Controller.Database;
|
using Phantom.Controller.Database;
|
||||||
using Phantom.Controller.Minecraft;
|
using Phantom.Controller.Minecraft;
|
||||||
using Phantom.Controller.Services.Users.Sessions;
|
using Phantom.Controller.Services.Users.Sessions;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
namespace Phantom.Controller.Services.Agents;
|
namespace Phantom.Controller.Services.Agents;
|
||||||
@@ -22,19 +20,17 @@ sealed class AgentManager {
|
|||||||
private static readonly ILogger Logger = PhantomLogger.Create<AgentManager>();
|
private static readonly ILogger Logger = PhantomLogger.Create<AgentManager>();
|
||||||
|
|
||||||
private readonly IActorRefFactory actorSystem;
|
private readonly IActorRefFactory actorSystem;
|
||||||
private readonly AuthToken authToken;
|
|
||||||
private readonly ControllerState controllerState;
|
private readonly ControllerState controllerState;
|
||||||
private readonly MinecraftVersions minecraftVersions;
|
private readonly MinecraftVersions minecraftVersions;
|
||||||
private readonly UserLoginManager userLoginManager;
|
private readonly UserLoginManager userLoginManager;
|
||||||
private readonly IDbContextProvider dbProvider;
|
private readonly IDbContextProvider dbProvider;
|
||||||
private readonly CancellationToken cancellationToken;
|
private readonly CancellationToken cancellationToken;
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<Guid, ActorRef<AgentActor.ICommand>> agentsByGuid = new ();
|
private readonly ConcurrentDictionary<Guid, ActorRef<AgentActor.ICommand>> agentsByAgentGuid = new ();
|
||||||
private readonly Func<Guid, AgentConfiguration, ActorRef<AgentActor.ICommand>> addAgentActorFactory;
|
private readonly Func<Guid, AgentConfiguration, ActorRef<AgentActor.ICommand>> addAgentActorFactory;
|
||||||
|
|
||||||
public AgentManager(IActorRefFactory actorSystem, AuthToken authToken, ControllerState controllerState, MinecraftVersions minecraftVersions, UserLoginManager userLoginManager, IDbContextProvider dbProvider, CancellationToken cancellationToken) {
|
public AgentManager(IActorRefFactory actorSystem, ControllerState controllerState, MinecraftVersions minecraftVersions, UserLoginManager userLoginManager, IDbContextProvider dbProvider, CancellationToken cancellationToken) {
|
||||||
this.actorSystem = actorSystem;
|
this.actorSystem = actorSystem;
|
||||||
this.authToken = authToken;
|
|
||||||
this.controllerState = controllerState;
|
this.controllerState = controllerState;
|
||||||
this.minecraftVersions = minecraftVersions;
|
this.minecraftVersions = minecraftVersions;
|
||||||
this.userLoginManager = userLoginManager;
|
this.userLoginManager = userLoginManager;
|
||||||
@@ -57,28 +53,20 @@ sealed class AgentManager {
|
|||||||
var agentGuid = entity.AgentGuid;
|
var agentGuid = entity.AgentGuid;
|
||||||
var agentConfiguration = new AgentConfiguration(entity.Name, entity.ProtocolVersion, entity.BuildVersion, entity.MaxInstances, entity.MaxMemory);
|
var agentConfiguration = new AgentConfiguration(entity.Name, entity.ProtocolVersion, entity.BuildVersion, entity.MaxInstances, entity.MaxMemory);
|
||||||
|
|
||||||
if (agentsByGuid.TryAdd(agentGuid, CreateAgentActor(agentGuid, agentConfiguration))) {
|
if (agentsByAgentGuid.TryAdd(agentGuid, CreateAgentActor(agentGuid, agentConfiguration))) {
|
||||||
Logger.Information("Loaded agent \"{AgentName}\" (GUID {AgentGuid}) from database.", agentConfiguration.AgentName, agentGuid);
|
Logger.Information("Loaded agent \"{AgentName}\" (GUID {AgentGuid}) from database.", agentConfiguration.AgentName, agentGuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> RegisterAgent(AuthToken authToken, AgentInfo agentInfo, RpcConnectionToClient<IMessageToAgent> connection) {
|
public async Task<ImmutableArray<ConfigureInstanceMessage>> RegisterAgent(AgentRegistration registration) {
|
||||||
if (!this.authToken.FixedTimeEquals(authToken)) {
|
var agentConfiguration = AgentConfiguration.From(registration.AgentInfo);
|
||||||
await connection.Send(new RegisterAgentFailureMessage(RegisterAgentFailure.InvalidToken));
|
var agentActor = agentsByAgentGuid.GetOrAdd(registration.AgentInfo.AgentGuid, addAgentActorFactory, agentConfiguration);
|
||||||
return false;
|
return await agentActor.Request(new AgentActor.RegisterCommand(agentConfiguration, registration.JavaRuntimes), cancellationToken);
|
||||||
}
|
|
||||||
|
|
||||||
var agentProperties = AgentConfiguration.From(agentInfo);
|
|
||||||
var agentActor = agentsByGuid.GetOrAdd(agentInfo.AgentGuid, addAgentActorFactory, agentProperties);
|
|
||||||
var configureInstanceMessages = await agentActor.Request(new AgentActor.RegisterCommand(agentProperties, connection), cancellationToken);
|
|
||||||
await connection.Send(new RegisterAgentSuccessMessage(configureInstanceMessages));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TellAgent(Guid agentGuid, AgentActor.ICommand command) {
|
public bool TellAgent(Guid agentGuid, AgentActor.ICommand command) {
|
||||||
if (agentsByGuid.TryGetValue(agentGuid, out var agent)) {
|
if (agentsByAgentGuid.TryGetValue(agentGuid, out var agent)) {
|
||||||
agent.Tell(command);
|
agent.Tell(command);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -94,7 +82,7 @@ sealed class AgentManager {
|
|||||||
return (UserInstanceActionFailure) UserActionFailure.NotAuthorized;
|
return (UserInstanceActionFailure) UserActionFailure.NotAuthorized;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!agentsByGuid.TryGetValue(agentGuid, out var agent)) {
|
if (!agentsByAgentGuid.TryGetValue(agentGuid, out var agent)) {
|
||||||
return (UserInstanceActionFailure) InstanceActionFailure.AgentDoesNotExist;
|
return (UserInstanceActionFailure) InstanceActionFailure.AgentDoesNotExist;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,9 +1,4 @@
|
|||||||
using Akka.Actor;
|
using Akka.Actor;
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Messages.Agent;
|
|
||||||
using Phantom.Common.Messages.Agent.ToController;
|
|
||||||
using Phantom.Common.Messages.Web;
|
|
||||||
using Phantom.Common.Messages.Web.ToController;
|
|
||||||
using Phantom.Controller.Database;
|
using Phantom.Controller.Database;
|
||||||
using Phantom.Controller.Minecraft;
|
using Phantom.Controller.Minecraft;
|
||||||
using Phantom.Controller.Services.Agents;
|
using Phantom.Controller.Services.Agents;
|
||||||
@@ -13,9 +8,8 @@ using Phantom.Controller.Services.Rpc;
|
|||||||
using Phantom.Controller.Services.Users;
|
using Phantom.Controller.Services.Users;
|
||||||
using Phantom.Controller.Services.Users.Sessions;
|
using Phantom.Controller.Services.Users.Sessions;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
using IRpcAgentRegistrar = Phantom.Utils.Rpc.Runtime.Server.IRpcServerClientRegistrar<Phantom.Common.Messages.Agent.IMessageToController, Phantom.Common.Messages.Agent.IMessageToAgent, Phantom.Common.Data.Agent.AgentInfo>;
|
||||||
using IMessageFromAgentToController = Phantom.Common.Messages.Agent.IMessageToController;
|
using IRpcWebRegistrar = Phantom.Utils.Rpc.Runtime.Server.IRpcServerClientRegistrar<Phantom.Common.Messages.Web.IMessageToController, Phantom.Common.Messages.Web.IMessageToWeb, Phantom.Utils.Rpc.Runtime.Server.RpcServerClientHandshake.NoValue>;
|
||||||
using IMessageFromWebToController = Phantom.Common.Messages.Web.IMessageToController;
|
|
||||||
|
|
||||||
namespace Phantom.Controller.Services;
|
namespace Phantom.Controller.Services;
|
||||||
|
|
||||||
@@ -38,13 +32,14 @@ public sealed class ControllerServices : IDisposable {
|
|||||||
private AuditLogManager AuditLogManager { get; }
|
private AuditLogManager AuditLogManager { get; }
|
||||||
private EventLogManager EventLogManager { get; }
|
private EventLogManager EventLogManager { get; }
|
||||||
|
|
||||||
public IRegistrationHandler<IMessageToAgent, IMessageFromAgentToController, RegisterAgentMessage> AgentRegistrationHandler { get; }
|
public IRpcAgentRegistrar AgentRegistrar { get; }
|
||||||
public IRegistrationHandler<IMessageToWeb, IMessageFromWebToController, RegisterWebMessage> WebRegistrationHandler { get; }
|
public AgentClientHandshake AgentHandshake { get; }
|
||||||
|
public IRpcWebRegistrar WebRegistrar { get; }
|
||||||
|
|
||||||
private readonly IDbContextProvider dbProvider;
|
private readonly IDbContextProvider dbProvider;
|
||||||
private readonly CancellationToken cancellationToken;
|
private readonly CancellationToken cancellationToken;
|
||||||
|
|
||||||
public ControllerServices(IDbContextProvider dbProvider, AuthToken agentAuthToken, AuthToken webAuthToken, CancellationToken shutdownCancellationToken) {
|
public ControllerServices(IDbContextProvider dbProvider, CancellationToken shutdownCancellationToken) {
|
||||||
this.dbProvider = dbProvider;
|
this.dbProvider = dbProvider;
|
||||||
this.cancellationToken = shutdownCancellationToken;
|
this.cancellationToken = shutdownCancellationToken;
|
||||||
|
|
||||||
@@ -60,14 +55,15 @@ public sealed class ControllerServices : IDisposable {
|
|||||||
this.UserLoginManager = new UserLoginManager(AuthenticatedUserCache, UserManager, dbProvider);
|
this.UserLoginManager = new UserLoginManager(AuthenticatedUserCache, UserManager, dbProvider);
|
||||||
this.PermissionManager = new PermissionManager(dbProvider);
|
this.PermissionManager = new PermissionManager(dbProvider);
|
||||||
|
|
||||||
this.AgentManager = new AgentManager(ActorSystem, agentAuthToken, ControllerState, MinecraftVersions, UserLoginManager, dbProvider, cancellationToken);
|
this.AgentManager = new AgentManager(ActorSystem, ControllerState, MinecraftVersions, UserLoginManager, dbProvider, cancellationToken);
|
||||||
this.InstanceLogManager = new InstanceLogManager();
|
this.InstanceLogManager = new InstanceLogManager();
|
||||||
|
|
||||||
this.AuditLogManager = new AuditLogManager(dbProvider);
|
this.AuditLogManager = new AuditLogManager(dbProvider);
|
||||||
this.EventLogManager = new EventLogManager(ControllerState, ActorSystem, dbProvider, shutdownCancellationToken);
|
this.EventLogManager = new EventLogManager(ControllerState, ActorSystem, dbProvider, shutdownCancellationToken);
|
||||||
|
|
||||||
this.AgentRegistrationHandler = new AgentRegistrationHandler(AgentManager, InstanceLogManager, EventLogManager);
|
this.AgentRegistrar = new AgentClientRegistrar(ActorSystem, AgentManager, InstanceLogManager, EventLogManager);
|
||||||
this.WebRegistrationHandler = new WebRegistrationHandler(webAuthToken, ControllerState, InstanceLogManager, UserManager, RoleManager, UserRoleManager, UserLoginManager, AuditLogManager, AgentManager, MinecraftVersions, EventLogManager);
|
this.AgentHandshake = new AgentClientHandshake(AgentManager);
|
||||||
|
this.WebRegistrar = new WebClientRegistrar(ActorSystem, ControllerState, InstanceLogManager, UserManager, RoleManager, UserRoleManager, UserLoginManager, AuditLogManager, AgentManager, MinecraftVersions, EventLogManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Initialize() {
|
public async Task Initialize() {
|
||||||
|
@@ -34,6 +34,6 @@ sealed class ControllerState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateOrDeleteUser(Guid userGuid) {
|
public void UpdateOrDeleteUser(Guid userGuid) {
|
||||||
UserUpdatedOrDeleted?.Invoke(null, userGuid);
|
UserUpdatedOrDeleted?.Invoke(sender: null, userGuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,49 @@
|
|||||||
|
using Phantom.Common.Data.Agent;
|
||||||
|
using Phantom.Common.Messages.Agent.Handshake;
|
||||||
|
using Phantom.Controller.Services.Agents;
|
||||||
|
using Phantom.Utils.Monads;
|
||||||
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
|
using Phantom.Utils.Rpc.Runtime.Server;
|
||||||
|
|
||||||
|
namespace Phantom.Controller.Services.Rpc;
|
||||||
|
|
||||||
|
public sealed class AgentClientHandshake : IRpcServerClientHandshake<AgentInfo> {
|
||||||
|
private const int MaxRegistrationBytes = 1024 * 1024 * 8;
|
||||||
|
|
||||||
|
private readonly AgentManager agentManager;
|
||||||
|
|
||||||
|
internal AgentClientHandshake(AgentManager agentManager) {
|
||||||
|
this.agentManager = agentManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Either<AgentInfo, Exception>> Perform(Stream stream, CancellationToken cancellationToken) {
|
||||||
|
int serializedRegistrationLength = await RpcSerialization.ReadSignedInt(stream, cancellationToken);
|
||||||
|
if (serializedRegistrationLength is < 0 or > MaxRegistrationBytes) {
|
||||||
|
await RpcSerialization.WriteByte(value: 0, stream, cancellationToken);
|
||||||
|
return Either.Right<Exception>(new InvalidOperationException("Registration must be between 0 and " + MaxRegistrationBytes + " bytes."));
|
||||||
|
}
|
||||||
|
|
||||||
|
var serializedRegistration = await RpcSerialization.ReadBytes(serializedRegistrationLength, stream, cancellationToken);
|
||||||
|
|
||||||
|
AgentRegistration registration;
|
||||||
|
try {
|
||||||
|
registration = RpcSerialization.Deserialize<AgentRegistration>(serializedRegistration);
|
||||||
|
} catch (Exception e) {
|
||||||
|
await RpcSerialization.WriteByte(value: 0, stream, cancellationToken);
|
||||||
|
return Either.Right<Exception>(new InvalidOperationException("Caught exception during deserialization.", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
var configureInstanceMessages = await agentManager.RegisterAgent(registration);
|
||||||
|
|
||||||
|
await RpcSerialization.WriteByte(value: 1, stream, cancellationToken);
|
||||||
|
await RpcSerialization.WriteUnsignedInt((uint) configureInstanceMessages.Length, stream, cancellationToken);
|
||||||
|
|
||||||
|
foreach (var configureInstanceMessage in configureInstanceMessages) {
|
||||||
|
ReadOnlyMemory<byte> serializedMessage = RpcSerialization.Serialize(configureInstanceMessage);
|
||||||
|
await RpcSerialization.WriteSignedInt(serializedMessage.Length, stream, cancellationToken);
|
||||||
|
await stream.WriteAsync(serializedMessage, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Either.Left(registration.AgentInfo);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,33 @@
|
|||||||
|
using Akka.Actor;
|
||||||
|
using Phantom.Common.Data.Agent;
|
||||||
|
using Phantom.Common.Messages.Agent;
|
||||||
|
using Phantom.Controller.Services.Agents;
|
||||||
|
using Phantom.Controller.Services.Events;
|
||||||
|
using Phantom.Controller.Services.Instances;
|
||||||
|
using Phantom.Utils.Actor;
|
||||||
|
using Phantom.Utils.Rpc.Message;
|
||||||
|
using Phantom.Utils.Rpc.Runtime.Server;
|
||||||
|
|
||||||
|
namespace Phantom.Controller.Services.Rpc;
|
||||||
|
|
||||||
|
sealed class AgentClientRegistrar(
|
||||||
|
IActorRefFactory actorSystem,
|
||||||
|
AgentManager agentManager,
|
||||||
|
InstanceLogManager instanceLogManager,
|
||||||
|
EventLogManager eventLogManager
|
||||||
|
) : IRpcServerClientRegistrar<IMessageToController, IMessageToAgent, AgentInfo> {
|
||||||
|
public IMessageReceiver<IMessageToController> Register(RpcServerToClientConnection<IMessageToController, IMessageToAgent> connection, AgentInfo handshakeResult) {
|
||||||
|
var agentGuid = handshakeResult.AgentGuid;
|
||||||
|
agentManager.TellAgent(agentGuid, new AgentActor.SetConnectionCommand(connection));
|
||||||
|
|
||||||
|
var name = "AgentClient-" + connection.SessionId;
|
||||||
|
var init = new AgentMessageHandlerActor.Init(agentGuid, connection, agentManager, instanceLogManager, eventLogManager);
|
||||||
|
return new Receiver(agentGuid, agentManager, actorSystem.ActorOf(AgentMessageHandlerActor.Factory(init), name));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Receiver(Guid agentGuid, AgentManager agentManager, ActorRef<IMessageToController> actor) : IMessageReceiver<IMessageToController>.Actor(actor) {
|
||||||
|
public override void OnPing() {
|
||||||
|
agentManager.TellAgent(agentGuid, new AgentActor.NotifyIsAliveCommand());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,26 +1,22 @@
|
|||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Messages.Agent;
|
||||||
using Phantom.Common.Messages.Agent;
|
|
||||||
using Phantom.Common.Messages.Agent.BiDirectional;
|
|
||||||
using Phantom.Common.Messages.Agent.ToAgent;
|
|
||||||
using Phantom.Common.Messages.Agent.ToController;
|
using Phantom.Common.Messages.Agent.ToController;
|
||||||
using Phantom.Controller.Services.Agents;
|
using Phantom.Controller.Services.Agents;
|
||||||
using Phantom.Controller.Services.Events;
|
using Phantom.Controller.Services.Events;
|
||||||
using Phantom.Controller.Services.Instances;
|
using Phantom.Controller.Services.Instances;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
using Phantom.Utils.Rpc.Runtime.Server;
|
||||||
|
|
||||||
namespace Phantom.Controller.Services.Rpc;
|
namespace Phantom.Controller.Services.Rpc;
|
||||||
|
|
||||||
sealed class AgentMessageHandlerActor : ReceiveActor<IMessageToController> {
|
sealed class AgentMessageHandlerActor : ReceiveActor<IMessageToController> {
|
||||||
public readonly record struct Init(Guid AgentGuid, RpcConnectionToClient<IMessageToAgent> Connection, AgentRegistrationHandler AgentRegistrationHandler, AgentManager AgentManager, InstanceLogManager InstanceLogManager, EventLogManager EventLogManager);
|
public readonly record struct Init(Guid AgentGuid, RpcServerToClientConnection<IMessageToController, IMessageToAgent> Connection, AgentManager AgentManager, InstanceLogManager InstanceLogManager, EventLogManager EventLogManager);
|
||||||
|
|
||||||
public static Props<IMessageToController> Factory(Init init) {
|
public static Props<IMessageToController> Factory(Init init) {
|
||||||
return Props<IMessageToController>.Create(() => new AgentMessageHandlerActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume });
|
return Props<IMessageToController>.Create(() => new AgentMessageHandlerActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume });
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly Guid agentGuid;
|
private readonly Guid agentGuid;
|
||||||
private readonly RpcConnectionToClient<IMessageToAgent> connection;
|
private readonly RpcServerToClientConnection<IMessageToController, IMessageToAgent> connection;
|
||||||
private readonly AgentRegistrationHandler agentRegistrationHandler;
|
|
||||||
private readonly AgentManager agentManager;
|
private readonly AgentManager agentManager;
|
||||||
private readonly InstanceLogManager instanceLogManager;
|
private readonly InstanceLogManager instanceLogManager;
|
||||||
private readonly EventLogManager eventLogManager;
|
private readonly EventLogManager eventLogManager;
|
||||||
@@ -28,43 +24,23 @@ sealed class AgentMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
private AgentMessageHandlerActor(Init init) {
|
private AgentMessageHandlerActor(Init init) {
|
||||||
this.agentGuid = init.AgentGuid;
|
this.agentGuid = init.AgentGuid;
|
||||||
this.connection = init.Connection;
|
this.connection = init.Connection;
|
||||||
this.agentRegistrationHandler = init.AgentRegistrationHandler;
|
|
||||||
this.agentManager = init.AgentManager;
|
this.agentManager = init.AgentManager;
|
||||||
this.instanceLogManager = init.InstanceLogManager;
|
this.instanceLogManager = init.InstanceLogManager;
|
||||||
this.eventLogManager = init.EventLogManager;
|
this.eventLogManager = init.EventLogManager;
|
||||||
|
|
||||||
ReceiveAsync<RegisterAgentMessage>(HandleRegisterAgent);
|
ReceiveAsync<UnregisterAgentMessage>(HandleUnregisterAgent);
|
||||||
Receive<UnregisterAgentMessage>(HandleUnregisterAgent);
|
|
||||||
Receive<AgentIsAliveMessage>(HandleAgentIsAlive);
|
|
||||||
Receive<AdvertiseJavaRuntimesMessage>(HandleAdvertiseJavaRuntimes);
|
|
||||||
Receive<ReportAgentStatusMessage>(HandleReportAgentStatus);
|
Receive<ReportAgentStatusMessage>(HandleReportAgentStatus);
|
||||||
Receive<ReportInstanceStatusMessage>(HandleReportInstanceStatus);
|
Receive<ReportInstanceStatusMessage>(HandleReportInstanceStatus);
|
||||||
Receive<ReportInstancePlayerCountsMessage>(HandleReportInstancePlayerCounts);
|
Receive<ReportInstancePlayerCountsMessage>(HandleReportInstancePlayerCounts);
|
||||||
Receive<ReportInstanceEventMessage>(HandleReportInstanceEvent);
|
Receive<ReportInstanceEventMessage>(HandleReportInstanceEvent);
|
||||||
Receive<InstanceOutputMessage>(HandleInstanceOutput);
|
Receive<InstanceOutputMessage>(HandleInstanceOutput);
|
||||||
Receive<ReplyMessage>(HandleReply);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleRegisterAgent(RegisterAgentMessage message) {
|
private Task HandleUnregisterAgent(UnregisterAgentMessage message) {
|
||||||
if (agentGuid != message.AgentInfo.AgentGuid) {
|
|
||||||
await connection.Send(new RegisterAgentFailureMessage(RegisterAgentFailure.ConnectionAlreadyHasAnAgent));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
await agentRegistrationHandler.TryRegisterImpl(connection, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleUnregisterAgent(UnregisterAgentMessage message) {
|
|
||||||
agentManager.TellAgent(agentGuid, new AgentActor.UnregisterCommand(connection));
|
agentManager.TellAgent(agentGuid, new AgentActor.UnregisterCommand(connection));
|
||||||
connection.Close();
|
|
||||||
}
|
Context.Stop(Self);
|
||||||
|
return connection.ClientClosedSession();
|
||||||
private void HandleAgentIsAlive(AgentIsAliveMessage message) {
|
|
||||||
agentManager.TellAgent(agentGuid, new AgentActor.NotifyIsAliveCommand());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleAdvertiseJavaRuntimes(AdvertiseJavaRuntimesMessage message) {
|
|
||||||
agentManager.TellAgent(agentGuid, new AgentActor.UpdateJavaRuntimesCommand(message.Runtimes));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleReportAgentStatus(ReportAgentStatusMessage message) {
|
private void HandleReportAgentStatus(ReportAgentStatusMessage message) {
|
||||||
@@ -86,8 +62,4 @@ sealed class AgentMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
private void HandleInstanceOutput(InstanceOutputMessage message) {
|
private void HandleInstanceOutput(InstanceOutputMessage message) {
|
||||||
instanceLogManager.ReceiveLines(message.InstanceGuid, message.Lines);
|
instanceLogManager.ReceiveLines(message.InstanceGuid, message.Lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleReply(ReplyMessage message) {
|
|
||||||
connection.Receive(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,34 +0,0 @@
|
|||||||
using Phantom.Common.Messages.Agent;
|
|
||||||
using Phantom.Common.Messages.Agent.ToController;
|
|
||||||
using Phantom.Controller.Services.Agents;
|
|
||||||
using Phantom.Controller.Services.Events;
|
|
||||||
using Phantom.Controller.Services.Instances;
|
|
||||||
using Phantom.Utils.Actor;
|
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
|
||||||
|
|
||||||
namespace Phantom.Controller.Services.Rpc;
|
|
||||||
|
|
||||||
sealed class AgentRegistrationHandler : IRegistrationHandler<IMessageToAgent, IMessageToController, RegisterAgentMessage> {
|
|
||||||
private readonly AgentManager agentManager;
|
|
||||||
private readonly InstanceLogManager instanceLogManager;
|
|
||||||
private readonly EventLogManager eventLogManager;
|
|
||||||
|
|
||||||
public AgentRegistrationHandler(AgentManager agentManager, InstanceLogManager instanceLogManager, EventLogManager eventLogManager) {
|
|
||||||
this.agentManager = agentManager;
|
|
||||||
this.instanceLogManager = instanceLogManager;
|
|
||||||
this.eventLogManager = eventLogManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<Props<IMessageToController>?> IRegistrationHandler<IMessageToAgent, IMessageToController, RegisterAgentMessage>.TryRegister(RpcConnectionToClient<IMessageToAgent> connection, RegisterAgentMessage message) {
|
|
||||||
return await TryRegisterImpl(connection, message) ? CreateMessageHandlerActorProps(message.AgentInfo.AgentGuid, connection) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<bool> TryRegisterImpl(RpcConnectionToClient<IMessageToAgent> connection, RegisterAgentMessage message) {
|
|
||||||
return agentManager.RegisterAgent(message.AuthToken, message.AgentInfo, connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Props<IMessageToController> CreateMessageHandlerActorProps(Guid agentGuid, RpcConnectionToClient<IMessageToAgent> connection) {
|
|
||||||
var init = new AgentMessageHandlerActor.Init(agentGuid, connection, this, agentManager, instanceLogManager, eventLogManager);
|
|
||||||
return AgentMessageHandlerActor.Factory(init);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -0,0 +1,33 @@
|
|||||||
|
using Akka.Actor;
|
||||||
|
using Phantom.Common.Messages.Web;
|
||||||
|
using Phantom.Controller.Minecraft;
|
||||||
|
using Phantom.Controller.Services.Agents;
|
||||||
|
using Phantom.Controller.Services.Events;
|
||||||
|
using Phantom.Controller.Services.Instances;
|
||||||
|
using Phantom.Controller.Services.Users;
|
||||||
|
using Phantom.Controller.Services.Users.Sessions;
|
||||||
|
using Phantom.Utils.Actor;
|
||||||
|
using Phantom.Utils.Rpc.Message;
|
||||||
|
using Phantom.Utils.Rpc.Runtime.Server;
|
||||||
|
|
||||||
|
namespace Phantom.Controller.Services.Rpc;
|
||||||
|
|
||||||
|
sealed class WebClientRegistrar(
|
||||||
|
IActorRefFactory actorSystem,
|
||||||
|
ControllerState controllerState,
|
||||||
|
InstanceLogManager instanceLogManager,
|
||||||
|
UserManager userManager,
|
||||||
|
RoleManager roleManager,
|
||||||
|
UserRoleManager userRoleManager,
|
||||||
|
UserLoginManager userLoginManager,
|
||||||
|
AuditLogManager auditLogManager,
|
||||||
|
AgentManager agentManager,
|
||||||
|
MinecraftVersions minecraftVersions,
|
||||||
|
EventLogManager eventLogManager
|
||||||
|
) : IRpcServerClientRegistrar<IMessageToController, IMessageToWeb, RpcServerClientHandshake.NoValue> {
|
||||||
|
public IMessageReceiver<IMessageToController> Register(RpcServerToClientConnection<IMessageToController, IMessageToWeb> connection, RpcServerClientHandshake.NoValue handshakeResult) {
|
||||||
|
var name = "WebClient-" + connection.SessionId;
|
||||||
|
var init = new WebMessageHandlerActor.Init(connection, controllerState, instanceLogManager, userManager, roleManager, userRoleManager, userLoginManager, auditLogManager, agentManager, minecraftVersions, eventLogManager);
|
||||||
|
return new IMessageReceiver<IMessageToController>.Actor(actorSystem.ActorOf(WebMessageHandlerActor.Factory(init), name));
|
||||||
|
}
|
||||||
|
}
|
@@ -5,24 +5,24 @@ using Phantom.Common.Messages.Web;
|
|||||||
using Phantom.Common.Messages.Web.ToWeb;
|
using Phantom.Common.Messages.Web.ToWeb;
|
||||||
using Phantom.Controller.Services.Instances;
|
using Phantom.Controller.Services.Instances;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
using Phantom.Utils.Rpc.Message;
|
||||||
|
|
||||||
namespace Phantom.Controller.Services.Rpc;
|
namespace Phantom.Controller.Services.Rpc;
|
||||||
|
|
||||||
sealed class WebMessageDataUpdateSenderActor : ReceiveActor<WebMessageDataUpdateSenderActor.ICommand> {
|
sealed class WebMessageDataUpdateSenderActor : ReceiveActor<WebMessageDataUpdateSenderActor.ICommand> {
|
||||||
public readonly record struct Init(RpcConnectionToClient<IMessageToWeb> Connection, ControllerState ControllerState, InstanceLogManager InstanceLogManager);
|
public readonly record struct Init(MessageSender<IMessageToWeb> MessageSender, ControllerState ControllerState, InstanceLogManager InstanceLogManager);
|
||||||
|
|
||||||
public static Props<ICommand> Factory(Init init) {
|
public static Props<ICommand> Factory(Init init) {
|
||||||
return Props<ICommand>.Create(() => new WebMessageDataUpdateSenderActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume });
|
return Props<ICommand>.Create(() => new WebMessageDataUpdateSenderActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume });
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly RpcConnectionToClient<IMessageToWeb> connection;
|
private readonly MessageSender<IMessageToWeb> messageSender;
|
||||||
private readonly ControllerState controllerState;
|
private readonly ControllerState controllerState;
|
||||||
private readonly InstanceLogManager instanceLogManager;
|
private readonly InstanceLogManager instanceLogManager;
|
||||||
private readonly ActorRef<ICommand> selfCached;
|
private readonly ActorRef<ICommand> selfCached;
|
||||||
|
|
||||||
private WebMessageDataUpdateSenderActor(Init init) {
|
private WebMessageDataUpdateSenderActor(Init init) {
|
||||||
this.connection = init.Connection;
|
this.messageSender = init.MessageSender;
|
||||||
this.controllerState = init.ControllerState;
|
this.controllerState = init.ControllerState;
|
||||||
this.instanceLogManager = init.InstanceLogManager;
|
this.instanceLogManager = init.InstanceLogManager;
|
||||||
this.selfCached = SelfTyped;
|
this.selfCached = SelfTyped;
|
||||||
@@ -70,18 +70,18 @@ sealed class WebMessageDataUpdateSenderActor : ReceiveActor<WebMessageDataUpdate
|
|||||||
private sealed record RefreshUserSessionCommand(Guid UserGuid) : ICommand;
|
private sealed record RefreshUserSessionCommand(Guid UserGuid) : ICommand;
|
||||||
|
|
||||||
private Task RefreshAgents(RefreshAgentsCommand command) {
|
private Task RefreshAgents(RefreshAgentsCommand command) {
|
||||||
return connection.Send(new RefreshAgentsMessage(command.Agents.Values.ToImmutableArray()));
|
return messageSender.Send(new RefreshAgentsMessage(command.Agents.Values.ToImmutableArray())).AsTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task RefreshInstances(RefreshInstancesCommand command) {
|
private Task RefreshInstances(RefreshInstancesCommand command) {
|
||||||
return connection.Send(new RefreshInstancesMessage(command.Instances.Values.ToImmutableArray()));
|
return messageSender.Send(new RefreshInstancesMessage(command.Instances.Values.ToImmutableArray())).AsTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task ReceiveInstanceLogs(ReceiveInstanceLogsCommand command) {
|
private Task ReceiveInstanceLogs(ReceiveInstanceLogsCommand command) {
|
||||||
return connection.Send(new InstanceOutputMessage(command.InstanceGuid, command.Lines));
|
return messageSender.Send(new InstanceOutputMessage(command.InstanceGuid, command.Lines)).AsTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task RefreshUserSession(RefreshUserSessionCommand command) {
|
private Task RefreshUserSession(RefreshUserSessionCommand command) {
|
||||||
return connection.Send(new RefreshUserSessionMessage(command.UserGuid));
|
return messageSender.Send(new RefreshUserSessionMessage(command.UserGuid)).AsTask();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
using Akka.Actor;
|
||||||
using Phantom.Common.Data;
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Java;
|
using Phantom.Common.Data.Java;
|
||||||
using Phantom.Common.Data.Minecraft;
|
using Phantom.Common.Data.Minecraft;
|
||||||
@@ -7,7 +8,6 @@ using Phantom.Common.Data.Web.AuditLog;
|
|||||||
using Phantom.Common.Data.Web.EventLog;
|
using Phantom.Common.Data.Web.EventLog;
|
||||||
using Phantom.Common.Data.Web.Instance;
|
using Phantom.Common.Data.Web.Instance;
|
||||||
using Phantom.Common.Data.Web.Users;
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Common.Messages.Agent.BiDirectional;
|
|
||||||
using Phantom.Common.Messages.Web;
|
using Phantom.Common.Messages.Web;
|
||||||
using Phantom.Common.Messages.Web.ToController;
|
using Phantom.Common.Messages.Web.ToController;
|
||||||
using Phantom.Controller.Minecraft;
|
using Phantom.Controller.Minecraft;
|
||||||
@@ -17,14 +17,13 @@ using Phantom.Controller.Services.Instances;
|
|||||||
using Phantom.Controller.Services.Users;
|
using Phantom.Controller.Services.Users;
|
||||||
using Phantom.Controller.Services.Users.Sessions;
|
using Phantom.Controller.Services.Users.Sessions;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
using Phantom.Utils.Rpc.Runtime.Server;
|
||||||
|
|
||||||
namespace Phantom.Controller.Services.Rpc;
|
namespace Phantom.Controller.Services.Rpc;
|
||||||
|
|
||||||
sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
|
sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
|
||||||
public readonly record struct Init(
|
public readonly record struct Init(
|
||||||
RpcConnectionToClient<IMessageToWeb> Connection,
|
RpcServerToClientConnection<IMessageToController, IMessageToWeb> Connection,
|
||||||
WebRegistrationHandler WebRegistrationHandler,
|
|
||||||
ControllerState ControllerState,
|
ControllerState ControllerState,
|
||||||
InstanceLogManager InstanceLogManager,
|
InstanceLogManager InstanceLogManager,
|
||||||
UserManager UserManager,
|
UserManager UserManager,
|
||||||
@@ -41,8 +40,7 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
return Props<IMessageToController>.Create(() => new WebMessageHandlerActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume });
|
return Props<IMessageToController>.Create(() => new WebMessageHandlerActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume });
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly RpcConnectionToClient<IMessageToWeb> connection;
|
private readonly RpcServerToClientConnection<IMessageToController, IMessageToWeb> connection;
|
||||||
private readonly WebRegistrationHandler webRegistrationHandler;
|
|
||||||
private readonly ControllerState controllerState;
|
private readonly ControllerState controllerState;
|
||||||
private readonly UserManager userManager;
|
private readonly UserManager userManager;
|
||||||
private readonly RoleManager roleManager;
|
private readonly RoleManager roleManager;
|
||||||
@@ -55,7 +53,6 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
|
|
||||||
private WebMessageHandlerActor(Init init) {
|
private WebMessageHandlerActor(Init init) {
|
||||||
this.connection = init.Connection;
|
this.connection = init.Connection;
|
||||||
this.webRegistrationHandler = init.WebRegistrationHandler;
|
|
||||||
this.controllerState = init.ControllerState;
|
this.controllerState = init.ControllerState;
|
||||||
this.userManager = init.UserManager;
|
this.userManager = init.UserManager;
|
||||||
this.roleManager = init.RoleManager;
|
this.roleManager = init.RoleManager;
|
||||||
@@ -66,11 +63,10 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
this.minecraftVersions = init.MinecraftVersions;
|
this.minecraftVersions = init.MinecraftVersions;
|
||||||
this.eventLogManager = init.EventLogManager;
|
this.eventLogManager = init.EventLogManager;
|
||||||
|
|
||||||
var senderActorInit = new WebMessageDataUpdateSenderActor.Init(connection, controllerState, init.InstanceLogManager);
|
var senderActorInit = new WebMessageDataUpdateSenderActor.Init(connection.MessageSender, controllerState, init.InstanceLogManager);
|
||||||
Context.ActorOf(WebMessageDataUpdateSenderActor.Factory(senderActorInit), "DataUpdateSender");
|
Context.ActorOf(WebMessageDataUpdateSenderActor.Factory(senderActorInit), "DataUpdateSender");
|
||||||
|
|
||||||
ReceiveAsync<RegisterWebMessage>(HandleRegisterWeb);
|
ReceiveAsync<UnregisterWebMessage>(HandleUnregisterWeb);
|
||||||
Receive<UnregisterWebMessage>(HandleUnregisterWeb);
|
|
||||||
ReceiveAndReplyLater<LogInMessage, Optional<LogInSuccess>>(HandleLogIn);
|
ReceiveAndReplyLater<LogInMessage, Optional<LogInSuccess>>(HandleLogIn);
|
||||||
Receive<LogOutMessage>(HandleLogOut);
|
Receive<LogOutMessage>(HandleLogOut);
|
||||||
ReceiveAndReply<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(GetAuthenticatedUser);
|
ReceiveAndReply<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(GetAuthenticatedUser);
|
||||||
@@ -89,15 +85,11 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
ReceiveAndReply<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(HandleGetAgentJavaRuntimes);
|
ReceiveAndReply<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(HandleGetAgentJavaRuntimes);
|
||||||
ReceiveAndReplyLater<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(HandleGetAuditLog);
|
ReceiveAndReplyLater<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(HandleGetAuditLog);
|
||||||
ReceiveAndReplyLater<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(HandleGetEventLog);
|
ReceiveAndReplyLater<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(HandleGetEventLog);
|
||||||
Receive<ReplyMessage>(HandleReply);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleRegisterWeb(RegisterWebMessage message) {
|
private Task HandleUnregisterWeb(UnregisterWebMessage message) {
|
||||||
await webRegistrationHandler.TryRegisterImpl(connection, message);
|
Self.Tell(PoisonPill.Instance);
|
||||||
}
|
return connection.ClientClosedSession();
|
||||||
|
|
||||||
private void HandleUnregisterWeb(UnregisterWebMessage message) {
|
|
||||||
connection.Close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Optional<LogInSuccess>> HandleLogIn(LogInMessage message) {
|
private Task<Optional<LogInSuccess>> HandleLogIn(LogInMessage message) {
|
||||||
@@ -191,8 +183,4 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
private Task<Result<ImmutableArray<EventLogItem>, UserActionFailure>> HandleGetEventLog(GetEventLogMessage message) {
|
private Task<Result<ImmutableArray<EventLogItem>, UserActionFailure>> HandleGetEventLog(GetEventLogMessage message) {
|
||||||
return eventLogManager.GetMostRecentItems(userLoginManager.GetLoggedInUser(message.AuthToken), message.Count);
|
return eventLogManager.GetMostRecentItems(userLoginManager.GetLoggedInUser(message.AuthToken), message.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleReply(ReplyMessage message) {
|
|
||||||
connection.Receive(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,68 +0,0 @@
|
|||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Messages.Web;
|
|
||||||
using Phantom.Common.Messages.Web.ToController;
|
|
||||||
using Phantom.Common.Messages.Web.ToWeb;
|
|
||||||
using Phantom.Controller.Minecraft;
|
|
||||||
using Phantom.Controller.Services.Agents;
|
|
||||||
using Phantom.Controller.Services.Events;
|
|
||||||
using Phantom.Controller.Services.Instances;
|
|
||||||
using Phantom.Controller.Services.Users;
|
|
||||||
using Phantom.Controller.Services.Users.Sessions;
|
|
||||||
using Phantom.Utils.Actor;
|
|
||||||
using Phantom.Utils.Logging;
|
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
namespace Phantom.Controller.Services.Rpc;
|
|
||||||
|
|
||||||
sealed class WebRegistrationHandler : IRegistrationHandler<IMessageToWeb, IMessageToController, RegisterWebMessage> {
|
|
||||||
private static readonly ILogger Logger = PhantomLogger.Create<WebRegistrationHandler>();
|
|
||||||
|
|
||||||
private readonly AuthToken webAuthToken;
|
|
||||||
private readonly ControllerState controllerState;
|
|
||||||
private readonly InstanceLogManager instanceLogManager;
|
|
||||||
private readonly UserManager userManager;
|
|
||||||
private readonly RoleManager roleManager;
|
|
||||||
private readonly UserRoleManager userRoleManager;
|
|
||||||
private readonly UserLoginManager userLoginManager;
|
|
||||||
private readonly AuditLogManager auditLogManager;
|
|
||||||
private readonly AgentManager agentManager;
|
|
||||||
private readonly MinecraftVersions minecraftVersions;
|
|
||||||
private readonly EventLogManager eventLogManager;
|
|
||||||
|
|
||||||
public WebRegistrationHandler(AuthToken webAuthToken, ControllerState controllerState, InstanceLogManager instanceLogManager, UserManager userManager, RoleManager roleManager, UserRoleManager userRoleManager, UserLoginManager userLoginManager, AuditLogManager auditLogManager, AgentManager agentManager, MinecraftVersions minecraftVersions, EventLogManager eventLogManager) {
|
|
||||||
this.webAuthToken = webAuthToken;
|
|
||||||
this.controllerState = controllerState;
|
|
||||||
this.userManager = userManager;
|
|
||||||
this.roleManager = roleManager;
|
|
||||||
this.userRoleManager = userRoleManager;
|
|
||||||
this.userLoginManager = userLoginManager;
|
|
||||||
this.auditLogManager = auditLogManager;
|
|
||||||
this.agentManager = agentManager;
|
|
||||||
this.minecraftVersions = minecraftVersions;
|
|
||||||
this.eventLogManager = eventLogManager;
|
|
||||||
this.instanceLogManager = instanceLogManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<Props<IMessageToController>?> IRegistrationHandler<IMessageToWeb, IMessageToController, RegisterWebMessage>.TryRegister(RpcConnectionToClient<IMessageToWeb> connection, RegisterWebMessage message) {
|
|
||||||
return await TryRegisterImpl(connection, message) ? CreateMessageHandlerActorProps(connection) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> TryRegisterImpl(RpcConnectionToClient<IMessageToWeb> connection, RegisterWebMessage message) {
|
|
||||||
if (webAuthToken.FixedTimeEquals(message.AuthToken)) {
|
|
||||||
Logger.Information("Web authorized successfully.");
|
|
||||||
await connection.Send(new RegisterWebResultMessage(true));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Logger.Warning("Web failed to authorize, invalid token.");
|
|
||||||
await connection.Send(new RegisterWebResultMessage(false));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Props<IMessageToController> CreateMessageHandlerActorProps(RpcConnectionToClient<IMessageToWeb> connection) {
|
|
||||||
var init = new WebMessageHandlerActor.Init(connection, this, controllerState, instanceLogManager, userManager, roleManager, userRoleManager, userLoginManager, auditLogManager, agentManager, minecraftVersions, eventLogManager);
|
|
||||||
return WebMessageHandlerActor.Factory(init);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,6 +1,6 @@
|
|||||||
using NetMQ;
|
using Phantom.Utils.Rpc.Handshake;
|
||||||
using Phantom.Common.Data;
|
using Phantom.Utils.Rpc.Runtime.Tls;
|
||||||
|
|
||||||
namespace Phantom.Controller;
|
namespace Phantom.Controller;
|
||||||
|
|
||||||
readonly record struct ConnectionKeyData(NetMQCertificate Certificate, AuthToken AuthToken);
|
readonly record struct ConnectionKeyData(RpcServerCertificate Certificate, AuthToken AuthToken);
|
||||||
|
@@ -1,39 +1,37 @@
|
|||||||
using NetMQ;
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Utils.Cryptography;
|
using Phantom.Utils.Cryptography;
|
||||||
using Phantom.Utils.IO;
|
using Phantom.Utils.IO;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
|
using Phantom.Utils.Monads;
|
||||||
|
using Phantom.Utils.Rpc.Handshake;
|
||||||
|
using Phantom.Utils.Rpc.Runtime.Tls;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
namespace Phantom.Controller;
|
namespace Phantom.Controller;
|
||||||
|
|
||||||
abstract class ConnectionKeyFiles {
|
abstract class ConnectionKeyFiles {
|
||||||
private const string CommonKeyFileExtension = ".key";
|
|
||||||
private const string SecretKeyFileExtension = ".secret";
|
|
||||||
|
|
||||||
private readonly ILogger logger;
|
private readonly ILogger logger;
|
||||||
private readonly string commonKeyFileName;
|
private readonly string certificateFileName;
|
||||||
private readonly string secretKeyFileName;
|
private readonly string authTokenFileName;
|
||||||
|
|
||||||
private ConnectionKeyFiles(ILogger logger, string name) {
|
private ConnectionKeyFiles(ILogger logger, string name) {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.commonKeyFileName = name + CommonKeyFileExtension;
|
this.certificateFileName = name + ".pfx";
|
||||||
this.secretKeyFileName = name + SecretKeyFileExtension;
|
this.authTokenFileName = name + ".auth";
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ConnectionKeyData?> CreateOrLoad(string folderPath) {
|
public async Task<ConnectionKeyData?> CreateOrLoad(string folderPath) {
|
||||||
string commonKeyFilePath = Path.Combine(folderPath, commonKeyFileName);
|
string certificateFilePath = Path.Combine(folderPath, certificateFileName);
|
||||||
string secretKeyFilePath = Path.Combine(folderPath, secretKeyFileName);
|
string authTokenFilePath = Path.Combine(folderPath, authTokenFileName);
|
||||||
|
|
||||||
bool commonKeyFileExists = File.Exists(commonKeyFilePath);
|
bool certificateFileExists = File.Exists(certificateFilePath);
|
||||||
bool secretKeyFileExists = File.Exists(secretKeyFilePath);
|
bool authTokenFileExists = File.Exists(authTokenFilePath);
|
||||||
|
|
||||||
if (commonKeyFileExists && secretKeyFileExists) {
|
if (certificateFileExists && authTokenFileExists) {
|
||||||
try {
|
try {
|
||||||
return await ReadKeyFiles(commonKeyFilePath, secretKeyFilePath);
|
return await ReadKeyFiles(certificateFilePath, authTokenFilePath);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.Fatal("Error reading connection key files.");
|
logger.Fatal(e, "Error reading connection key files.");
|
||||||
logger.Fatal(e.Message);
|
|
||||||
return null;
|
return null;
|
||||||
} catch (Exception) {
|
} catch (Exception) {
|
||||||
logger.Fatal("Connection key files contain invalid data.");
|
logger.Fatal("Connection key files contain invalid data.");
|
||||||
@@ -41,72 +39,75 @@ abstract class ConnectionKeyFiles {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commonKeyFileExists || secretKeyFileExists) {
|
if (certificateFileExists || authTokenFileExists) {
|
||||||
string existingKeyFilePath = commonKeyFileExists ? commonKeyFilePath : secretKeyFilePath;
|
string existingKeyFilePath = certificateFileExists ? certificateFilePath : authTokenFilePath;
|
||||||
string missingKeyFileName = commonKeyFileExists ? secretKeyFileName : commonKeyFileName;
|
string missingKeyFileName = certificateFileExists ? authTokenFileName : certificateFileName;
|
||||||
logger.Fatal("The connection key file {ExistingKeyFilePath} exists but {MissingKeyFileName} does not. Please delete it to regenerate both files.", existingKeyFilePath, missingKeyFileName);
|
logger.Fatal("Connection key file {ExistingKeyFilePath} exists but {MissingKeyFileName} does not. Please delete it to regenerate both files.", existingKeyFilePath, missingKeyFileName);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Information("Creating connection key files in: {FolderPath}", folderPath);
|
logger.Information("Creating connection key files in: {FolderPath}", folderPath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await GenerateKeyFiles(commonKeyFilePath, secretKeyFilePath);
|
return await GenerateKeyFiles(certificateFilePath, authTokenFilePath);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.Fatal("Error creating connection key files.");
|
logger.Fatal(e, "Error creating connection key files.");
|
||||||
logger.Fatal(e.Message);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ConnectionKeyData?> ReadKeyFiles(string commonKeyFilePath, string secretKeyFilePath) {
|
private async Task<ConnectionKeyData?> ReadKeyFiles(string certificateFilePath, string authTokenFilePath) {
|
||||||
byte[] commonKeyBytes = await ReadKeyFile(commonKeyFilePath);
|
RpcServerCertificate certificate = null!;
|
||||||
byte[] secretKeyBytes = await ReadKeyFile(secretKeyFilePath);
|
|
||||||
|
|
||||||
var (publicKey, authToken) = ConnectionCommonKey.FromBytes(commonKeyBytes);
|
switch (RpcServerCertificate.Load(certificateFilePath)) {
|
||||||
var certificate = new NetMQCertificate(secretKeyBytes, publicKey);
|
case Left<RpcServerCertificate, DisallowedAlgorithmError>(var rpcServerCertificate):
|
||||||
|
certificate = rpcServerCertificate;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Right<RpcServerCertificate, DisallowedAlgorithmError>(var error):
|
||||||
|
logger.Fatal("Certificate {CertificateFilePath} was expected to use {ExpectedAlgorithmName}, instead it uses {ActualAlgorithmName}.", certificateFilePath, error.ExpectedAlgorithmName, error.ActualAlgorithmName);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var authToken = new AuthToken([..await ReadKeyFile(authTokenFilePath)]);
|
||||||
logger.Information("Loaded connection key files.");
|
logger.Information("Loaded connection key files.");
|
||||||
LogCommonKey(commonKeyFilePath, TokenGenerator.EncodeBytes(commonKeyBytes));
|
|
||||||
|
var connectionKey = new ConnectionKey(certificate.Thumbprint, authToken);
|
||||||
|
LogCommonKey(TokenGenerator.EncodeBytes(connectionKey.ToBytes()));
|
||||||
|
|
||||||
return new ConnectionKeyData(certificate, authToken);
|
return new ConnectionKeyData(certificate, authToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Task<byte[]> ReadKeyFile(string filePath) {
|
private static Task<byte[]> ReadKeyFile(string filePath) {
|
||||||
Files.RequireMaximumFileSize(filePath, 64);
|
Files.RequireMaximumFileSize(filePath, maximumBytes: 64);
|
||||||
return File.ReadAllBytesAsync(filePath);
|
return File.ReadAllBytesAsync(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ConnectionKeyData> GenerateKeyFiles(string commonKeyFilePath, string secretKeyFilePath) {
|
private async Task<ConnectionKeyData> GenerateKeyFiles(string certificateFilePath, string authTokenFilePath) {
|
||||||
var certificate = new NetMQCertificate();
|
var certificateBytes = RpcServerCertificate.CreateAndExport("phantom-controller");
|
||||||
var authToken = AuthToken.Generate();
|
var authToken = AuthToken.Generate();
|
||||||
var commonKey = new ConnectionCommonKey(certificate.PublicKey, authToken).ToBytes();
|
|
||||||
|
|
||||||
await Files.WriteBytesAsync(secretKeyFilePath, certificate.SecretKey, FileMode.Create, Chmod.URW_GR);
|
|
||||||
await Files.WriteBytesAsync(commonKeyFilePath, commonKey, FileMode.Create, Chmod.URW_GR);
|
|
||||||
|
|
||||||
|
await Files.WriteBytesAsync(certificateFilePath, certificateBytes, FileMode.Create, Chmod.URW_GR);
|
||||||
|
await Files.WriteBytesAsync(authTokenFilePath, authToken.Bytes.ToArray(), FileMode.Create, Chmod.URW_GR);
|
||||||
logger.Information("Created new connection key files.");
|
logger.Information("Created new connection key files.");
|
||||||
LogCommonKey(commonKeyFilePath, TokenGenerator.EncodeBytes(commonKey));
|
|
||||||
|
var certificate = RpcServerCertificate.Load(certificateFilePath).RequireLeft;
|
||||||
|
var connectionKey = new ConnectionKey(certificate.Thumbprint, authToken);
|
||||||
|
LogCommonKey(TokenGenerator.EncodeBytes(connectionKey.ToBytes()));
|
||||||
|
|
||||||
return new ConnectionKeyData(certificate, authToken);
|
return new ConnectionKeyData(certificate, authToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract void LogCommonKey(string commonKeyFilePath, string commonKeyEncoded);
|
protected abstract void LogCommonKey(string commonKeyEncoded);
|
||||||
|
|
||||||
internal sealed class Agent : ConnectionKeyFiles {
|
internal sealed class Agent() : ConnectionKeyFiles(PhantomLogger.Create<ConnectionKeyFiles, Agent>(), "agent") {
|
||||||
public Agent() : base(PhantomLogger.Create<ConnectionKeyFiles, Agent>(), "agent") {}
|
protected override void LogCommonKey(string commonKeyEncoded) {
|
||||||
|
|
||||||
protected override void LogCommonKey(string commonKeyFilePath, string commonKeyEncoded) {
|
|
||||||
logger.Information("Agent key file: {AgentKeyFilePath}", commonKeyFilePath);
|
|
||||||
logger.Information("Agent key: {AgentKey}", commonKeyEncoded);
|
logger.Information("Agent key: {AgentKey}", commonKeyEncoded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class Web : ConnectionKeyFiles {
|
internal sealed class Web() : ConnectionKeyFiles(PhantomLogger.Create<ConnectionKeyFiles, Web>(), "web") {
|
||||||
public Web() : base(PhantomLogger.Create<ConnectionKeyFiles, Web>(), "web") {}
|
protected override void LogCommonKey(string commonKeyEncoded) {
|
||||||
|
|
||||||
protected override void LogCommonKey(string commonKeyFilePath, string commonKeyEncoded) {
|
|
||||||
logger.Information("Web key file: {WebKeyFilePath}", commonKeyFilePath);
|
|
||||||
logger.Information("Web key: {WebKey}", commonKeyEncoded);
|
logger.Information("Web key: {WebKey}", commonKeyEncoded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using NetMQ;
|
|
||||||
using Phantom.Common.Messages.Agent;
|
using Phantom.Common.Messages.Agent;
|
||||||
using Phantom.Common.Messages.Web;
|
using Phantom.Common.Messages.Web;
|
||||||
using Phantom.Controller;
|
using Phantom.Controller;
|
||||||
@@ -7,9 +6,12 @@ using Phantom.Controller.Database.Postgres;
|
|||||||
using Phantom.Controller.Services;
|
using Phantom.Controller.Services;
|
||||||
using Phantom.Utils.IO;
|
using Phantom.Utils.IO;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
using Phantom.Utils.Rpc;
|
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
|
using Phantom.Utils.Rpc.Runtime.Server;
|
||||||
using Phantom.Utils.Runtime;
|
using Phantom.Utils.Runtime;
|
||||||
|
using Phantom.Utils.Tasks;
|
||||||
|
using RpcAgentServer = Phantom.Utils.Rpc.Runtime.Server.RpcServer<Phantom.Common.Messages.Agent.IMessageToController, Phantom.Common.Messages.Agent.IMessageToAgent, Phantom.Common.Data.Agent.AgentInfo>;
|
||||||
|
using RpcWebServer = Phantom.Utils.Rpc.Runtime.Server.RpcServer<Phantom.Common.Messages.Web.IMessageToController, Phantom.Common.Messages.Web.IMessageToWeb, Phantom.Utils.Rpc.Runtime.Server.RpcServerClientHandshake.NoValue>;
|
||||||
|
|
||||||
var shutdownCancellationTokenSource = new CancellationTokenSource();
|
var shutdownCancellationTokenSource = new CancellationTokenSource();
|
||||||
var shutdownCancellationToken = shutdownCancellationTokenSource.Token;
|
var shutdownCancellationToken = shutdownCancellationTokenSource.Token;
|
||||||
@@ -37,7 +39,7 @@ try {
|
|||||||
PhantomLogger.Root.InformationHeading("Initializing Phantom Panel controller...");
|
PhantomLogger.Root.InformationHeading("Initializing Phantom Panel controller...");
|
||||||
PhantomLogger.Root.Information("Controller version: {Version}", fullVersion);
|
PhantomLogger.Root.Information("Controller version: {Version}", fullVersion);
|
||||||
|
|
||||||
var (agentRpcServerHost, agentRpcServerPort, webRpcServerHost, webRpcServerPort, sqlConnectionString) = Variables.LoadOrStop();
|
var (agentRpcServerHost, webRpcServerHost, sqlConnectionString) = Variables.LoadOrStop();
|
||||||
|
|
||||||
string secretsPath = Path.GetFullPath("./secrets");
|
string secretsPath = Path.GetFullPath("./secrets");
|
||||||
CreateFolderOrStop(secretsPath, Chmod.URWX_GRX);
|
CreateFolderOrStop(secretsPath, Chmod.URWX_GRX);
|
||||||
@@ -56,20 +58,45 @@ try {
|
|||||||
|
|
||||||
var dbContextFactory = new ApplicationDbContextFactory(sqlConnectionString);
|
var dbContextFactory = new ApplicationDbContextFactory(sqlConnectionString);
|
||||||
|
|
||||||
using var controllerServices = new ControllerServices(dbContextFactory, agentKeyData.AuthToken, webKeyData.AuthToken, shutdownCancellationToken);
|
using var controllerServices = new ControllerServices(dbContextFactory, shutdownCancellationToken);
|
||||||
await controllerServices.Initialize();
|
await controllerServices.Initialize();
|
||||||
|
|
||||||
static RpcConfiguration ConfigureRpc(string serviceName, string host, ushort port, ConnectionKeyData connectionKey) {
|
var agentConnectionParameters = new RpcServerConnectionParameters(
|
||||||
return new RpcConfiguration(serviceName, host, port, connectionKey.Certificate);
|
EndPoint: agentRpcServerHost,
|
||||||
}
|
Certificate: agentKeyData.Certificate,
|
||||||
|
AuthToken: agentKeyData.AuthToken,
|
||||||
|
CommonParameters: new RpcCommonConnectionParameters(
|
||||||
|
MessageQueueCapacity: 50,
|
||||||
|
FrameQueueCapacity: 100,
|
||||||
|
MaxConcurrentlyHandledMessages: 20,
|
||||||
|
PingInterval: TimeSpan.FromSeconds(10)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
var webConnectionParameters = new RpcServerConnectionParameters(
|
||||||
await Task.WhenAll(
|
EndPoint: webRpcServerHost,
|
||||||
RpcServerRuntime.Launch(ConfigureRpc("Agent", agentRpcServerHost, agentRpcServerPort, agentKeyData), AgentMessageRegistries.Definitions, controllerServices.AgentRegistrationHandler, controllerServices.ActorSystem, shutdownCancellationToken),
|
Certificate: webKeyData.Certificate,
|
||||||
RpcServerRuntime.Launch(ConfigureRpc("Web", webRpcServerHost, webRpcServerPort, webKeyData), WebMessageRegistries.Definitions, controllerServices.WebRegistrationHandler, controllerServices.ActorSystem, shutdownCancellationToken)
|
AuthToken: webKeyData.AuthToken,
|
||||||
);
|
CommonParameters: new RpcCommonConnectionParameters(
|
||||||
} finally {
|
MessageQueueCapacity: 250,
|
||||||
NetMQConfig.Cleanup();
|
FrameQueueCapacity: 500,
|
||||||
|
MaxConcurrentlyHandledMessages: 100,
|
||||||
|
PingInterval: TimeSpan.FromMinutes(1)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
LinkedTasks<bool> rpcServerTasks = new LinkedTasks<bool>([
|
||||||
|
new RpcAgentServer("Agent", agentConnectionParameters, AgentMessageRegistries.Definitions, controllerServices.AgentHandshake, controllerServices.AgentRegistrar).Run(shutdownCancellationToken),
|
||||||
|
new RpcWebServer("Web", webConnectionParameters, WebMessageRegistries.Definitions, new RpcServerClientHandshake.NoOp(), controllerServices.WebRegistrar).Run(shutdownCancellationToken),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// If either RPC server crashes, stop the whole process.
|
||||||
|
await rpcServerTasks.CancelTokenWhenAnyCompletes(shutdownCancellationTokenSource);
|
||||||
|
|
||||||
|
foreach (Task<bool> rpcServerTask in await rpcServerTasks.WaitForAll()) {
|
||||||
|
if (rpcServerTask.IsFaulted || rpcServerTask is { IsCompletedSuccessfully: true, Result: false }) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
@@ -1,14 +1,13 @@
|
|||||||
using Npgsql;
|
using System.Net;
|
||||||
|
using Npgsql;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
using Phantom.Utils.Runtime;
|
using Phantom.Utils.Runtime;
|
||||||
|
|
||||||
namespace Phantom.Controller;
|
namespace Phantom.Controller;
|
||||||
|
|
||||||
sealed record Variables(
|
sealed record Variables(
|
||||||
string AgentRpcServerHost,
|
EndPoint AgentRpcServerHost,
|
||||||
ushort AgentRpcServerPort,
|
EndPoint WebRpcServerHost,
|
||||||
string WebRpcServerHost,
|
|
||||||
ushort WebRpcServerPort,
|
|
||||||
string SqlConnectionString
|
string SqlConnectionString
|
||||||
) {
|
) {
|
||||||
private static Variables LoadOrThrow() {
|
private static Variables LoadOrThrow() {
|
||||||
@@ -20,11 +19,19 @@ sealed record Variables(
|
|||||||
Database = EnvironmentVariables.GetString("PG_DATABASE").Require
|
Database = EnvironmentVariables.GetString("PG_DATABASE").Require
|
||||||
};
|
};
|
||||||
|
|
||||||
|
EndPoint agentRpcServerHost = new IPEndPoint(
|
||||||
|
EnvironmentVariables.GetIpAddress("AGENT_RPC_SERVER_HOST").WithDefault(IPAddress.Any),
|
||||||
|
EnvironmentVariables.GetPortNumber("AGENT_RPC_SERVER_PORT").WithDefault(9401)
|
||||||
|
);
|
||||||
|
|
||||||
|
EndPoint webRpcServerHost = new IPEndPoint(
|
||||||
|
EnvironmentVariables.GetIpAddress("WEB_RPC_SERVER_HOST").WithDefault(IPAddress.Any),
|
||||||
|
EnvironmentVariables.GetPortNumber("WEB_RPC_SERVER_PORT").WithDefault(9402)
|
||||||
|
);
|
||||||
|
|
||||||
return new Variables(
|
return new Variables(
|
||||||
EnvironmentVariables.GetString("AGENT_RPC_SERVER_HOST").WithDefault("0.0.0.0"),
|
agentRpcServerHost,
|
||||||
EnvironmentVariables.GetPortNumber("AGENT_RPC_SERVER_PORT").WithDefault(9401),
|
webRpcServerHost,
|
||||||
EnvironmentVariables.GetString("WEB_RPC_SERVER_HOST").WithDefault("0.0.0.0"),
|
|
||||||
EnvironmentVariables.GetPortNumber("WEB_RPC_SERVER_PORT").WithDefault(9402),
|
|
||||||
connectionStringBuilder.ToString()
|
connectionStringBuilder.ToString()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -23,7 +23,6 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Update="BCrypt.Net-Next.StrongName" Version="4.0.3" />
|
<PackageReference Update="BCrypt.Net-Next.StrongName" Version="4.0.3" />
|
||||||
<PackageReference Update="MemoryPack" Version="1.10.0" />
|
<PackageReference Update="MemoryPack" Version="1.10.0" />
|
||||||
<PackageReference Update="NetMQ" Version="4.0.1.13" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@@ -18,8 +18,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Agent", "Agent\Phan
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Agent.Minecraft", "Agent\Phantom.Agent.Minecraft\Phantom.Agent.Minecraft.csproj", "{9FE000D0-91AC-4CB4-8956-91CCC0270015}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Agent.Minecraft", "Agent\Phantom.Agent.Minecraft\Phantom.Agent.Minecraft.csproj", "{9FE000D0-91AC-4CB4-8956-91CCC0270015}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Agent.Rpc", "Agent\Phantom.Agent.Rpc\Phantom.Agent.Rpc.csproj", "{665C7B87-0165-48BC-B6A6-17A3812A70C9}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Agent.Services", "Agent\Phantom.Agent.Services\Phantom.Agent.Services.csproj", "{AEE8B77E-AB07-423F-9981-8CD829ACB834}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Agent.Services", "Agent\Phantom.Agent.Services\Phantom.Agent.Services.csproj", "{AEE8B77E-AB07-423F-9981-8CD829ACB834}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Common.Data", "Common\Phantom.Common.Data\Phantom.Common.Data.csproj", "{6C3DB1E5-F695-4D70-8F3A-78C2957274BE}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Common.Data", "Common\Phantom.Common.Data\Phantom.Common.Data.csproj", "{6C3DB1E5-F695-4D70-8F3A-78C2957274BE}"
|
||||||
@@ -76,10 +74,6 @@ Global
|
|||||||
{9FE000D0-91AC-4CB4-8956-91CCC0270015}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{9FE000D0-91AC-4CB4-8956-91CCC0270015}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{9FE000D0-91AC-4CB4-8956-91CCC0270015}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{9FE000D0-91AC-4CB4-8956-91CCC0270015}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{9FE000D0-91AC-4CB4-8956-91CCC0270015}.Release|Any CPU.Build.0 = Release|Any CPU
|
{9FE000D0-91AC-4CB4-8956-91CCC0270015}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{665C7B87-0165-48BC-B6A6-17A3812A70C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{665C7B87-0165-48BC-B6A6-17A3812A70C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{665C7B87-0165-48BC-B6A6-17A3812A70C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{665C7B87-0165-48BC-B6A6-17A3812A70C9}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{AEE8B77E-AB07-423F-9981-8CD829ACB834}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{AEE8B77E-AB07-423F-9981-8CD829ACB834}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{AEE8B77E-AB07-423F-9981-8CD829ACB834}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{AEE8B77E-AB07-423F-9981-8CD829ACB834}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{AEE8B77E-AB07-423F-9981-8CD829ACB834}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{AEE8B77E-AB07-423F-9981-8CD829ACB834}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
@@ -164,7 +158,6 @@ Global
|
|||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{418BE1BF-9F63-4B46-B4E4-DF64C3B3DDA7} = {F5878792-64C8-4ECF-A075-66341FF97127}
|
{418BE1BF-9F63-4B46-B4E4-DF64C3B3DDA7} = {F5878792-64C8-4ECF-A075-66341FF97127}
|
||||||
{9FE000D0-91AC-4CB4-8956-91CCC0270015} = {F5878792-64C8-4ECF-A075-66341FF97127}
|
{9FE000D0-91AC-4CB4-8956-91CCC0270015} = {F5878792-64C8-4ECF-A075-66341FF97127}
|
||||||
{665C7B87-0165-48BC-B6A6-17A3812A70C9} = {F5878792-64C8-4ECF-A075-66341FF97127}
|
|
||||||
{AEE8B77E-AB07-423F-9981-8CD829ACB834} = {F5878792-64C8-4ECF-A075-66341FF97127}
|
{AEE8B77E-AB07-423F-9981-8CD829ACB834} = {F5878792-64C8-4ECF-A075-66341FF97127}
|
||||||
{6C3DB1E5-F695-4D70-8F3A-78C2957274BE} = {01CB1A81-8950-471C-BFDF-F135FDDB2C18}
|
{6C3DB1E5-F695-4D70-8F3A-78C2957274BE} = {01CB1A81-8950-471C-BFDF-F135FDDB2C18}
|
||||||
{95B55357-F8F0-48C2-A1C2-5EA997651783} = {01CB1A81-8950-471C-BFDF-F135FDDB2C18}
|
{95B55357-F8F0-48C2-A1C2-5EA997651783} = {01CB1A81-8950-471C-BFDF-F135FDDB2C18}
|
||||||
|
@@ -28,10 +28,13 @@ This project is **work-in-progress**, and currently has no official releases. Fe
|
|||||||
|
|
||||||
For a quick start, I recommend using [Docker](https://www.docker.com/) or another containerization platform. The `Dockerfile` in the root of the repository can build three target images: `phantom-web`, `phantom-controller`, and `phantom-agent`.
|
For a quick start, I recommend using [Docker](https://www.docker.com/) or another containerization platform. The `Dockerfile` in the root of the repository can build three target images: `phantom-web`, `phantom-controller`, and `phantom-agent`.
|
||||||
|
|
||||||
All images put the built application into the `/app` folder. The Agent image also installs Java 8, 16, 17, and 18.
|
All images put the built application into the `/app` folder. The Agent image also installs Java 8, 16, 17, and 21.
|
||||||
|
|
||||||
Files are stored relative to the working directory. In the provided images, the working directory is set to `/data`.
|
Files are stored relative to the working directory. In the provided images, the working directory is set to `/data`.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> The 3 services communicate with each other using TLS. Due to inconsistent TLS support and implementation quirks between operating systems, Phantom Panel is intended to run only on Linux with up-to-date OpenSSL libraries. Support for other operating systems only exists for the purposes of local development, and components running on different operating systems may not be able to communicate with each other.
|
||||||
|
|
||||||
## Controller
|
## Controller
|
||||||
|
|
||||||
The Controller comprises 3 key areas:
|
The Controller comprises 3 key areas:
|
||||||
|
@@ -31,25 +31,48 @@ public static class PhantomLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static ILogger Create<T>() {
|
public static ILogger Create<T>() {
|
||||||
return Create(typeof(T).Name);
|
return Create(TypeName<T>());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ILogger Create<T>(string name) {
|
public static ILogger Create<T>(string name) {
|
||||||
return Create(typeof(T).Name, name);
|
return Create(ConcatNames(TypeName<T>(), name));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ILogger Create<T>(string name1, string name2) {
|
public static ILogger Create<T>(string name1, string name2) {
|
||||||
return Create(typeof(T).Name, ConcatNames(name1, name2));
|
return Create(ConcatNames(TypeName<T>(), name1, name2));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ILogger Create<T1, T2>() {
|
public static ILogger Create<T1, T2>() {
|
||||||
return Create(typeof(T1).Name, typeof(T2).Name);
|
return Create(ConcatNames(TypeName<T1>(), TypeName<T2>()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ConcatNames(string name1, string name2) {
|
public static ILogger Create<T1, T2>(string name) {
|
||||||
|
return Create(ConcatNames(TypeName<T1>(), TypeName<T2>(), name));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ILogger Create<T1, T2>(string name1, string name2) {
|
||||||
|
return Create(ConcatNames(TypeName<T1>(), TypeName<T2>(), ConcatNames(name1, name2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string TypeName<T>() {
|
||||||
|
string typeName = typeof(T).Name;
|
||||||
|
int genericsStartIndex = typeName.IndexOf('`');
|
||||||
|
return genericsStartIndex > 0 ? typeName[..genericsStartIndex] : typeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ConcatNames(string name1, string name2) {
|
||||||
return name1 + ":" + name2;
|
return name1 + ":" + name2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string ConcatNames(string name1, string name2, string name3) {
|
||||||
|
return ConcatNames(name1, ConcatNames(name2, name3));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ShortenGuid(Guid guid) {
|
||||||
|
var prefix = guid.ToString();
|
||||||
|
return prefix[..prefix.IndexOf('-')];
|
||||||
|
}
|
||||||
|
|
||||||
public static void Dispose() {
|
public static void Dispose() {
|
||||||
Root.Dispose();
|
Root.Dispose();
|
||||||
Base.Dispose();
|
Base.Dispose();
|
||||||
|
67
Utils/Phantom.Utils.Rpc/Frame/IFrame.cs
Normal file
67
Utils/Phantom.Utils.Rpc/Frame/IFrame.cs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
using Phantom.Utils.Rpc.Frame.Types;
|
||||||
|
|
||||||
|
namespace Phantom.Utils.Rpc.Frame;
|
||||||
|
|
||||||
|
interface IFrame {
|
||||||
|
private const byte TypePingId = 0;
|
||||||
|
private const byte TypePongId = 1;
|
||||||
|
private const byte TypeMessageId = 2;
|
||||||
|
private const byte TypeAcknowledgmentId = 3;
|
||||||
|
private const byte TypeReplyId = 4;
|
||||||
|
private const byte TypeErrorId = 5;
|
||||||
|
|
||||||
|
static readonly ReadOnlyMemory<byte> TypePing = new ([TypePingId]);
|
||||||
|
static readonly ReadOnlyMemory<byte> TypePong = new ([TypePongId]);
|
||||||
|
static readonly ReadOnlyMemory<byte> TypeMessage = new ([TypeMessageId]);
|
||||||
|
static readonly ReadOnlyMemory<byte> TypeAcknowledgment = new ([TypeAcknowledgmentId]);
|
||||||
|
static readonly ReadOnlyMemory<byte> TypeReply = new ([TypeReplyId]);
|
||||||
|
static readonly ReadOnlyMemory<byte> TypeError = new ([TypeErrorId]);
|
||||||
|
|
||||||
|
internal static async Task ReadFrom(Stream stream, IFrameReader reader, CancellationToken cancellationToken) {
|
||||||
|
byte[] oneByteBuffer = new byte[1];
|
||||||
|
|
||||||
|
while (!cancellationToken.IsCancellationRequested) {
|
||||||
|
await stream.ReadExactlyAsync(oneByteBuffer, cancellationToken);
|
||||||
|
|
||||||
|
switch (oneByteBuffer[0]) {
|
||||||
|
case TypePingId:
|
||||||
|
var pingTime = await PingFrame.Read(stream, cancellationToken);
|
||||||
|
await reader.OnPingFrame(pingTime, cancellationToken);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TypePongId:
|
||||||
|
var pongFrame = await PongFrame.Read(stream, cancellationToken);
|
||||||
|
reader.OnPongFrame(pongFrame);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TypeMessageId:
|
||||||
|
var messageFrame = await MessageFrame.Read(stream, cancellationToken);
|
||||||
|
await reader.OnMessageFrame(messageFrame, cancellationToken);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TypeAcknowledgmentId:
|
||||||
|
var acknowledgmentFrame = await AcknowledgmentFrame.Read(stream, cancellationToken);
|
||||||
|
reader.OnAcknowledgmentFrame(acknowledgmentFrame);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TypeReplyId:
|
||||||
|
var replyFrame = await ReplyFrame.Read(stream, cancellationToken);
|
||||||
|
reader.OnReplyFrame(replyFrame);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TypeErrorId:
|
||||||
|
var errorFrame = await ErrorFrame.Read(stream, cancellationToken);
|
||||||
|
reader.OnErrorFrame(errorFrame);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
reader.OnUnknownFrameId(oneByteBuffer[0]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ReadOnlyMemory<byte> FrameType { get; }
|
||||||
|
|
||||||
|
Task Write(Stream stream, CancellationToken cancellationToken = default);
|
||||||
|
}
|
13
Utils/Phantom.Utils.Rpc/Frame/IFrameReader.cs
Normal file
13
Utils/Phantom.Utils.Rpc/Frame/IFrameReader.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using Phantom.Utils.Rpc.Frame.Types;
|
||||||
|
|
||||||
|
namespace Phantom.Utils.Rpc.Frame;
|
||||||
|
|
||||||
|
interface IFrameReader {
|
||||||
|
ValueTask OnPingFrame(DateTimeOffset pingTime, CancellationToken cancellationToken);
|
||||||
|
void OnPongFrame(PongFrame frame);
|
||||||
|
Task OnMessageFrame(MessageFrame frame, CancellationToken cancellationToken);
|
||||||
|
void OnAcknowledgmentFrame(AcknowledgmentFrame frame);
|
||||||
|
void OnReplyFrame(ReplyFrame frame);
|
||||||
|
void OnErrorFrame(ErrorFrame frame);
|
||||||
|
void OnUnknownFrameId(byte frameId);
|
||||||
|
}
|
18
Utils/Phantom.Utils.Rpc/Frame/Types/AcknowledgmentFrame.cs
Normal file
18
Utils/Phantom.Utils.Rpc/Frame/Types/AcknowledgmentFrame.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
|
|
||||||
|
namespace Phantom.Utils.Rpc.Frame.Types;
|
||||||
|
|
||||||
|
sealed record AcknowledgmentFrame(uint FirstMessageId, uint LastMessageId) : IFrame {
|
||||||
|
public ReadOnlyMemory<byte> FrameType => IFrame.TypeAcknowledgment;
|
||||||
|
|
||||||
|
public async Task Write(Stream stream, CancellationToken cancellationToken = default) {
|
||||||
|
await RpcSerialization.WriteUnsignedInt(FirstMessageId, stream, cancellationToken);
|
||||||
|
await RpcSerialization.WriteUnsignedInt(LastMessageId, stream, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<AcknowledgmentFrame> Read(Stream stream, CancellationToken cancellationToken) {
|
||||||
|
var firstMessageId = await RpcSerialization.ReadUnsignedInt(stream, cancellationToken);
|
||||||
|
var lastMessageId = await RpcSerialization.ReadUnsignedInt(stream, cancellationToken);
|
||||||
|
return new AcknowledgmentFrame(firstMessageId, lastMessageId);
|
||||||
|
}
|
||||||
|
}
|
18
Utils/Phantom.Utils.Rpc/Frame/Types/ErrorFrame.cs
Normal file
18
Utils/Phantom.Utils.Rpc/Frame/Types/ErrorFrame.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
|
|
||||||
|
namespace Phantom.Utils.Rpc.Frame.Types;
|
||||||
|
|
||||||
|
sealed record ErrorFrame(uint ReplyingToMessageId, RpcError Error) : IFrame {
|
||||||
|
public ReadOnlyMemory<byte> FrameType => IFrame.TypeError;
|
||||||
|
|
||||||
|
public async Task Write(Stream stream, CancellationToken cancellationToken) {
|
||||||
|
await RpcSerialization.WriteUnsignedInt(ReplyingToMessageId, stream, cancellationToken);
|
||||||
|
await RpcSerialization.WriteByte((byte) Error, stream, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<ErrorFrame> Read(Stream stream, CancellationToken cancellationToken) {
|
||||||
|
var replyingToMessageId = await RpcSerialization.ReadUnsignedInt(stream, cancellationToken);
|
||||||
|
var messageError = (RpcError) await RpcSerialization.ReadByte(stream, cancellationToken);
|
||||||
|
return new ErrorFrame(replyingToMessageId, messageError);
|
||||||
|
}
|
||||||
|
}
|
35
Utils/Phantom.Utils.Rpc/Frame/Types/MessageFrame.cs
Normal file
35
Utils/Phantom.Utils.Rpc/Frame/Types/MessageFrame.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
|
|
||||||
|
namespace Phantom.Utils.Rpc.Frame.Types;
|
||||||
|
|
||||||
|
sealed record MessageFrame(uint MessageId, ushort RegistryCode, ReadOnlyMemory<byte> SerializedMessage) : IFrame {
|
||||||
|
public const int MaxMessageBytes = 1024 * 1024 * 8;
|
||||||
|
|
||||||
|
public ReadOnlyMemory<byte> FrameType => IFrame.TypeMessage;
|
||||||
|
|
||||||
|
public async Task Write(Stream stream, CancellationToken cancellationToken) {
|
||||||
|
uint serializedMessageLength = (uint) SerializedMessage.Length;
|
||||||
|
CheckMessageLength(serializedMessageLength);
|
||||||
|
|
||||||
|
await RpcSerialization.WriteUnsignedInt(MessageId, stream, cancellationToken);
|
||||||
|
await RpcSerialization.WriteUnsignedShort(RegistryCode, stream, cancellationToken);
|
||||||
|
await RpcSerialization.WriteUnsignedInt(serializedMessageLength, stream, cancellationToken);
|
||||||
|
await stream.WriteAsync(SerializedMessage, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<MessageFrame> Read(Stream stream, CancellationToken cancellationToken) {
|
||||||
|
var messageId = await RpcSerialization.ReadUnsignedInt(stream, cancellationToken);
|
||||||
|
var registryCode = await RpcSerialization.ReadUnsignedShort(stream, cancellationToken);
|
||||||
|
var serializedMessageLength = await RpcSerialization.ReadUnsignedInt(stream, cancellationToken);
|
||||||
|
CheckMessageLength(serializedMessageLength);
|
||||||
|
var serializedMessage = await RpcSerialization.ReadBytes(serializedMessageLength, stream, cancellationToken);
|
||||||
|
|
||||||
|
return new MessageFrame(messageId, registryCode, serializedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckMessageLength(uint messageLength) {
|
||||||
|
if (messageLength > MaxMessageBytes) {
|
||||||
|
throw new RpcErrorException("Message is too large: " + messageLength + " > " + MaxMessageBytes + " bytes", RpcError.MessageTooLarge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
Utils/Phantom.Utils.Rpc/Frame/Types/PingFrame.cs
Normal file
17
Utils/Phantom.Utils.Rpc/Frame/Types/PingFrame.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
|
|
||||||
|
namespace Phantom.Utils.Rpc.Frame.Types;
|
||||||
|
|
||||||
|
sealed record PingFrame : IFrame {
|
||||||
|
public static PingFrame Instance { get; } = new ();
|
||||||
|
|
||||||
|
public ReadOnlyMemory<byte> FrameType => IFrame.TypePing;
|
||||||
|
|
||||||
|
public async Task Write(Stream stream, CancellationToken cancellationToken) {
|
||||||
|
await RpcSerialization.WriteSignedLong(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), stream, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<DateTimeOffset> Read(Stream stream, CancellationToken cancellationToken) {
|
||||||
|
return DateTimeOffset.FromUnixTimeMilliseconds(await RpcSerialization.ReadSignedLong(stream, cancellationToken));
|
||||||
|
}
|
||||||
|
}
|
15
Utils/Phantom.Utils.Rpc/Frame/Types/PongFrame.cs
Normal file
15
Utils/Phantom.Utils.Rpc/Frame/Types/PongFrame.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
|
|
||||||
|
namespace Phantom.Utils.Rpc.Frame.Types;
|
||||||
|
|
||||||
|
sealed record PongFrame(DateTimeOffset PingTime) : IFrame {
|
||||||
|
public ReadOnlyMemory<byte> FrameType => IFrame.TypePong;
|
||||||
|
|
||||||
|
public async Task Write(Stream stream, CancellationToken cancellationToken) {
|
||||||
|
await RpcSerialization.WriteSignedLong(PingTime.ToUnixTimeMilliseconds(), stream, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<PongFrame> Read(Stream stream, CancellationToken cancellationToken) {
|
||||||
|
return new PongFrame(DateTimeOffset.FromUnixTimeMilliseconds(await RpcSerialization.ReadSignedLong(stream, cancellationToken)));
|
||||||
|
}
|
||||||
|
}
|
33
Utils/Phantom.Utils.Rpc/Frame/Types/ReplyFrame.cs
Normal file
33
Utils/Phantom.Utils.Rpc/Frame/Types/ReplyFrame.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
|
|
||||||
|
namespace Phantom.Utils.Rpc.Frame.Types;
|
||||||
|
|
||||||
|
sealed record ReplyFrame(uint ReplyingToMessageId, ReadOnlyMemory<byte> SerializedReply) : IFrame {
|
||||||
|
public const int MaxReplyBytes = 1024 * 1024 * 32;
|
||||||
|
|
||||||
|
public ReadOnlyMemory<byte> FrameType => IFrame.TypeReply;
|
||||||
|
|
||||||
|
public async Task Write(Stream stream, CancellationToken cancellationToken) {
|
||||||
|
uint serializedReplyLength = (uint) SerializedReply.Length;
|
||||||
|
CheckReplyLength(serializedReplyLength);
|
||||||
|
|
||||||
|
await RpcSerialization.WriteUnsignedInt(ReplyingToMessageId, stream, cancellationToken);
|
||||||
|
await RpcSerialization.WriteUnsignedInt(serializedReplyLength, stream, cancellationToken);
|
||||||
|
await stream.WriteAsync(SerializedReply, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<ReplyFrame> Read(Stream stream, CancellationToken cancellationToken) {
|
||||||
|
var replyingToMessageId = await RpcSerialization.ReadUnsignedInt(stream, cancellationToken);
|
||||||
|
var serializedReplyLength = await RpcSerialization.ReadUnsignedInt(stream, cancellationToken);
|
||||||
|
CheckReplyLength(serializedReplyLength);
|
||||||
|
var serializedReply = await RpcSerialization.ReadBytes(serializedReplyLength, stream, cancellationToken);
|
||||||
|
|
||||||
|
return new ReplyFrame(replyingToMessageId, serializedReply);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckReplyLength(uint replyLength) {
|
||||||
|
if (replyLength > MaxReplyBytes) {
|
||||||
|
throw new RpcErrorException("Reply is too large: " + replyLength + " > " + MaxReplyBytes + " bytes", RpcError.ReplyTooLarge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
Utils/Phantom.Utils.Rpc/Handshake/AuthToken.cs
Normal file
30
Utils/Phantom.Utils.Rpc/Handshake/AuthToken.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace Phantom.Utils.Rpc.Handshake;
|
||||||
|
|
||||||
|
public sealed class AuthToken {
|
||||||
|
public const int Length = 12;
|
||||||
|
|
||||||
|
public ImmutableArray<byte> Bytes { get; }
|
||||||
|
|
||||||
|
public AuthToken(ImmutableArray<byte> bytes) {
|
||||||
|
if (bytes.Length != Length) {
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(bytes), "Invalid token length: " + bytes.Length + ". Token length must be exactly " + Length + " bytes.");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Bytes = bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool FixedTimeEquals(AuthToken providedAuthToken) {
|
||||||
|
return FixedTimeEquals(providedAuthToken.Bytes.AsSpan());
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool FixedTimeEquals(ReadOnlySpan<byte> other) {
|
||||||
|
return CryptographicOperations.FixedTimeEquals(Bytes.AsSpan(), other);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AuthToken Generate() {
|
||||||
|
return new AuthToken([..RandomNumberGenerator.GetBytes(Length)]);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Phantom.Utils.Rpc.Handshake;
|
||||||
|
|
||||||
|
public enum RpcFinalHandshakeResult : byte {
|
||||||
|
Error = 0,
|
||||||
|
NewSession = 1,
|
||||||
|
ReusedSession = 2,
|
||||||
|
}
|
@@ -1,6 +1,6 @@
|
|||||||
namespace Phantom.Utils.Rpc.Message;
|
namespace Phantom.Utils.Rpc.Message;
|
||||||
|
|
||||||
public interface IMessageDefinitions<TClientMessage, TServerMessage, TReplyMessage> : IReplyMessageFactory<TReplyMessage> where TReplyMessage : TClientMessage, TServerMessage {
|
public interface IMessageDefinitions<TClientToServerMessage, TServerToClientMessage> {
|
||||||
MessageRegistry<TClientMessage> ToClient { get; }
|
MessageRegistry<TServerToClientMessage> ToClient { get; }
|
||||||
MessageRegistry<TServerMessage> ToServer { get; }
|
MessageRegistry<TClientToServerMessage> ToServer { get; }
|
||||||
}
|
}
|
||||||
|
21
Utils/Phantom.Utils.Rpc/Message/IMessageReceiver.cs
Normal file
21
Utils/Phantom.Utils.Rpc/Message/IMessageReceiver.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
|
namespace Phantom.Utils.Rpc.Message;
|
||||||
|
|
||||||
|
public interface IMessageReceiver<TMessageBase> {
|
||||||
|
void OnPing();
|
||||||
|
void OnMessage(TMessageBase message);
|
||||||
|
Task<TReply> OnMessage<TMessage, TReply>(TMessage message, CancellationToken cancellationToken = default) where TMessage : TMessageBase, ICanReply<TReply>;
|
||||||
|
|
||||||
|
class Actor(ActorRef<TMessageBase> actor) : IMessageReceiver<TMessageBase> {
|
||||||
|
public virtual void OnPing() {}
|
||||||
|
|
||||||
|
public void OnMessage(TMessageBase message) {
|
||||||
|
actor.Tell(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<TReply> OnMessage<TMessage, TReply>(TMessage message, CancellationToken cancellationToken = default) where TMessage : TMessageBase, ICanReply<TReply> {
|
||||||
|
return actor.Request(message, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
Utils/Phantom.Utils.Rpc/Message/IMessageReplySender.cs
Normal file
9
Utils/Phantom.Utils.Rpc/Message/IMessageReplySender.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
|
|
||||||
|
namespace Phantom.Utils.Rpc.Message;
|
||||||
|
|
||||||
|
interface IMessageReplySender {
|
||||||
|
ValueTask SendEmptyReply(uint replyingToMessageId, CancellationToken cancellationToken);
|
||||||
|
ValueTask SendReply<TReply>(uint replyingToMessageId, TReply reply, CancellationToken cancellationToken);
|
||||||
|
ValueTask SendError(uint replyingToMessageId, RpcError error, CancellationToken cancellationToken);
|
||||||
|
}
|
@@ -1,6 +0,0 @@
|
|||||||
namespace Phantom.Utils.Rpc.Message;
|
|
||||||
|
|
||||||
public interface IReply {
|
|
||||||
uint SequenceId { get; }
|
|
||||||
byte[] SerializedReply { get; }
|
|
||||||
}
|
|
@@ -1,5 +0,0 @@
|
|||||||
namespace Phantom.Utils.Rpc.Message;
|
|
||||||
|
|
||||||
public interface IReplyMessageFactory<TReplyMessage> {
|
|
||||||
TReplyMessage CreateReplyMessage(uint sequenceId, byte[] serializedReply);
|
|
||||||
}
|
|
@@ -1,5 +0,0 @@
|
|||||||
namespace Phantom.Utils.Rpc.Message;
|
|
||||||
|
|
||||||
interface IReplySender {
|
|
||||||
Task SendReply(uint sequenceId, byte[] serializedReply);
|
|
||||||
}
|
|
@@ -0,0 +1,39 @@
|
|||||||
|
using Phantom.Utils.Collections;
|
||||||
|
using Phantom.Utils.Rpc.Frame.Types;
|
||||||
|
using Phantom.Utils.Threading;
|
||||||
|
|
||||||
|
namespace Phantom.Utils.Rpc.Message;
|
||||||
|
|
||||||
|
sealed class MessageAcknowledgmentQueue {
|
||||||
|
private readonly Lock @lock = new ();
|
||||||
|
private readonly RangeSet<uint> pendingMessageIdRanges = new ();
|
||||||
|
private readonly ManualResetEventSlim pendingEvent = new ();
|
||||||
|
|
||||||
|
public void Enqueue(uint messageId) {
|
||||||
|
lock (@lock) {
|
||||||
|
pendingMessageIdRanges.Add(messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingEvent.Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Wait(CancellationToken cancellationToken) {
|
||||||
|
return pendingEvent.WaitHandle.WaitOneAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<AcknowledgmentFrame> Drain() {
|
||||||
|
pendingEvent.Reset();
|
||||||
|
|
||||||
|
List<AcknowledgmentFrame> frames = [];
|
||||||
|
|
||||||
|
lock (@lock) {
|
||||||
|
foreach (var range in pendingMessageIdRanges) {
|
||||||
|
frames.Add(new AcknowledgmentFrame(range.Min, range.Max));
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingMessageIdRanges.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,35 +1,23 @@
|
|||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
using Phantom.Utils.Logging;
|
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
namespace Phantom.Utils.Rpc.Message;
|
namespace Phantom.Utils.Rpc.Message;
|
||||||
|
|
||||||
sealed class MessageHandler<TMessageBase> {
|
sealed class MessageHandler<TMessageBase>(IMessageReceiver<TMessageBase> messageReceiver, IMessageReplySender replySender) {
|
||||||
private readonly ILogger logger;
|
public IMessageReceiver<TMessageBase> Receiver => messageReceiver;
|
||||||
private readonly ActorRef<TMessageBase> handlerActor;
|
|
||||||
private readonly IReplySender replySender;
|
|
||||||
|
|
||||||
public MessageHandler(string loggerName, ActorRef<TMessageBase> handlerActor, IReplySender replySender) {
|
public void OnPing() {
|
||||||
this.logger = PhantomLogger.Create("MessageHandler", loggerName);
|
messageReceiver.OnPing();
|
||||||
this.handlerActor = handlerActor;
|
|
||||||
this.replySender = replySender;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Tell(TMessageBase message) {
|
public ValueTask SendEmptyReply(uint messageId, CancellationToken cancellationToken) {
|
||||||
handlerActor.Tell(message);
|
return replySender.SendEmptyReply(messageId, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task TellAndReply<TMessage, TReply>(TMessage message, uint sequenceId) where TMessage : ICanReply<TReply> {
|
public ValueTask SendReply<TReply>(uint messageId, TReply reply, CancellationToken cancellationToken) {
|
||||||
return handlerActor.Request(message).ContinueWith(task => {
|
return replySender.SendReply(messageId, reply, cancellationToken);
|
||||||
if (task.IsCompletedSuccessfully) {
|
}
|
||||||
return replySender.SendReply(sequenceId, MessageSerializer.Serialize(task.Result));
|
|
||||||
}
|
public ValueTask SendError(uint messageId, RpcError error, CancellationToken cancellationToken) {
|
||||||
|
return replySender.SendError(messageId, error, cancellationToken);
|
||||||
if (task.IsFaulted) {
|
|
||||||
logger.Error(task.Exception, "Failed to handle message {Type}.", message.GetType().Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return task;
|
|
||||||
}, TaskScheduler.Default);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
13
Utils/Phantom.Utils.Rpc/Message/MessageReceiveTracker.cs
Normal file
13
Utils/Phantom.Utils.Rpc/Message/MessageReceiveTracker.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using Phantom.Utils.Collections;
|
||||||
|
|
||||||
|
namespace Phantom.Utils.Rpc.Message;
|
||||||
|
|
||||||
|
sealed class MessageReceiveTracker {
|
||||||
|
private readonly RangeSet<uint> receivedMessageIds = new ();
|
||||||
|
|
||||||
|
public bool ReceiveMessage(uint messageId) {
|
||||||
|
lock (receivedMessageIds) {
|
||||||
|
return receivedMessageIds.Add(messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,26 +1,19 @@
|
|||||||
using System.Buffers;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
using Phantom.Utils.Rpc.Frame.Types;
|
||||||
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Serilog.Events;
|
|
||||||
|
|
||||||
namespace Phantom.Utils.Rpc.Message;
|
namespace Phantom.Utils.Rpc.Message;
|
||||||
|
|
||||||
public sealed class MessageRegistry<TMessageBase> {
|
public sealed class MessageRegistry<TMessageBase>(ILogger logger) {
|
||||||
private const int DefaultBufferSize = 512;
|
|
||||||
|
|
||||||
private readonly ILogger logger;
|
|
||||||
private readonly Dictionary<Type, ushort> typeToCodeMapping = new ();
|
private readonly Dictionary<Type, ushort> typeToCodeMapping = new ();
|
||||||
private readonly Dictionary<ushort, Type> codeToTypeMapping = new ();
|
private readonly Dictionary<ushort, Type> codeToTypeMapping = new ();
|
||||||
private readonly Dictionary<ushort, Action<ReadOnlyMemory<byte>, ushort, MessageHandler<TMessageBase>>> codeToHandlerMapping = new ();
|
private readonly Dictionary<ushort, Func<uint, ReadOnlyMemory<byte>, MessageHandler<TMessageBase>, CancellationToken, Task>> codeToHandlerMapping = new ();
|
||||||
|
|
||||||
public MessageRegistry(ILogger logger) {
|
|
||||||
this.logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add<TMessage>(ushort code) where TMessage : TMessageBase {
|
public void Add<TMessage>(ushort code) where TMessage : TMessageBase {
|
||||||
if (HasReplyType(typeof(TMessage))) {
|
if (HasReplyType(typeof(TMessage))) {
|
||||||
throw new ArgumentException("This overload is for messages without a reply");
|
throw new ArgumentException("This overload is for messages without a reply.");
|
||||||
}
|
}
|
||||||
|
|
||||||
AddTypeCodeMapping<TMessage>(code);
|
AddTypeCodeMapping<TMessage>(code);
|
||||||
@@ -44,140 +37,90 @@ public sealed class MessageRegistry<TMessageBase> {
|
|||||||
return messageType.GetInterfaces().Any(type => type.FullName is {} name && name.StartsWith(replyInterfaceName, StringComparison.Ordinal));
|
return messageType.GetInterfaces().Any(type => type.FullName is {} name && name.StartsWith(replyInterfaceName, StringComparison.Ordinal));
|
||||||
}
|
}
|
||||||
|
|
||||||
internal bool TryGetType(ReadOnlyMemory<byte> data, [NotNullWhen(true)] out Type? type) {
|
internal bool TryGetType(MessageFrame frame, [NotNullWhen(true)] out Type? type) {
|
||||||
try {
|
return codeToTypeMapping.TryGetValue(frame.RegistryCode, out type);
|
||||||
var code = MessageSerializer.ReadCode(ref data);
|
|
||||||
return codeToTypeMapping.TryGetValue(code, out type);
|
|
||||||
} catch (Exception) {
|
|
||||||
type = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ReadOnlySpan<byte> Write<TMessage>(TMessage message) where TMessage : TMessageBase {
|
internal MessageFrame CreateFrame<TMessage>(uint messageId, TMessage message) where TMessage : TMessageBase {
|
||||||
if (!GetMessageCode<TMessage>(out var code)) {
|
if (typeToCodeMapping.TryGetValue(typeof(TMessage), out ushort code)) {
|
||||||
return default;
|
return new MessageFrame(messageId, code, RpcSerialization.Serialize(message));
|
||||||
}
|
|
||||||
|
|
||||||
var buffer = new ArrayBufferWriter<byte>(DefaultBufferSize);
|
|
||||||
|
|
||||||
try {
|
|
||||||
MessageSerializer.WriteCode(buffer, code);
|
|
||||||
MessageSerializer.Serialize(buffer, message);
|
|
||||||
|
|
||||||
CheckWrittenBufferLength<TMessage>(buffer);
|
|
||||||
return buffer.WrittenSpan;
|
|
||||||
} catch (Exception e) {
|
|
||||||
LogWriteFailure<TMessage>(e);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReadOnlySpan<byte> Write<TMessage, TReply>(uint sequenceId, TMessage message) where TMessage : TMessageBase, ICanReply<TReply> {
|
|
||||||
if (!GetMessageCode<TMessage>(out var code)) {
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
var buffer = new ArrayBufferWriter<byte>(DefaultBufferSize);
|
|
||||||
|
|
||||||
try {
|
|
||||||
MessageSerializer.WriteCode(buffer, code);
|
|
||||||
MessageSerializer.WriteSequenceId(buffer, sequenceId);
|
|
||||||
MessageSerializer.Serialize(buffer, message);
|
|
||||||
|
|
||||||
CheckWrittenBufferLength<TMessage>(buffer);
|
|
||||||
return buffer.WrittenSpan;
|
|
||||||
} catch (Exception e) {
|
|
||||||
LogWriteFailure<TMessage>(e);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool GetMessageCode<TMessage>(out ushort code) where TMessage : TMessageBase {
|
|
||||||
if (typeToCodeMapping.TryGetValue(typeof(TMessage), out code)) {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
logger.Error("Unknown message type {Type}.", typeof(TMessage));
|
throw new ArgumentException("Unknown message type: " + typeof(TMessage));
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CheckWrittenBufferLength<TMessage>(ArrayBufferWriter<byte> buffer) where TMessage : TMessageBase {
|
internal async Task Handle(MessageFrame frame, MessageHandler<TMessageBase> handler, CancellationToken cancellationToken) {
|
||||||
if (buffer.WrittenCount > DefaultBufferSize && logger.IsEnabled(LogEventLevel.Verbose)) {
|
uint messageId = frame.MessageId;
|
||||||
logger.Verbose("Serializing {Type} exceeded default buffer size: {WrittenSize} B > {DefaultBufferSize} B", typeof(TMessage).Name, buffer.WrittenCount, DefaultBufferSize);
|
|
||||||
}
|
if (codeToHandlerMapping.TryGetValue(frame.RegistryCode, out var action)) {
|
||||||
}
|
await action(messageId, frame.SerializedMessage, handler, cancellationToken);
|
||||||
|
|
||||||
private void LogWriteFailure<TMessage>(Exception e) where TMessage : TMessageBase {
|
|
||||||
logger.Error(e, "Failed to serialize message {Type}.", typeof(TMessage).Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal bool Read<TMessage>(ReadOnlyMemory<byte> data, out TMessage message) where TMessage : TMessageBase {
|
|
||||||
if (ReadTypeCode(ref data, out ushort code) && codeToTypeMapping.TryGetValue(code, out var expectedType) && expectedType == typeof(TMessage) && ReadMessage(data, out message)) {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
message = default!;
|
logger.Error("Unknown message code {Code} for message {MessageId}.", frame.RegistryCode, messageId);
|
||||||
return false;
|
await handler.SendError(messageId, RpcError.UnknownMessageRegistryCode, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void Handle(ReadOnlyMemory<byte> data, MessageHandler<TMessageBase> handler) {
|
private async Task DeserializationHandler<TMessage>(uint messageId, ReadOnlyMemory<byte> serializedMessage, MessageHandler<TMessageBase> handler, CancellationToken cancellationToken) where TMessage : TMessageBase {
|
||||||
if (!ReadTypeCode(ref data, out var code)) {
|
TMessage message;
|
||||||
|
try {
|
||||||
|
message = RpcSerialization.Deserialize<TMessage>(serializedMessage);
|
||||||
|
} catch (Exception e) {
|
||||||
|
await OnMessageDeserializationError<TMessage>(messageId, e, handler, cancellationToken);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!codeToHandlerMapping.TryGetValue(code, out var handle)) {
|
try {
|
||||||
logger.Error("Unknown message code {Code}.", code);
|
handler.Receiver.OnMessage(message);
|
||||||
|
} catch (Exception e) {
|
||||||
|
await OnMessageHandlingError<TMessage>(messageId, e, handler, cancellationToken);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handle(data, code, handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ReadTypeCode(ref ReadOnlyMemory<byte> data, out ushort code) {
|
|
||||||
try {
|
try {
|
||||||
code = MessageSerializer.ReadCode(ref data);
|
await handler.SendEmptyReply(messageId, cancellationToken);
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
code = default;
|
await OnMessageReplyingError<TMessage>(messageId, e, handler, cancellationToken);
|
||||||
logger.Error(e, "Failed to deserialize message code.");
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ReadSequenceId<TMessage, TReply>(ref ReadOnlyMemory<byte> data, out uint sequenceId) where TMessage : TMessageBase, ICanReply<TReply> {
|
private async Task DeserializationHandler<TMessage, TReply>(uint messageId, ReadOnlyMemory<byte> serializedMessage, MessageHandler<TMessageBase> handler, CancellationToken cancellationToken) where TMessage : TMessageBase, ICanReply<TReply> {
|
||||||
|
TMessage message;
|
||||||
try {
|
try {
|
||||||
sequenceId = MessageSerializer.ReadSequenceId(ref data);
|
message = RpcSerialization.Deserialize<TMessage>(serializedMessage);
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
sequenceId = default;
|
await OnMessageDeserializationError<TMessage>(messageId, e, handler, cancellationToken);
|
||||||
logger.Error(e, "Failed to deserialize sequence ID of message {Type}.", typeof(TMessage).Name);
|
return;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
TReply reply;
|
||||||
private bool ReadMessage<TMessage>(ReadOnlyMemory<byte> data, out TMessage message) where TMessage : TMessageBase {
|
|
||||||
try {
|
try {
|
||||||
message = MessageSerializer.Deserialize<TMessage>(data);
|
reply = await handler.Receiver.OnMessage<TMessage, TReply>(message, cancellationToken);
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
message = default!;
|
await OnMessageHandlingError<TMessage>(messageId, e, handler, cancellationToken);
|
||||||
logger.Error(e, "Failed to deserialize message {Type}.", typeof(TMessage).Name);
|
return;
|
||||||
return false;
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await handler.SendReply(messageId, reply, cancellationToken);
|
||||||
|
} catch (Exception e) {
|
||||||
|
await OnMessageReplyingError<TMessage>(messageId, e, handler, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DeserializationHandler<TMessage>(ReadOnlyMemory<byte> data, ushort code, MessageHandler<TMessageBase> handler) where TMessage : TMessageBase {
|
private async Task OnMessageDeserializationError<TMessage>(uint messageId, Exception exception, MessageHandler<TMessageBase> handler, CancellationToken cancellationToken) where TMessage : TMessageBase {
|
||||||
if (ReadMessage<TMessage>(data, out var message)) {
|
logger.Error(exception, "Could not deserialize message {MessageId} ({MessageType}).", messageId, typeof(TMessage).Name);
|
||||||
handler.Tell(message);
|
await handler.SendError(messageId, RpcError.MessageDeserializationError, cancellationToken);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DeserializationHandler<TMessage, TReply>(ReadOnlyMemory<byte> data, ushort code, MessageHandler<TMessageBase> handler) where TMessage : TMessageBase, ICanReply<TReply> {
|
private async Task OnMessageHandlingError<TMessage>(uint messageId, Exception exception, MessageHandler<TMessageBase> handler, CancellationToken cancellationToken) where TMessage : TMessageBase {
|
||||||
if (ReadSequenceId<TMessage, TReply>(ref data, out var sequenceId) && ReadMessage<TMessage>(data, out var message)) {
|
logger.Error(exception, "Could not handle message {MessageId} ({MessageType}).", messageId, typeof(TMessage).Name);
|
||||||
handler.TellAndReply<TMessage, TReply>(message, sequenceId);
|
await handler.SendError(messageId, RpcError.MessageHandlingError, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task OnMessageReplyingError<TMessage>(uint messageId, Exception exception, MessageHandler<TMessageBase> handler, CancellationToken cancellationToken) where TMessage : TMessageBase {
|
||||||
|
logger.Error(exception, "Could not reply to message {MessageId} ({MessageType}).", messageId, typeof(TMessage).Name);
|
||||||
|
await handler.SendError(messageId, RpcError.MessageReplyingError, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
using Phantom.Utils.Tasks;
|
using Phantom.Utils.Tasks;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
@@ -7,55 +8,54 @@ namespace Phantom.Utils.Rpc.Message;
|
|||||||
|
|
||||||
sealed class MessageReplyTracker {
|
sealed class MessageReplyTracker {
|
||||||
private readonly ILogger logger;
|
private readonly ILogger logger;
|
||||||
private readonly ConcurrentDictionary<uint, TaskCompletionSource<byte[]>> replyTasks = new (4, 16);
|
private readonly ConcurrentDictionary<uint, TaskCompletionSource<ReadOnlyMemory<byte>>> replyTasks = new (concurrencyLevel: 2, capacity: 16);
|
||||||
|
|
||||||
private uint lastSequenceId;
|
|
||||||
|
|
||||||
internal MessageReplyTracker(string loggerName) {
|
internal MessageReplyTracker(string loggerName) {
|
||||||
this.logger = PhantomLogger.Create<MessageReplyTracker>(loggerName);
|
this.logger = PhantomLogger.Create<MessageReplyTracker>(loggerName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public uint RegisterReply() {
|
public void RegisterReply(uint messageId) {
|
||||||
var sequenceId = Interlocked.Increment(ref lastSequenceId);
|
replyTasks[messageId] = AsyncTasks.CreateCompletionSource<ReadOnlyMemory<byte>>();
|
||||||
replyTasks[sequenceId] = AsyncTasks.CreateCompletionSource<byte[]>();
|
|
||||||
return sequenceId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TReply> WaitForReply<TReply>(uint sequenceId, TimeSpan waitForReplyTime, CancellationToken cancellationToken) {
|
public async Task<TReply> WaitForReply<TReply>(uint messageId, TimeSpan waitForReplyTime, CancellationToken cancellationToken) {
|
||||||
if (!replyTasks.TryGetValue(sequenceId, out var completionSource)) {
|
if (!replyTasks.TryGetValue(messageId, out var completionSource)) {
|
||||||
logger.Warning("No reply callback for id {SequenceId}.", sequenceId);
|
logger.Warning("No reply callback for message {MessageId}.", messageId);
|
||||||
throw new ArgumentException("No reply callback for id: " + sequenceId, nameof(sequenceId));
|
throw new ArgumentException("No reply callback for message: " + messageId, nameof(messageId));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
byte[] replyBytes = await completionSource.Task.WaitAsync(waitForReplyTime, cancellationToken);
|
ReadOnlyMemory<byte> serializedReply = await completionSource.Task.WaitAsync(waitForReplyTime, cancellationToken);
|
||||||
return MessageSerializer.Deserialize<TReply>(replyBytes);
|
return RpcSerialization.Deserialize<TReply>(serializedReply);
|
||||||
} catch (TimeoutException) {
|
} catch (TimeoutException) {
|
||||||
logger.Debug("Timed out waiting for reply with id {SequenceId}.", sequenceId);
|
logger.Debug("Timed out waiting for reply with message {MessageId}.", messageId);
|
||||||
throw;
|
throw;
|
||||||
} catch (OperationCanceledException) {
|
} catch (OperationCanceledException) {
|
||||||
logger.Debug("Cancelled waiting for reply with id {SequenceId}.", sequenceId);
|
logger.Debug("Cancelled waiting for reply with message {MessageId}.", messageId);
|
||||||
throw;
|
throw;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.Warning(e, "Error processing reply with id {SequenceId}.", sequenceId);
|
logger.Warning(e, "Error processing reply with message {MessageId}.", messageId);
|
||||||
throw;
|
throw;
|
||||||
} finally {
|
} finally {
|
||||||
ForgetReply(sequenceId);
|
ForgetReply(messageId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ForgetReply(uint sequenceId) {
|
public void ReceiveReply(uint messageId, ReadOnlyMemory<byte> serializedReply) {
|
||||||
if (replyTasks.TryRemove(sequenceId, out var task)) {
|
if (replyTasks.TryRemove(messageId, out var task)) {
|
||||||
|
task.SetResult(serializedReply);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void FailReply(uint messageId, RpcErrorException e) {
|
||||||
|
if (replyTasks.TryRemove(messageId, out var task)) {
|
||||||
|
task.SetException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ForgetReply(uint messageId) {
|
||||||
|
if (replyTasks.TryRemove(messageId, out var task)) {
|
||||||
task.SetCanceled();
|
task.SetCanceled();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ReceiveReply(uint sequenceId, byte[] serializedReply) {
|
|
||||||
if (replyTasks.TryRemove(sequenceId, out var task)) {
|
|
||||||
task.SetResult(serializedReply);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
logger.Warning("Received a reply with id {SequenceId} but no registered callback.", sequenceId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
117
Utils/Phantom.Utils.Rpc/Message/MessageSender.cs
Normal file
117
Utils/Phantom.Utils.Rpc/Message/MessageSender.cs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
using System.Threading.Channels;
|
||||||
|
using Phantom.Utils.Actor;
|
||||||
|
using Phantom.Utils.Logging;
|
||||||
|
using Phantom.Utils.Rpc.Frame.Types;
|
||||||
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace Phantom.Utils.Rpc.Message;
|
||||||
|
|
||||||
|
public sealed class MessageSender<TMessageBase> {
|
||||||
|
private readonly ILogger logger;
|
||||||
|
private readonly MessageRegistry<TMessageBase> messageRegistry;
|
||||||
|
private readonly MessageReplyTracker messageReplyTracker;
|
||||||
|
private uint nextMessageId;
|
||||||
|
|
||||||
|
private readonly Channel<PreparedMessage> messageQueue;
|
||||||
|
private readonly Task messageQueueTask;
|
||||||
|
|
||||||
|
private readonly CancellationTokenSource shutdownCancellationTokenSource = new ();
|
||||||
|
|
||||||
|
internal MessageSender(string loggerName, MessageRegistry<TMessageBase> messageRegistry, RpcCommonConnectionParameters connectionParameters) {
|
||||||
|
this.logger = PhantomLogger.Create<MessageSender<TMessageBase>>(loggerName);
|
||||||
|
this.messageRegistry = messageRegistry;
|
||||||
|
this.messageReplyTracker = new MessageReplyTracker(loggerName);
|
||||||
|
|
||||||
|
this.messageQueue = Channel.CreateBounded<PreparedMessage>(new BoundedChannelOptions(connectionParameters.MessageQueueCapacity) {
|
||||||
|
AllowSynchronousContinuations = false,
|
||||||
|
FullMode = BoundedChannelFullMode.Wait,
|
||||||
|
SingleReader = true,
|
||||||
|
SingleWriter = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.messageQueueTask = ProcessQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TrySend<TMessage>(TMessage message) where TMessage : TMessageBase {
|
||||||
|
return messageQueue.Writer.TryWrite(PrepareMessage(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask Send<TMessage>(TMessage message, CancellationToken cancellationToken = default) where TMessage : TMessageBase {
|
||||||
|
await messageQueue.Writer.WriteAsync(PrepareMessage(message), cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TReply> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken cancellationToken) where TMessage : TMessageBase, ICanReply<TReply> {
|
||||||
|
var preparedMessage = PrepareMessage(message);
|
||||||
|
var messageId = preparedMessage.MessageId;
|
||||||
|
|
||||||
|
messageReplyTracker.RegisterReply(messageId);
|
||||||
|
try {
|
||||||
|
await messageQueue.Writer.WriteAsync(preparedMessage, cancellationToken);
|
||||||
|
} catch (Exception) {
|
||||||
|
messageReplyTracker.ForgetReply(messageId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await messageReplyTracker.WaitForReply<TReply>(messageId, waitForReplyTime, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PreparedMessage PrepareMessage(TMessageBase message) {
|
||||||
|
uint messageId = Interlocked.Increment(ref nextMessageId);
|
||||||
|
return new PreparedMessage(messageId, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct PreparedMessage(uint MessageId, TMessageBase Message);
|
||||||
|
|
||||||
|
private async Task ProcessQueue() {
|
||||||
|
CancellationToken cancellationToken = shutdownCancellationTokenSource.Token;
|
||||||
|
Queue<PreparedMessage> messagesInTransit = new (capacity: 10);
|
||||||
|
|
||||||
|
while (await messageQueue.Reader.WaitToReadAsync(cancellationToken)) {
|
||||||
|
do {
|
||||||
|
while (messagesInTransit.Count < messagesInTransit.Capacity) {
|
||||||
|
if (messageQueue.Reader.TryRead(out var nextMessage)) {
|
||||||
|
messagesInTransit.Enqueue(nextMessage);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RpcFrameSender<TMessageBase> frameSender;
|
||||||
|
|
||||||
|
foreach ((uint messageId, TMessageBase message) in messagesInTransit) {
|
||||||
|
await frameSender.SendFrame(messageRegistry.CreateFrame(messageId, message), cancellationToken);
|
||||||
|
}
|
||||||
|
} while (messagesInTransit.Count > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void ReceiveAcknowledgment(AcknowledgmentFrame frame) {
|
||||||
|
// TODO wait for reply instead?
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void ReceiveReply(ReplyFrame frame) {
|
||||||
|
messageReplyTracker.ReceiveReply(frame.ReplyingToMessageId, frame.SerializedReply);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void ReceiveError(ErrorFrame frame) {
|
||||||
|
messageReplyTracker.FailReply(frame.ReplyingToMessageId, RpcErrorException.From(frame.Error));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task Close() {
|
||||||
|
messageQueue.Writer.TryComplete();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await messageQueueTask.WaitAsync(TimeSpan.FromSeconds(15));
|
||||||
|
} catch (TimeoutException) {
|
||||||
|
logger.Warning("Could not finish processing message queue before timeout, forcibly shutting it down.");
|
||||||
|
await shutdownCancellationTokenSource.CancelAsync();
|
||||||
|
} catch (Exception) {
|
||||||
|
// Ignore.
|
||||||
|
}
|
||||||
|
|
||||||
|
messageQueueTask.Dispose();
|
||||||
|
shutdownCancellationTokenSource.Dispose();
|
||||||
|
}
|
||||||
|
}
|
@@ -1,45 +0,0 @@
|
|||||||
using System.Buffers;
|
|
||||||
using System.Buffers.Binary;
|
|
||||||
using MemoryPack;
|
|
||||||
|
|
||||||
namespace Phantom.Utils.Rpc.Message;
|
|
||||||
|
|
||||||
static class MessageSerializer {
|
|
||||||
private static readonly MemoryPackSerializerOptions SerializerOptions = MemoryPackSerializerOptions.Utf8;
|
|
||||||
|
|
||||||
public static byte[] Serialize<T>(T message) {
|
|
||||||
return MemoryPackSerializer.Serialize(message, SerializerOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Serialize<T>(IBufferWriter<byte> destination, T message) {
|
|
||||||
MemoryPackSerializer.Serialize(typeof(T), destination, message, SerializerOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static T Deserialize<T>(ReadOnlyMemory<byte> memory) {
|
|
||||||
return MemoryPackSerializer.Deserialize<T>(memory.Span) ?? throw new NullReferenceException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void WriteCode(IBufferWriter<byte> destination, ushort value) {
|
|
||||||
Span<byte> buffer = stackalloc byte[2];
|
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer, value);
|
|
||||||
destination.Write(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ushort ReadCode(ref ReadOnlyMemory<byte> memory) {
|
|
||||||
ushort value = BinaryPrimitives.ReadUInt16LittleEndian(memory.Span);
|
|
||||||
memory = memory[2..];
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void WriteSequenceId(IBufferWriter<byte> destination, uint sequenceId) {
|
|
||||||
Span<byte> buffer = stackalloc byte[4];
|
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, sequenceId);
|
|
||||||
destination.Write(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static uint ReadSequenceId(ref ReadOnlyMemory<byte> memory) {
|
|
||||||
uint value = BinaryPrimitives.ReadUInt32LittleEndian(memory.Span);
|
|
||||||
memory = memory[4..];
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,17 +0,0 @@
|
|||||||
using Phantom.Utils.Rpc.Runtime;
|
|
||||||
|
|
||||||
namespace Phantom.Utils.Rpc.Message;
|
|
||||||
|
|
||||||
sealed class ReplySender<TMessageBase, TReplyMessage> : IReplySender where TReplyMessage : TMessageBase {
|
|
||||||
private readonly RpcConnection<TMessageBase> connection;
|
|
||||||
private readonly IReplyMessageFactory<TReplyMessage> replyMessageFactory;
|
|
||||||
|
|
||||||
public ReplySender(RpcConnection<TMessageBase> connection, IReplyMessageFactory<TReplyMessage> replyMessageFactory) {
|
|
||||||
this.connection = connection;
|
|
||||||
this.replyMessageFactory = replyMessageFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task SendReply(uint sequenceId, byte[] serializedReply) {
|
|
||||||
return connection.Send(replyMessageFactory.CreateReplyMessage(sequenceId, serializedReply));
|
|
||||||
}
|
|
||||||
}
|
|
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MemoryPack" />
|
<PackageReference Include="MemoryPack" />
|
||||||
<PackageReference Include="NetMQ" />
|
|
||||||
<PackageReference Include="Serilog" />
|
<PackageReference Include="Serilog" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
@@ -1,8 +0,0 @@
|
|||||||
using NetMQ;
|
|
||||||
|
|
||||||
namespace Phantom.Utils.Rpc;
|
|
||||||
|
|
||||||
public sealed record RpcConfiguration(string ServiceName, string Host, ushort Port, NetMQCertificate ServerCertificate) {
|
|
||||||
internal string LoggerName => "Rpc:" + ServiceName;
|
|
||||||
internal string TcpUrl => "tcp://" + Host + ":" + Port;
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user