mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2025-09-16 09:24:49 +02:00
Compare commits
42 Commits
c99f5bc6bf
...
wip-rpc
Author | SHA1 | Date | |
---|---|---|---|
cf609a02f8
|
|||
23f9e079c1
|
|||
8f8e0cf2e0
|
|||
a65f22f97d
|
|||
9a4ac4604e
|
|||
a8fe48ec77
|
|||
62d10f248d
|
|||
3f8e46ae2d
|
|||
584e8acfd0
|
|||
47e563f7ce
|
|||
29c403f2d4
|
|||
2947fa3522
|
|||
4d1a79307f
|
|||
cc4eb8aa9c
|
|||
de1767c876
|
|||
39f2fa4b17
|
|||
19e0d6fd3d
|
|||
591a6a62ab
|
|||
ae32537d8c
|
|||
8149d31d51
|
|||
9873f8779f
|
|||
31e101b21e
|
|||
398bb14742
|
|||
4e89d7b12f
|
|||
3b313bf0a8
|
|||
21c90cb7c2
|
|||
7b68b5d50d
|
|||
2d70d2b7d1
|
|||
8c623171f3
|
|||
62f8c685f0
|
|||
4a110db078
|
|||
f683a1f700
|
|||
3ffb37529c
|
|||
4c3b81c54a
|
|||
8e2b019aa1
|
|||
9a2c13c1e0
|
|||
991b32032c
|
|||
875fd9a766
|
|||
f7f08ec55c
|
|||
1b12fd9c3b
|
|||
424dccb14e
|
|||
d03f532996
|
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "7.0.0-rc.1.22426.7",
|
||||
"version": "8.0.3",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -192,7 +192,6 @@ ClientBin/
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
|
@@ -5,7 +5,7 @@
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent1" />
|
||||
<option name="PASS_PARENT_ENVS" value="1" />
|
||||
<envs>
|
||||
<env name="AGENT_KEY" value="JXBQQYG5T267RQS75MXWBTCJZY5CKTCCGQY22MCZPHSQQSJYCHH2NG2TCNXQY6TBSXM9NQDRS2CMX" />
|
||||
<env name="AGENT_KEY" value="K5ZRZYYJ9GWM2FS6XH5N5QQ7WZRPNDGHYMN5QP7RP6PPY27KRPMSYGCN" />
|
||||
<env name="AGENT_NAME" value="Agent 1" />
|
||||
<env name="ALLOWED_RCON_PORTS" value="25575,27000,27001" />
|
||||
<env name="ALLOWED_SERVER_PORTS" value="25565,26000,26001" />
|
||||
@@ -15,14 +15,19 @@
|
||||
<env name="MAX_MEMORY" value="12G" />
|
||||
</envs>
|
||||
<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="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_EXE_PATH_TRACKING" value="1" />
|
||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
|
||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||
<option name="PROJECT_TFM" value="net8.0" />
|
||||
<option name="PROJECT_TFM" value="net9.0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
|
@@ -5,7 +5,7 @@
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent2" />
|
||||
<option name="PASS_PARENT_ENVS" value="1" />
|
||||
<envs>
|
||||
<env name="AGENT_KEY" value="JXBQQYG5T267RQS75MXWBTCJZY5CKTCCGQY22MCZPHSQQSJYCHH2NG2TCNXQY6TBSXM9NQDRS2CMX" />
|
||||
<env name="AGENT_KEY" value="K5ZRZYYJ9GWM2FS6XH5N5QQ7WZRPNDGHYMN5QP7RP6PPY27KRPMSYGCN" />
|
||||
<env name="AGENT_NAME" value="Agent 2" />
|
||||
<env name="ALLOWED_RCON_PORTS" value="27002-27006" />
|
||||
<env name="ALLOWED_SERVER_PORTS" value="26002-26006" />
|
||||
@@ -15,14 +15,19 @@
|
||||
<env name="MAX_MEMORY" value="10G" />
|
||||
</envs>
|
||||
<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="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_EXE_PATH_TRACKING" value="1" />
|
||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
|
||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||
<option name="PROJECT_TFM" value="net8.0" />
|
||||
<option name="PROJECT_TFM" value="net9.0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
|
@@ -5,7 +5,7 @@
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent3" />
|
||||
<option name="PASS_PARENT_ENVS" value="1" />
|
||||
<envs>
|
||||
<env name="AGENT_KEY" value="JXBQQYG5T267RQS75MXWBTCJZY5CKTCCGQY22MCZPHSQQSJYCHH2NG2TCNXQY6TBSXM9NQDRS2CMX" />
|
||||
<env name="AGENT_KEY" value="K5ZRZYYJ9GWM2FS6XH5N5QQ7WZRPNDGHYMN5QP7RP6PPY27KRPMSYGCN" />
|
||||
<env name="AGENT_NAME" value="Agent 3" />
|
||||
<env name="ALLOWED_RCON_PORTS" value="27007" />
|
||||
<env name="ALLOWED_SERVER_PORTS" value="26007" />
|
||||
@@ -15,14 +15,19 @@
|
||||
<env name="MAX_MEMORY" value="2560M" />
|
||||
</envs>
|
||||
<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="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_EXE_PATH_TRACKING" value="1" />
|
||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
|
||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||
<option name="PROJECT_TFM" value="net8.0" />
|
||||
<option name="PROJECT_TFM" value="net9.0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
|
@@ -14,14 +14,19 @@
|
||||
<env name="WEB_RPC_SERVER_HOST" value="localhost" />
|
||||
</envs>
|
||||
<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="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_EXE_PATH_TRACKING" value="1" />
|
||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
|
||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||
<option name="PROJECT_TFM" value="net8.0" />
|
||||
<option name="PROJECT_TFM" value="net9.0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
|
@@ -7,18 +7,23 @@
|
||||
<envs>
|
||||
<env name="ASPNETCORE_ENVIRONMENT" value="Development" />
|
||||
<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" />
|
||||
</envs>
|
||||
<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="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_EXE_PATH_TRACKING" value="1" />
|
||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
|
||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||
<option name="PROJECT_TFM" value="net8.0" />
|
||||
<option name="PROJECT_TFM" value="net9.0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</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><:
|
@@ -105,7 +105,7 @@ public abstract class BaseLauncher : IServerLauncher {
|
||||
|
||||
private static async Task AcceptEula(InstanceProperties instanceProperties) {
|
||||
var eulaFilePath = Path.Combine(instanceProperties.InstanceFolder, "eula.txt");
|
||||
await File.WriteAllLinesAsync(eulaFilePath, new [] { "# EULA", "eula=true" }, Encoding.UTF8);
|
||||
await File.WriteAllLinesAsync(eulaFilePath, new[] { "# EULA", "eula=true" }, Encoding.UTF8);
|
||||
}
|
||||
|
||||
private static async Task UpdateServerProperties(InstanceProperties instanceProperties) {
|
||||
|
@@ -18,4 +18,5 @@ static class MinecraftServerProperties {
|
||||
public static readonly MinecraftServerProperty<ushort> ServerPort = new UnsignedShort("server-port");
|
||||
public static readonly MinecraftServerProperty<ushort> RconPort = new UnsignedShort("rcon.port");
|
||||
public static readonly MinecraftServerProperty<bool> EnableRcon = new Boolean("enable-rcon");
|
||||
public static readonly MinecraftServerProperty<bool> SyncChunkWrites = new Boolean("sync-chunk-writes");
|
||||
}
|
||||
|
@@ -5,11 +5,13 @@ namespace Phantom.Agent.Minecraft.Properties;
|
||||
public sealed record ServerProperties(
|
||||
ushort ServerPort,
|
||||
ushort RconPort,
|
||||
bool EnableRcon = true
|
||||
bool EnableRcon = true,
|
||||
bool SyncChunkWrites = false
|
||||
) {
|
||||
internal void SetTo(JavaPropertiesFileEditor properties) {
|
||||
MinecraftServerProperties.ServerPort.Set(properties, ServerPort);
|
||||
MinecraftServerProperties.RconPort.Set(properties, RconPort);
|
||||
MinecraftServerProperties.EnableRcon.Set(properties, EnableRcon);
|
||||
MinecraftServerProperties.SyncChunkWrites.Set(properties, SyncChunkWrites);
|
||||
}
|
||||
}
|
||||
|
@@ -16,32 +16,47 @@ sealed class MinecraftServerExecutableDownloader {
|
||||
public event EventHandler? Completed;
|
||||
|
||||
private readonly CancellationTokenSource cancellationTokenSource = new ();
|
||||
private int listeners = 0;
|
||||
|
||||
private readonly List<CancellationTokenRegistration> listenerCancellationRegistrations = new ();
|
||||
private int listenerCount = 0;
|
||||
|
||||
public MinecraftServerExecutableDownloader(FileDownloadInfo fileDownloadInfo, string minecraftVersion, string filePath, MinecraftServerExecutableDownloadListener listener) {
|
||||
Register(listener);
|
||||
Task = DownloadAndGetPath(fileDownloadInfo, minecraftVersion, filePath);
|
||||
Task = DownloadAndGetPath(fileDownloadInfo, minecraftVersion, filePath, new DownloadProgressCallback(this), cancellationTokenSource.Token);
|
||||
Task.ContinueWith(OnCompleted, TaskScheduler.Default);
|
||||
}
|
||||
|
||||
public void Register(MinecraftServerExecutableDownloadListener listener) {
|
||||
++listeners;
|
||||
Logger.Debug("Registered download listener, current listener count: {Listeners}", listeners);
|
||||
int newListenerCount;
|
||||
|
||||
lock (this) {
|
||||
newListenerCount = ++listenerCount;
|
||||
|
||||
DownloadProgress += listener.DownloadProgressEventHandler;
|
||||
listener.CancellationToken.Register(Unregister, listener);
|
||||
listenerCancellationRegistrations.Add(listener.CancellationToken.Register(Unregister, listener));
|
||||
}
|
||||
|
||||
Logger.Debug("Registered download listener, current listener count: {Listeners}", newListenerCount);
|
||||
}
|
||||
|
||||
private void Unregister(object? listenerObject) {
|
||||
int newListenerCount;
|
||||
|
||||
lock (this) {
|
||||
MinecraftServerExecutableDownloadListener listener = (MinecraftServerExecutableDownloadListener) listenerObject!;
|
||||
DownloadProgress -= listener.DownloadProgressEventHandler;
|
||||
|
||||
if (--listeners <= 0) {
|
||||
Logger.Debug("Unregistered last download listener, cancelling download.");
|
||||
newListenerCount = --listenerCount;
|
||||
if (newListenerCount <= 0) {
|
||||
cancellationTokenSource.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
if (newListenerCount <= 0) {
|
||||
Logger.Debug("Unregistered last download listener, cancelling download.");
|
||||
}
|
||||
else {
|
||||
Logger.Debug("Unregistered download listener, current listener count: {Listeners}", listeners);
|
||||
Logger.Debug("Unregistered download listener, current listener count: {Listeners}", newListenerCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,9 +66,19 @@ sealed class MinecraftServerExecutableDownloader {
|
||||
|
||||
private void OnCompleted(Task task) {
|
||||
Logger.Debug("Download task completed.");
|
||||
|
||||
lock (this) {
|
||||
Completed?.Invoke(this, EventArgs.Empty);
|
||||
Completed = null;
|
||||
DownloadProgress = null;
|
||||
|
||||
foreach (var registration in listenerCancellationRegistrations) {
|
||||
registration.Dispose();
|
||||
}
|
||||
|
||||
listenerCancellationRegistrations.Clear();
|
||||
cancellationTokenSource.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DownloadProgressCallback {
|
||||
@@ -68,15 +93,14 @@ sealed class MinecraftServerExecutableDownloader {
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string?> DownloadAndGetPath(FileDownloadInfo fileDownloadInfo, string minecraftVersion, string filePath) {
|
||||
private static async Task<string?> DownloadAndGetPath(FileDownloadInfo fileDownloadInfo, string minecraftVersion, string filePath, DownloadProgressCallback progressCallback, CancellationToken cancellationToken) {
|
||||
string tmpFilePath = filePath + ".tmp";
|
||||
|
||||
var cancellationToken = cancellationTokenSource.Token;
|
||||
try {
|
||||
Logger.Information("Downloading server version {Version} from: {Url} ({Size})", minecraftVersion, fileDownloadInfo.DownloadUrl, fileDownloadInfo.Size.ToHumanReadable(decimalPlaces: 1));
|
||||
try {
|
||||
using var http = new HttpClient();
|
||||
await FetchServerExecutableFile(http, new DownloadProgressCallback(this), fileDownloadInfo, tmpFilePath, cancellationToken);
|
||||
await FetchServerExecutableFile(http, progressCallback, fileDownloadInfo, tmpFilePath, cancellationToken);
|
||||
} catch (Exception) {
|
||||
TryDeleteExecutableAfterFailure(tmpFilePath);
|
||||
throw;
|
||||
@@ -94,8 +118,6 @@ sealed class MinecraftServerExecutableDownloader {
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "An unexpected error occurred.");
|
||||
return null;
|
||||
} finally {
|
||||
cancellationTokenSource.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -3,28 +3,12 @@ using System.Buffers.Binary;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using Phantom.Utils.Logging;
|
||||
using Serilog;
|
||||
using Phantom.Common.Data.Instance;
|
||||
|
||||
namespace Phantom.Agent.Minecraft.Server;
|
||||
|
||||
public sealed class ServerStatusProtocol {
|
||||
private readonly ILogger logger;
|
||||
|
||||
public ServerStatusProtocol(string loggerName) {
|
||||
this.logger = PhantomLogger.Create<ServerStatusProtocol>(loggerName);
|
||||
}
|
||||
|
||||
public async Task<int?> GetOnlinePlayerCount(int serverPort, CancellationToken cancellationToken) {
|
||||
try {
|
||||
return await GetOnlinePlayerCountOrThrow(serverPort, cancellationToken);
|
||||
} catch (Exception e) {
|
||||
logger.Error(e, "Caught exception while checking if players are online.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<int?> GetOnlinePlayerCountOrThrow(int serverPort, CancellationToken cancellationToken) {
|
||||
public static class ServerStatusProtocol {
|
||||
public static async Task<InstancePlayerCounts> GetPlayerCounts(ushort serverPort, CancellationToken cancellationToken) {
|
||||
using var tcpClient = new TcpClient();
|
||||
await tcpClient.ConnectAsync(IPAddress.Loopback, serverPort, cancellationToken);
|
||||
var tcpStream = tcpClient.GetStream();
|
||||
@@ -33,24 +17,22 @@ public sealed class ServerStatusProtocol {
|
||||
tcpStream.WriteByte(0xFE);
|
||||
await tcpStream.FlushAsync(cancellationToken);
|
||||
|
||||
short? messageLength = await ReadStreamHeader(tcpStream, cancellationToken);
|
||||
return messageLength == null ? null : await ReadOnlinePlayerCount(tcpStream, messageLength.Value * 2, cancellationToken);
|
||||
short messageLength = await ReadStreamHeader(tcpStream, cancellationToken);
|
||||
return await ReadPlayerCounts(tcpStream, messageLength * 2, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<short?> ReadStreamHeader(NetworkStream tcpStream, CancellationToken cancellationToken) {
|
||||
private static async Task<short> ReadStreamHeader(NetworkStream tcpStream, CancellationToken cancellationToken) {
|
||||
var headerBuffer = ArrayPool<byte>.Shared.Rent(3);
|
||||
try {
|
||||
await tcpStream.ReadExactlyAsync(headerBuffer, 0, 3, cancellationToken);
|
||||
|
||||
if (headerBuffer[0] != 0xFF) {
|
||||
logger.Error("Unexpected first byte in response from server: {FirstByte}.", headerBuffer[0]);
|
||||
return null;
|
||||
throw new ProtocolException("Unexpected first byte in response from server: " + headerBuffer[0]);
|
||||
}
|
||||
|
||||
short messageLength = BinaryPrimitives.ReadInt16BigEndian(headerBuffer.AsSpan(1));
|
||||
if (messageLength <= 0) {
|
||||
logger.Error("Unexpected message length in response from server: {MessageLength}.", messageLength);
|
||||
return null;
|
||||
throw new ProtocolException("Unexpected message length in response from server: " + messageLength);
|
||||
}
|
||||
|
||||
return messageLength;
|
||||
@@ -59,35 +41,54 @@ public sealed class ServerStatusProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<int?> ReadOnlinePlayerCount(NetworkStream tcpStream, int messageLength, CancellationToken cancellationToken) {
|
||||
private static async Task<InstancePlayerCounts> ReadPlayerCounts(NetworkStream tcpStream, int messageLength, CancellationToken cancellationToken) {
|
||||
var messageBuffer = ArrayPool<byte>.Shared.Rent(messageLength);
|
||||
try {
|
||||
await tcpStream.ReadExactlyAsync(messageBuffer, 0, messageLength, cancellationToken);
|
||||
|
||||
// Valid response separator encoded in UTF-16BE is 0x00 0xA7 (§).
|
||||
const byte SeparatorSecondByte = 0xA7;
|
||||
|
||||
static bool IsValidSeparator(ReadOnlySpan<byte> buffer, int index) {
|
||||
return index > 0 && buffer[index - 1] == 0x00;
|
||||
}
|
||||
|
||||
int separator2 = Array.LastIndexOf(messageBuffer, SeparatorSecondByte);
|
||||
int separator1 = separator2 == -1 ? -1 : Array.LastIndexOf(messageBuffer, SeparatorSecondByte, separator2 - 1);
|
||||
if (!IsValidSeparator(messageBuffer, separator1) || !IsValidSeparator(messageBuffer, separator2)) {
|
||||
logger.Error("Could not find message separators in response from server.");
|
||||
return null;
|
||||
}
|
||||
|
||||
string onlinePlayerCountStr = Encoding.BigEndianUnicode.GetString(messageBuffer.AsSpan((separator1 + 1)..(separator2 - 1)));
|
||||
if (!int.TryParse(onlinePlayerCountStr, out int onlinePlayerCount)) {
|
||||
logger.Error("Could not parse online player count in response from server: {OnlinePlayerCount}.", onlinePlayerCountStr);
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.Debug("Detected {OnlinePlayerCount} online player(s).", onlinePlayerCount);
|
||||
return onlinePlayerCount;
|
||||
return ReadPlayerCountsFromResponse(messageBuffer.AsSpan(0, messageLength));
|
||||
} finally {
|
||||
ArrayPool<byte>.Shared.Return(messageBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Legacy query protocol uses the paragraph symbol (§) as separator encoded in UTF-16BE.
|
||||
/// </summary>
|
||||
private static readonly byte[] Separator = { 0x00, 0xA7 };
|
||||
|
||||
private static InstancePlayerCounts ReadPlayerCountsFromResponse(ReadOnlySpan<byte> messageBuffer) {
|
||||
int lastSeparator = messageBuffer.LastIndexOf(Separator);
|
||||
int middleSeparator = messageBuffer[..lastSeparator].LastIndexOf(Separator);
|
||||
|
||||
if (lastSeparator == -1 || middleSeparator == -1) {
|
||||
throw new ProtocolException("Could not find message separators in response from server.");
|
||||
}
|
||||
|
||||
var onlinePlayerCountBuffer = messageBuffer[(middleSeparator + Separator.Length)..lastSeparator];
|
||||
var maximumPlayerCountBuffer = messageBuffer[(lastSeparator + Separator.Length)..];
|
||||
|
||||
// Player counts are integers, whose maximum string length is 10 characters.
|
||||
Span<char> integerStringBuffer = stackalloc char[10];
|
||||
|
||||
return new InstancePlayerCounts(
|
||||
DecodeAndParsePlayerCount(onlinePlayerCountBuffer, integerStringBuffer, "online"),
|
||||
DecodeAndParsePlayerCount(maximumPlayerCountBuffer, integerStringBuffer, "maximum")
|
||||
);
|
||||
}
|
||||
|
||||
private static int DecodeAndParsePlayerCount(ReadOnlySpan<byte> inputBuffer, Span<char> tempCharBuffer, string countType) {
|
||||
if (!Encoding.BigEndianUnicode.TryGetChars(inputBuffer, tempCharBuffer, out int charCount)) {
|
||||
throw new ProtocolException("Could not decode " + countType + " player count in response from server.");
|
||||
}
|
||||
|
||||
if (!int.TryParse(tempCharBuffer, out int playerCount)) {
|
||||
throw new ProtocolException("Could not parse " + countType + " player count in response from server: " + tempCharBuffer[..charCount].ToString());
|
||||
}
|
||||
|
||||
return playerCount;
|
||||
}
|
||||
|
||||
public sealed class ProtocolException : Exception {
|
||||
internal ProtocolException(string message) : base(message) {}
|
||||
}
|
||||
}
|
||||
|
@@ -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,13 @@
|
||||
using Akka.Actor;
|
||||
using System.Collections.Immutable;
|
||||
using Akka.Actor;
|
||||
using Phantom.Agent.Minecraft.Java;
|
||||
using Phantom.Agent.Rpc;
|
||||
using Phantom.Agent.Services.Backups;
|
||||
using Phantom.Agent.Services.Instances;
|
||||
using Phantom.Agent.Services.Rpc;
|
||||
using Phantom.Common.Data.Agent;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Messages.Agent.ToAgent;
|
||||
using Phantom.Common.Messages.Agent.ToController;
|
||||
using Phantom.Utils.Actor;
|
||||
using Phantom.Utils.Logging;
|
||||
using Serilog;
|
||||
@@ -15,6 +19,7 @@ public sealed class AgentServices {
|
||||
|
||||
public ActorSystem ActorSystem { get; }
|
||||
|
||||
private AgentInfo AgentInfo { get; }
|
||||
private AgentFolders AgentFolders { get; }
|
||||
private AgentState AgentState { get; }
|
||||
private BackupManager BackupManager { get; }
|
||||
@@ -26,6 +31,7 @@ public sealed class AgentServices {
|
||||
public AgentServices(AgentInfo agentInfo, AgentFolders agentFolders, AgentServiceConfiguration serviceConfiguration, ControllerConnection controllerConnection) {
|
||||
this.ActorSystem = ActorSystemFactory.Create("Agent");
|
||||
|
||||
this.AgentInfo = agentInfo;
|
||||
this.AgentFolders = agentFolders;
|
||||
this.AgentState = new AgentState();
|
||||
this.BackupManager = new BackupManager(agentFolders, serviceConfiguration.MaxConcurrentCompressionTasks);
|
||||
@@ -43,6 +49,41 @@ public sealed class AgentServices {
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> Register(ControllerConnection connection, CancellationToken cancellationToken) {
|
||||
Logger.Information("Registering with the controller...");
|
||||
|
||||
// TODO NEED TO SEND WHEN SERVER RESTARTS!!!
|
||||
|
||||
ImmutableArray<ConfigureInstanceMessage> configureInstanceMessages;
|
||||
try {
|
||||
configureInstanceMessages = await connection.Send<RegisterAgentMessage, ImmutableArray<ConfigureInstanceMessage>>(new RegisterAgentMessage(AgentInfo), TimeSpan.FromMinutes(1), cancellationToken);
|
||||
} catch (Exception e) {
|
||||
Logger.Fatal(e, "Registration 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;
|
||||
}
|
||||
}
|
||||
|
||||
await connection.Send(new AdvertiseJavaRuntimesMessage(JavaRuntimeRepository.All), cancellationToken);
|
||||
InstanceTicketManager.RefreshAgentStatus();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task Shutdown() {
|
||||
Logger.Information("Stopping services...");
|
||||
|
||||
|
@@ -67,6 +67,10 @@ sealed class BackupManager : IDisposable {
|
||||
resultBuilder.Kind = BackupCreationResultKind.BackupCancelled;
|
||||
logger.Warning("Backup creation was cancelled.");
|
||||
return null;
|
||||
} catch (TimeoutException) {
|
||||
resultBuilder.Kind = BackupCreationResultKind.BackupTimedOut;
|
||||
logger.Warning("Backup creation timed out.");
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
resultBuilder.Kind = BackupCreationResultKind.UnknownError;
|
||||
logger.Error(e, "Caught exception while creating an instance backup.");
|
||||
@@ -76,6 +80,9 @@ sealed class BackupManager : IDisposable {
|
||||
await dispatcher.EnableAutomaticSaving();
|
||||
} catch (OperationCanceledException) {
|
||||
// Ignore.
|
||||
} catch (TimeoutException) {
|
||||
resultBuilder.Warnings |= BackupCreationWarnings.CouldNotRestoreAutomaticSaving;
|
||||
logger.Warning("Timed out waiting for automatic saving to be re-enabled.");
|
||||
} catch (Exception e) {
|
||||
resultBuilder.Warnings |= BackupCreationWarnings.CouldNotRestoreAutomaticSaving;
|
||||
logger.Error(e, "Caught exception while enabling automatic saving after creating an instance backup.");
|
||||
@@ -120,6 +127,7 @@ sealed class BackupManager : IDisposable {
|
||||
BackupCreationResultKind.Success => "Backup created successfully.",
|
||||
BackupCreationResultKind.InstanceNotRunning => "Instance is not running.",
|
||||
BackupCreationResultKind.BackupCancelled => "Backup cancelled.",
|
||||
BackupCreationResultKind.BackupTimedOut => "Backup timed out.",
|
||||
BackupCreationResultKind.BackupAlreadyRunning => "A backup is already being created.",
|
||||
BackupCreationResultKind.BackupFileAlreadyExists => "Backup with the same name already exists.",
|
||||
BackupCreationResultKind.CouldNotCreateBackupFolder => "Could not create backup folder.",
|
||||
|
@@ -1,10 +1,8 @@
|
||||
using Phantom.Agent.Minecraft.Instance;
|
||||
using Phantom.Agent.Minecraft.Server;
|
||||
using Phantom.Agent.Services.Instances;
|
||||
using Phantom.Agent.Services.Instances;
|
||||
using Phantom.Agent.Services.Instances.State;
|
||||
using Phantom.Common.Data.Backups;
|
||||
using Phantom.Utils.Logging;
|
||||
using Phantom.Utils.Tasks;
|
||||
using Phantom.Utils.Threading;
|
||||
|
||||
namespace Phantom.Agent.Services.Backups;
|
||||
|
||||
@@ -16,20 +14,16 @@ sealed class BackupScheduler : CancellableBackgroundTask {
|
||||
|
||||
private readonly BackupManager backupManager;
|
||||
private readonly InstanceContext context;
|
||||
private readonly InstanceProcess process;
|
||||
private readonly SemaphoreSlim backupSemaphore = new (1, 1);
|
||||
private readonly int serverPort;
|
||||
private readonly ServerStatusProtocol serverStatusProtocol;
|
||||
private readonly ManualResetEventSlim serverOutputWhileWaitingForOnlinePlayers = new ();
|
||||
private readonly InstancePlayerCountTracker playerCountTracker;
|
||||
|
||||
public event EventHandler<BackupCreationResult>? BackupCompleted;
|
||||
|
||||
public BackupScheduler(InstanceContext context, InstanceProcess process, int serverPort) : base(PhantomLogger.Create<BackupScheduler>(context.ShortName)) {
|
||||
public BackupScheduler(InstanceContext context, InstancePlayerCountTracker playerCountTracker) : base(PhantomLogger.Create<BackupScheduler>(context.ShortName)) {
|
||||
this.backupManager = context.Services.BackupManager;
|
||||
this.context = context;
|
||||
this.process = process;
|
||||
this.serverPort = serverPort;
|
||||
this.serverStatusProtocol = new ServerStatusProtocol(context.ShortName);
|
||||
this.playerCountTracker = playerCountTracker;
|
||||
Start();
|
||||
}
|
||||
|
||||
@@ -69,43 +63,18 @@ sealed class BackupScheduler : CancellableBackgroundTask {
|
||||
}
|
||||
|
||||
private async Task WaitForOnlinePlayers() {
|
||||
bool needsToLogOfflinePlayersMessage = true;
|
||||
var task = playerCountTracker.WaitForOnlinePlayers(CancellationToken);
|
||||
if (!task.IsCompleted) {
|
||||
Logger.Information("Waiting for someone to join before starting a new backup.");
|
||||
}
|
||||
|
||||
process.AddOutputListener(ServerOutputListener, maxLinesToReadFromHistory: 0);
|
||||
try {
|
||||
while (!CancellationToken.IsCancellationRequested) {
|
||||
serverOutputWhileWaitingForOnlinePlayers.Reset();
|
||||
|
||||
var onlinePlayerCount = await serverStatusProtocol.GetOnlinePlayerCount(serverPort, CancellationToken);
|
||||
if (onlinePlayerCount == null) {
|
||||
Logger.Warning("Could not detect whether any players are online, starting a new backup.");
|
||||
break;
|
||||
}
|
||||
|
||||
if (onlinePlayerCount > 0) {
|
||||
await task;
|
||||
Logger.Information("Players are online, starting a new backup.");
|
||||
break;
|
||||
}
|
||||
|
||||
if (needsToLogOfflinePlayersMessage) {
|
||||
needsToLogOfflinePlayersMessage = false;
|
||||
Logger.Information("No players are online, waiting for someone to join before starting a new backup.");
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), CancellationToken);
|
||||
|
||||
Logger.Debug("Waiting for server output before checking for online players again...");
|
||||
await serverOutputWhileWaitingForOnlinePlayers.WaitHandle.WaitOneAsync(CancellationToken);
|
||||
}
|
||||
} finally {
|
||||
process.RemoveOutputListener(ServerOutputListener);
|
||||
}
|
||||
}
|
||||
|
||||
private void ServerOutputListener(object? sender, string line) {
|
||||
if (!serverOutputWhileWaitingForOnlinePlayers.IsSet) {
|
||||
serverOutputWhileWaitingForOnlinePlayers.Set();
|
||||
Logger.Debug("Detected server output, signalling to check for online players again.");
|
||||
} catch (OperationCanceledException) {
|
||||
throw;
|
||||
} catch (Exception) {
|
||||
Logger.Warning("Could not detect whether any players are online, starting a new backup.");
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.RegularExpressions;
|
||||
using Phantom.Agent.Minecraft.Command;
|
||||
using Phantom.Agent.Minecraft.Instance;
|
||||
using Phantom.Utils.Tasks;
|
||||
@@ -7,9 +8,27 @@ using Serilog;
|
||||
namespace Phantom.Agent.Services.Backups;
|
||||
|
||||
sealed partial class BackupServerCommandDispatcher : IDisposable {
|
||||
[GeneratedRegex(@"^\[(?:.*?)\] \[Server thread/INFO\]: (.*?)$", RegexOptions.NonBacktracking)]
|
||||
[GeneratedRegex(@"^(?:(?:\[.*?\] \[Server thread/INFO\].*?:)|(?:[\d-]+? [\d:]+? \[INFO\])) (.*?)$", RegexOptions.NonBacktracking)]
|
||||
private static partial Regex ServerThreadInfoRegex();
|
||||
|
||||
private static readonly ImmutableHashSet<string> AutomaticSavingDisabledMessages = ImmutableHashSet.Create(
|
||||
"Automatic saving is now disabled",
|
||||
"Turned off world auto-saving",
|
||||
"CONSOLE: Disabling level saving.."
|
||||
);
|
||||
|
||||
private static readonly ImmutableHashSet<string> SavedTheGameMessages = ImmutableHashSet.Create(
|
||||
"Saved the game",
|
||||
"Saved the world",
|
||||
"CONSOLE: Save complete."
|
||||
);
|
||||
|
||||
private static readonly ImmutableHashSet<string> AutomaticSavingEnabledMessages = ImmutableHashSet.Create(
|
||||
"Automatic saving is now enabled",
|
||||
"Turned on world auto-saving",
|
||||
"CONSOLE: Enabling level saving.."
|
||||
);
|
||||
|
||||
private readonly ILogger logger;
|
||||
private readonly InstanceProcess process;
|
||||
private readonly CancellationToken cancellationToken;
|
||||
@@ -32,18 +51,17 @@ sealed partial class BackupServerCommandDispatcher : IDisposable {
|
||||
|
||||
public async Task DisableAutomaticSaving() {
|
||||
await process.SendCommand(MinecraftCommand.SaveOff, cancellationToken);
|
||||
await automaticSavingDisabled.Task.WaitAsync(cancellationToken);
|
||||
await automaticSavingDisabled.Task.WaitAsync(TimeSpan.FromSeconds(30), cancellationToken);
|
||||
}
|
||||
|
||||
public async Task SaveAllChunks() {
|
||||
// TODO Try if not flushing and waiting a few seconds before flushing reduces lag.
|
||||
await process.SendCommand(MinecraftCommand.SaveAll(flush: true), cancellationToken);
|
||||
await savedTheGame.Task.WaitAsync(cancellationToken);
|
||||
await savedTheGame.Task.WaitAsync(TimeSpan.FromMinutes(1), cancellationToken);
|
||||
}
|
||||
|
||||
public async Task EnableAutomaticSaving() {
|
||||
await process.SendCommand(MinecraftCommand.SaveOn, cancellationToken);
|
||||
await automaticSavingEnabled.Task.WaitAsync(cancellationToken);
|
||||
await automaticSavingEnabled.Task.WaitAsync(TimeSpan.FromMinutes(1), cancellationToken);
|
||||
}
|
||||
|
||||
private void OnOutput(object? sender, string? line) {
|
||||
@@ -59,19 +77,19 @@ sealed partial class BackupServerCommandDispatcher : IDisposable {
|
||||
string info = match.Groups[1].Value;
|
||||
|
||||
if (!automaticSavingDisabled.Task.IsCompleted) {
|
||||
if (info == "Automatic saving is now disabled") {
|
||||
if (AutomaticSavingDisabledMessages.Contains(info)) {
|
||||
logger.Debug("Detected that automatic saving is disabled.");
|
||||
automaticSavingDisabled.SetResult();
|
||||
}
|
||||
}
|
||||
else if (!savedTheGame.Task.IsCompleted) {
|
||||
if (info == "Saved the game") {
|
||||
if (SavedTheGameMessages.Contains(info)) {
|
||||
logger.Debug("Detected that the game is saved.");
|
||||
savedTheGame.SetResult();
|
||||
}
|
||||
}
|
||||
else if (!automaticSavingEnabled.Task.IsCompleted) {
|
||||
if (info == "Automatic saving is now enabled") {
|
||||
if (AutomaticSavingEnabledMessages.Contains(info)) {
|
||||
logger.Debug("Detected that automatic saving is enabled.");
|
||||
automaticSavingEnabled.SetResult();
|
||||
}
|
||||
|
@@ -58,7 +58,7 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
|
||||
|
||||
private void ReportCurrentStatus() {
|
||||
agentState.UpdateInstance(new Instance(instanceGuid, currentStatus));
|
||||
instanceServices.ControllerConnection.Send(new ReportInstanceStatusMessage(instanceGuid, currentStatus));
|
||||
instanceServices.ControllerConnection.TrySend(new ReportInstanceStatusMessage(instanceGuid, currentStatus));
|
||||
}
|
||||
|
||||
private void TransitionState(InstanceRunningState? newState) {
|
||||
@@ -135,7 +135,12 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
|
||||
return new BackupCreationResult(BackupCreationResultKind.InstanceNotRunning);
|
||||
}
|
||||
else {
|
||||
SetAndReportStatus(InstanceStatus.BackingUp);
|
||||
try {
|
||||
return await command.BackupManager.CreateBackup(context.ShortName, runningState.Process, shutdownCancellationToken);
|
||||
} finally {
|
||||
SetAndReportStatus(InstanceStatus.Running);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -7,6 +7,6 @@ namespace Phantom.Agent.Services.Instances;
|
||||
|
||||
sealed record InstanceContext(Guid InstanceGuid, string ShortName, ILogger Logger, InstanceServices Services, ActorRef<InstanceActor.ICommand> Actor, CancellationToken ActorCancellationToken) {
|
||||
public void ReportEvent(IInstanceEvent instanceEvent) {
|
||||
Services.ControllerConnection.Send(new ReportInstanceEventMessage(Guid.NewGuid(), DateTime.UtcNow, InstanceGuid, instanceEvent));
|
||||
Services.ControllerConnection.TrySend(new ReportInstanceEventMessage(Guid.NewGuid(), DateTime.UtcNow, InstanceGuid, instanceEvent));
|
||||
}
|
||||
}
|
||||
|
@@ -4,15 +4,15 @@ using Phantom.Agent.Minecraft.Launcher;
|
||||
using Phantom.Agent.Minecraft.Launcher.Types;
|
||||
using Phantom.Agent.Minecraft.Properties;
|
||||
using Phantom.Agent.Minecraft.Server;
|
||||
using Phantom.Agent.Rpc;
|
||||
using Phantom.Agent.Services.Backups;
|
||||
using Phantom.Agent.Services.Rpc;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Instance;
|
||||
using Phantom.Common.Data.Minecraft;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Utils.Actor;
|
||||
using Phantom.Utils.IO;
|
||||
using Phantom.Utils.Logging;
|
||||
using Phantom.Utils.Tasks;
|
||||
using Serilog;
|
||||
|
||||
namespace Phantom.Agent.Services.Instances;
|
||||
@@ -49,10 +49,10 @@ sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand>
|
||||
|
||||
this.instanceServices = new InstanceServices(init.ControllerConnection, init.BackupManager, launchServices);
|
||||
|
||||
ReceiveAndReply<ConfigureInstanceCommand, InstanceActionResult<ConfigureInstanceResult>>(ConfigureInstance);
|
||||
ReceiveAndReply<LaunchInstanceCommand, InstanceActionResult<LaunchInstanceResult>>(LaunchInstance);
|
||||
ReceiveAndReply<StopInstanceCommand, InstanceActionResult<StopInstanceResult>>(StopInstance);
|
||||
ReceiveAsyncAndReply<SendCommandToInstanceCommand, InstanceActionResult<SendCommandToInstanceResult>>(SendCommandToInstance);
|
||||
ReceiveAndReply<ConfigureInstanceCommand, Result<ConfigureInstanceResult, InstanceActionFailure>>(ConfigureInstance);
|
||||
ReceiveAndReply<LaunchInstanceCommand, Result<LaunchInstanceResult, InstanceActionFailure>>(LaunchInstance);
|
||||
ReceiveAndReply<StopInstanceCommand, Result<StopInstanceResult, InstanceActionFailure>>(StopInstance);
|
||||
ReceiveAsyncAndReply<SendCommandToInstanceCommand, Result<SendCommandToInstanceResult, InstanceActionFailure>>(SendCommandToInstance);
|
||||
ReceiveAsync<ShutdownCommand>(Shutdown);
|
||||
}
|
||||
|
||||
@@ -65,17 +65,17 @@ sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand>
|
||||
|
||||
public interface ICommand {}
|
||||
|
||||
public sealed record ConfigureInstanceCommand(Guid InstanceGuid, InstanceConfiguration Configuration, InstanceLaunchProperties LaunchProperties, bool LaunchNow, bool AlwaysReportStatus) : ICommand, ICanReply<InstanceActionResult<ConfigureInstanceResult>>;
|
||||
public sealed record ConfigureInstanceCommand(Guid InstanceGuid, InstanceConfiguration Configuration, InstanceLaunchProperties LaunchProperties, bool LaunchNow, bool AlwaysReportStatus) : ICommand, ICanReply<Result<ConfigureInstanceResult, InstanceActionFailure>>;
|
||||
|
||||
public sealed record LaunchInstanceCommand(Guid InstanceGuid) : ICommand, ICanReply<InstanceActionResult<LaunchInstanceResult>>;
|
||||
public sealed record LaunchInstanceCommand(Guid InstanceGuid) : ICommand, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>;
|
||||
|
||||
public sealed record StopInstanceCommand(Guid InstanceGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<InstanceActionResult<StopInstanceResult>>;
|
||||
public sealed record StopInstanceCommand(Guid InstanceGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>;
|
||||
|
||||
public sealed record SendCommandToInstanceCommand(Guid InstanceGuid, string Command) : ICommand, ICanReply<InstanceActionResult<SendCommandToInstanceResult>>;
|
||||
public sealed record SendCommandToInstanceCommand(Guid InstanceGuid, string Command) : ICommand, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>;
|
||||
|
||||
public sealed record ShutdownCommand : ICommand;
|
||||
|
||||
private InstanceActionResult<ConfigureInstanceResult> ConfigureInstance(ConfigureInstanceCommand command) {
|
||||
private Result<ConfigureInstanceResult, InstanceActionFailure> ConfigureInstance(ConfigureInstanceCommand command) {
|
||||
var instanceGuid = command.InstanceGuid;
|
||||
var configuration = command.Configuration;
|
||||
|
||||
@@ -130,64 +130,64 @@ sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand>
|
||||
LaunchInstance(new LaunchInstanceCommand(instanceGuid));
|
||||
}
|
||||
|
||||
return InstanceActionResult.Concrete(ConfigureInstanceResult.Success);
|
||||
return ConfigureInstanceResult.Success;
|
||||
}
|
||||
|
||||
private InstanceActionResult<LaunchInstanceResult> LaunchInstance(LaunchInstanceCommand command) {
|
||||
private Result<LaunchInstanceResult, InstanceActionFailure> LaunchInstance(LaunchInstanceCommand command) {
|
||||
var instanceGuid = command.InstanceGuid;
|
||||
if (!instances.TryGetValue(instanceGuid, out var instanceInfo)) {
|
||||
return InstanceActionResult.General<LaunchInstanceResult>(InstanceActionGeneralResult.InstanceDoesNotExist);
|
||||
return InstanceActionFailure.InstanceDoesNotExist;
|
||||
}
|
||||
|
||||
var ticket = instanceTicketManager.Reserve(instanceInfo.Configuration);
|
||||
if (ticket is Result<InstanceTicketManager.Ticket, LaunchInstanceResult>.Fail fail) {
|
||||
return InstanceActionResult.Concrete(fail.Error);
|
||||
if (!ticket) {
|
||||
return ticket.Error;
|
||||
}
|
||||
|
||||
if (agentState.InstancesByGuid.TryGetValue(instanceGuid, out var instance)) {
|
||||
var status = instance.Status;
|
||||
if (status.IsRunning()) {
|
||||
return InstanceActionResult.Concrete(LaunchInstanceResult.InstanceAlreadyRunning);
|
||||
return LaunchInstanceResult.InstanceAlreadyRunning;
|
||||
}
|
||||
else if (status.IsLaunching()) {
|
||||
return InstanceActionResult.Concrete(LaunchInstanceResult.InstanceAlreadyLaunching);
|
||||
return LaunchInstanceResult.InstanceAlreadyLaunching;
|
||||
}
|
||||
}
|
||||
|
||||
instanceInfo.Actor.Tell(new InstanceActor.LaunchInstanceCommand(instanceInfo.Configuration, instanceInfo.Launcher, ticket.Value, IsRestarting: false));
|
||||
return InstanceActionResult.Concrete(LaunchInstanceResult.LaunchInitiated);
|
||||
return LaunchInstanceResult.LaunchInitiated;
|
||||
}
|
||||
|
||||
private InstanceActionResult<StopInstanceResult> StopInstance(StopInstanceCommand command) {
|
||||
private Result<StopInstanceResult, InstanceActionFailure> StopInstance(StopInstanceCommand command) {
|
||||
var instanceGuid = command.InstanceGuid;
|
||||
if (!instances.TryGetValue(instanceGuid, out var instanceInfo)) {
|
||||
return InstanceActionResult.General<StopInstanceResult>(InstanceActionGeneralResult.InstanceDoesNotExist);
|
||||
return InstanceActionFailure.InstanceDoesNotExist;
|
||||
}
|
||||
|
||||
if (agentState.InstancesByGuid.TryGetValue(instanceGuid, out var instance)) {
|
||||
var status = instance.Status;
|
||||
if (status.IsStopping()) {
|
||||
return InstanceActionResult.Concrete(StopInstanceResult.InstanceAlreadyStopping);
|
||||
return StopInstanceResult.InstanceAlreadyStopping;
|
||||
}
|
||||
else if (!status.CanStop()) {
|
||||
return InstanceActionResult.Concrete(StopInstanceResult.InstanceAlreadyStopped);
|
||||
return StopInstanceResult.InstanceAlreadyStopped;
|
||||
}
|
||||
}
|
||||
|
||||
instanceInfo.Actor.Tell(new InstanceActor.StopInstanceCommand(command.StopStrategy));
|
||||
return InstanceActionResult.Concrete(StopInstanceResult.StopInitiated);
|
||||
return StopInstanceResult.StopInitiated;
|
||||
}
|
||||
|
||||
private async Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommandToInstance(SendCommandToInstanceCommand command) {
|
||||
private async Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> SendCommandToInstance(SendCommandToInstanceCommand command) {
|
||||
var instanceGuid = command.InstanceGuid;
|
||||
if (!instances.TryGetValue(instanceGuid, out var instanceInfo)) {
|
||||
return InstanceActionResult.General<SendCommandToInstanceResult>(InstanceActionGeneralResult.InstanceDoesNotExist);
|
||||
return InstanceActionFailure.InstanceDoesNotExist;
|
||||
}
|
||||
|
||||
try {
|
||||
return InstanceActionResult.Concrete(await instanceInfo.Actor.Request(new InstanceActor.SendCommandToInstanceCommand(command.Command), shutdownCancellationToken));
|
||||
return await instanceInfo.Actor.Request(new InstanceActor.SendCommandToInstanceCommand(command.Command), shutdownCancellationToken);
|
||||
} catch (OperationCanceledException) {
|
||||
return InstanceActionResult.General<SendCommandToInstanceResult>(InstanceActionGeneralResult.AgentShuttingDown);
|
||||
return InstanceActionFailure.AgentShuttingDown;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
using Phantom.Agent.Minecraft.Launcher;
|
||||
using Phantom.Agent.Rpc;
|
||||
using Phantom.Agent.Services.Backups;
|
||||
using Phantom.Agent.Services.Rpc;
|
||||
|
||||
namespace Phantom.Agent.Services.Instances;
|
||||
|
||||
|
@@ -1,11 +1,10 @@
|
||||
using Phantom.Agent.Rpc;
|
||||
using Phantom.Agent.Services.Rpc;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Agent;
|
||||
using Phantom.Common.Data.Instance;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Messages.Agent.ToController;
|
||||
using Phantom.Utils.Logging;
|
||||
using Phantom.Utils.Tasks;
|
||||
using Serilog;
|
||||
|
||||
namespace Phantom.Agent.Services.Instances;
|
||||
@@ -92,7 +91,7 @@ sealed class InstanceTicketManager {
|
||||
|
||||
public void RefreshAgentStatus() {
|
||||
lock (this) {
|
||||
controllerConnection.Send(new ReportAgentStatusMessage(activeTicketGuids.Count, usedMemory));
|
||||
controllerConnection.TrySend(new ReportAgentStatusMessage(activeTicketGuids.Count, usedMemory));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,8 +1,8 @@
|
||||
using Phantom.Agent.Minecraft.Instance;
|
||||
using Phantom.Agent.Minecraft.Launcher;
|
||||
using Phantom.Agent.Minecraft.Server;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Instance;
|
||||
using Phantom.Utils.Tasks;
|
||||
|
||||
namespace Phantom.Agent.Services.Instances.State;
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Threading.Channels;
|
||||
using Phantom.Agent.Rpc;
|
||||
using Phantom.Agent.Services.Rpc;
|
||||
using Phantom.Common.Messages.Agent.ToController;
|
||||
using Phantom.Utils.Logging;
|
||||
using Phantom.Utils.Tasks;
|
||||
@@ -63,7 +63,7 @@ sealed class InstanceLogSender : CancellableBackgroundTask {
|
||||
|
||||
private void SendOutputToServer(ImmutableArray<string> lines) {
|
||||
if (!lines.IsEmpty) {
|
||||
controllerConnection.Send(new InstanceOutputMessage(instanceGuid, lines));
|
||||
controllerConnection.TrySend(new InstanceOutputMessage(instanceGuid, lines));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,140 @@
|
||||
using Phantom.Agent.Minecraft.Instance;
|
||||
using Phantom.Agent.Minecraft.Server;
|
||||
using Phantom.Agent.Services.Rpc;
|
||||
using Phantom.Common.Data.Instance;
|
||||
using Phantom.Common.Messages.Agent.ToController;
|
||||
using Phantom.Utils.Logging;
|
||||
using Phantom.Utils.Tasks;
|
||||
using Phantom.Utils.Threading;
|
||||
|
||||
namespace Phantom.Agent.Services.Instances.State;
|
||||
|
||||
sealed class InstancePlayerCountTracker : CancellableBackgroundTask {
|
||||
private readonly ControllerConnection controllerConnection;
|
||||
private readonly Guid instanceGuid;
|
||||
private readonly ushort serverPort;
|
||||
private readonly InstanceProcess process;
|
||||
|
||||
private readonly TaskCompletionSource firstDetection = AsyncTasks.CreateCompletionSource();
|
||||
private readonly ManualResetEventSlim serverOutputEvent = new ();
|
||||
|
||||
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.TrySend(new ReportInstancePlayerCountsMessage(instanceGuid, value));
|
||||
}
|
||||
}
|
||||
|
||||
private event EventHandler<int?>? OnlinePlayerCountChanged;
|
||||
|
||||
private bool isDisposed = false;
|
||||
|
||||
public InstancePlayerCountTracker(InstanceContext context, InstanceProcess process, ushort serverPort) : base(PhantomLogger.Create<InstancePlayerCountTracker>(context.ShortName)) {
|
||||
this.controllerConnection = context.Services.ControllerConnection;
|
||||
this.instanceGuid = context.InstanceGuid;
|
||||
this.process = process;
|
||||
this.serverPort = serverPort;
|
||||
Start();
|
||||
}
|
||||
|
||||
protected override async Task RunTask() {
|
||||
// Give the server time to start accepting connections.
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), CancellationToken);
|
||||
|
||||
serverOutputEvent.Set();
|
||||
process.AddOutputListener(OnOutput, maxLinesToReadFromHistory: 0);
|
||||
|
||||
while (!CancellationToken.IsCancellationRequested) {
|
||||
serverOutputEvent.Reset();
|
||||
|
||||
PlayerCounts = await TryGetPlayerCounts();
|
||||
|
||||
if (!firstDetection.Task.IsCompleted) {
|
||||
firstDetection.SetResult();
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), CancellationToken);
|
||||
await serverOutputEvent.WaitHandle.WaitOneAsync(CancellationToken);
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), CancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<InstancePlayerCounts?> TryGetPlayerCounts() {
|
||||
try {
|
||||
var result = await ServerStatusProtocol.GetPlayerCounts(serverPort, CancellationToken);
|
||||
Logger.Debug("Detected {OnlinePlayerCount} / {MaximumPlayerCount} online player(s).", result.Online, result.Maximum);
|
||||
return result;
|
||||
} catch (ServerStatusProtocol.ProtocolException e) {
|
||||
Logger.Error(e.Message);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Caught exception while checking online player count.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task WaitForOnlinePlayers(CancellationToken cancellationToken) {
|
||||
await firstDetection.Task.WaitAsync(cancellationToken);
|
||||
|
||||
var onlinePlayersDetected = AsyncTasks.CreateCompletionSource();
|
||||
|
||||
lock (this) {
|
||||
if (playerCounts is { Online: > 0 }) {
|
||||
return;
|
||||
}
|
||||
else if (playerCounts == null) {
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
OnlinePlayerCountChanged += OnOnlinePlayerCountChanged;
|
||||
|
||||
void OnOnlinePlayerCountChanged(object? sender, int? newPlayerCount) {
|
||||
if (newPlayerCount == null) {
|
||||
onlinePlayersDetected.TrySetException(new InvalidOperationException());
|
||||
OnlinePlayerCountChanged -= OnOnlinePlayerCountChanged;
|
||||
}
|
||||
else if (newPlayerCount > 0) {
|
||||
onlinePlayersDetected.TrySetResult();
|
||||
OnlinePlayerCountChanged -= OnOnlinePlayerCountChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await onlinePlayersDetected.Task;
|
||||
}
|
||||
|
||||
private void OnOutput(object? sender, string? line) {
|
||||
lock (this) {
|
||||
if (!isDisposed) {
|
||||
serverOutputEvent.Set();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose() {
|
||||
lock (this) {
|
||||
isDisposed = true;
|
||||
playerCounts = null;
|
||||
}
|
||||
|
||||
process.RemoveOutputListener(OnOutput);
|
||||
serverOutputEvent.Dispose();
|
||||
}
|
||||
}
|
@@ -19,6 +19,7 @@ sealed class InstanceRunningState : IDisposable {
|
||||
private readonly CancellationToken cancellationToken;
|
||||
|
||||
private readonly InstanceLogSender logSender;
|
||||
private readonly InstancePlayerCountTracker playerCountTracker;
|
||||
private readonly BackupScheduler backupScheduler;
|
||||
|
||||
private bool isDisposed;
|
||||
@@ -32,8 +33,9 @@ sealed class InstanceRunningState : IDisposable {
|
||||
this.cancellationToken = cancellationToken;
|
||||
|
||||
this.logSender = new InstanceLogSender(context.Services.ControllerConnection, context.InstanceGuid, context.ShortName);
|
||||
this.playerCountTracker = new InstancePlayerCountTracker(context, process, configuration.ServerPort);
|
||||
|
||||
this.backupScheduler = new BackupScheduler(context, process, configuration.ServerPort);
|
||||
this.backupScheduler = new BackupScheduler(context, playerCountTracker);
|
||||
this.backupScheduler.BackupCompleted += OnScheduledBackupCompleted;
|
||||
}
|
||||
|
||||
@@ -93,6 +95,11 @@ sealed class InstanceRunningState : IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
public void OnStopInitiated() {
|
||||
backupScheduler.Stop();
|
||||
playerCountTracker.Stop();
|
||||
}
|
||||
|
||||
private bool TryDispose() {
|
||||
lock (this) {
|
||||
if (isDisposed) {
|
||||
@@ -102,8 +109,8 @@ sealed class InstanceRunningState : IDisposable {
|
||||
isDisposed = true;
|
||||
}
|
||||
|
||||
OnStopInitiated();
|
||||
logSender.Stop();
|
||||
backupScheduler.Stop();
|
||||
|
||||
Process.Dispose();
|
||||
|
||||
|
@@ -25,6 +25,8 @@ static class InstanceStopProcedure {
|
||||
|
||||
try {
|
||||
// Too late to cancel the stop procedure now.
|
||||
runningState.OnStopInitiated();
|
||||
|
||||
if (!process.HasEnded) {
|
||||
context.Logger.Information("Session stopping now.");
|
||||
await DoStop(context, process);
|
||||
@@ -85,7 +87,7 @@ static class InstanceStopProcedure {
|
||||
private static async Task WaitForSessionToEnd(InstanceContext context, InstanceProcess process) {
|
||||
try {
|
||||
await process.WaitForExit(TimeSpan.FromSeconds(55));
|
||||
} catch (OperationCanceledException) {
|
||||
} catch (TimeoutException) {
|
||||
try {
|
||||
context.Logger.Warning("Waiting timed out, killing session...");
|
||||
process.Kill();
|
||||
|
@@ -8,7 +8,6 @@
|
||||
<ItemGroup>
|
||||
<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.Rpc\Phantom.Agent.Rpc.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
20
Agent/Phantom.Agent.Services/Rpc/ControllerConnection.cs
Normal file
20
Agent/Phantom.Agent.Services/Rpc/ControllerConnection.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Phantom.Common.Messages.Agent;
|
||||
using Phantom.Utils.Actor;
|
||||
using Phantom.Utils.Rpc.Runtime;
|
||||
|
||||
namespace Phantom.Agent.Services.Rpc;
|
||||
|
||||
public sealed class ControllerConnection(RpcSendChannel<IMessageToController> sendChannel) {
|
||||
public ValueTask Send<TMessage>(TMessage message, CancellationToken cancellationToken) where TMessage : IMessageToController {
|
||||
return sendChannel.SendMessage(message, cancellationToken);
|
||||
}
|
||||
|
||||
// TODO handle properly
|
||||
public bool TrySend<TMessage>(TMessage message) where TMessage : IMessageToController {
|
||||
return sendChannel.TrySendMessage(message);
|
||||
}
|
||||
|
||||
public Task<TReply> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken cancellationToken) where TMessage : IMessageToController, ICanReply<TReply> {
|
||||
return sendChannel.SendMessage<TMessage, TReply>(message, waitForReplyTime, cancellationToken);
|
||||
}
|
||||
}
|
@@ -1,100 +1,43 @@
|
||||
using Phantom.Agent.Services.Instances;
|
||||
using Phantom.Common.Data.Instance;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Replies;
|
||||
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.Utils.Actor;
|
||||
using Phantom.Utils.Logging;
|
||||
using Phantom.Utils.Rpc.Runtime;
|
||||
using Serilog;
|
||||
|
||||
namespace Phantom.Agent.Services.Rpc;
|
||||
|
||||
public sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToAgent> {
|
||||
private static ILogger Logger { get; } = PhantomLogger.Create<ControllerMessageHandlerActor>();
|
||||
|
||||
public readonly record struct Init(RpcConnectionToServer<IMessageToController> Connection, AgentServices Agent, CancellationTokenSource ShutdownTokenSource);
|
||||
public readonly record struct Init(AgentServices Agent);
|
||||
|
||||
public static Props<IMessageToAgent> Factory(Init init) {
|
||||
return Props<IMessageToAgent>.Create(() => new ControllerMessageHandlerActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume });
|
||||
}
|
||||
|
||||
private readonly RpcConnectionToServer<IMessageToController> connection;
|
||||
private readonly AgentServices agent;
|
||||
private readonly CancellationTokenSource shutdownTokenSource;
|
||||
|
||||
private ControllerMessageHandlerActor(Init init) {
|
||||
this.connection = init.Connection;
|
||||
this.agent = init.Agent;
|
||||
this.shutdownTokenSource = init.ShutdownTokenSource;
|
||||
|
||||
ReceiveAsync<RegisterAgentSuccessMessage>(HandleRegisterAgentSuccess);
|
||||
Receive<RegisterAgentFailureMessage>(HandleRegisterAgentFailure);
|
||||
ReceiveAndReplyLater<ConfigureInstanceMessage, InstanceActionResult<ConfigureInstanceResult>>(HandleConfigureInstance);
|
||||
ReceiveAndReplyLater<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(HandleLaunchInstance);
|
||||
ReceiveAndReplyLater<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(HandleStopInstance);
|
||||
ReceiveAndReplyLater<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(HandleSendCommandToInstance);
|
||||
Receive<ReplyMessage>(HandleReply);
|
||||
ReceiveAndReplyLater<ConfigureInstanceMessage, Result<ConfigureInstanceResult, InstanceActionFailure>>(HandleConfigureInstance);
|
||||
ReceiveAndReplyLater<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(HandleLaunchInstance);
|
||||
ReceiveAndReplyLater<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(HandleStopInstance);
|
||||
ReceiveAndReplyLater<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(HandleSendCommandToInstance);
|
||||
}
|
||||
|
||||
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();
|
||||
private async Task<Result<ConfigureInstanceResult, InstanceActionFailure>> HandleConfigureInstance(ConfigureInstanceMessage message) {
|
||||
return await agent.InstanceManager.Request(new InstanceManagerActor.ConfigureInstanceCommand(message.InstanceGuid, message.Configuration, message.LaunchProperties, message.LaunchNow, AlwaysReportStatus: false));
|
||||
}
|
||||
|
||||
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<InstanceActionResult<ConfigureInstanceResult>> HandleConfigureInstance(ConfigureInstanceMessage message, bool alwaysReportStatus) {
|
||||
return agent.InstanceManager.Request(new InstanceManagerActor.ConfigureInstanceCommand(message.InstanceGuid, message.Configuration, message.LaunchProperties, message.LaunchNow, alwaysReportStatus));
|
||||
}
|
||||
|
||||
private async Task<InstanceActionResult<ConfigureInstanceResult>> HandleConfigureInstance(ConfigureInstanceMessage message) {
|
||||
return await HandleConfigureInstance(message, alwaysReportStatus: false);
|
||||
}
|
||||
|
||||
private async Task<InstanceActionResult<LaunchInstanceResult>> HandleLaunchInstance(LaunchInstanceMessage message) {
|
||||
private async Task<Result<LaunchInstanceResult, InstanceActionFailure>> HandleLaunchInstance(LaunchInstanceMessage message) {
|
||||
return await agent.InstanceManager.Request(new InstanceManagerActor.LaunchInstanceCommand(message.InstanceGuid));
|
||||
}
|
||||
|
||||
private async Task<InstanceActionResult<StopInstanceResult>> HandleStopInstance(StopInstanceMessage message) {
|
||||
private async Task<Result<StopInstanceResult, InstanceActionFailure>> HandleStopInstance(StopInstanceMessage message) {
|
||||
return await agent.InstanceManager.Request(new InstanceManagerActor.StopInstanceCommand(message.InstanceGuid, message.StopStrategy));
|
||||
}
|
||||
|
||||
private async Task<InstanceActionResult<SendCommandToInstanceResult>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) {
|
||||
private async Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) {
|
||||
return await agent.InstanceManager.Request(new InstanceManagerActor.SendCommandToInstanceCommand(message.InstanceGuid, message.Command));
|
||||
}
|
||||
|
||||
private void HandleReply(ReplyMessage message) {
|
||||
connection.Receive(message);
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
using NetMQ;
|
||||
using System.Text;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Utils.Cryptography;
|
||||
using Phantom.Utils.IO;
|
||||
@@ -10,7 +10,7 @@ namespace Phantom.Agent;
|
||||
static class 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) {
|
||||
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)) {
|
||||
Logger.Fatal("Missing agent key file: {AgentKeyFilePath}", agentKeyFilePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Files.RequireMaximumFileSize(agentKeyFilePath, 64);
|
||||
return LoadFromBytes(await File.ReadAllBytesAsync(agentKeyFilePath));
|
||||
Files.RequireMaximumFileSize(agentKeyFilePath, maximumBytes: 64);
|
||||
string[] lines = await File.ReadAllLinesAsync(agentKeyFilePath, Encoding.UTF8);
|
||||
return LoadFromToken(lines[0]);
|
||||
} catch (IOException e) {
|
||||
Logger.Fatal("Error loading agent key from file: {AgentKeyFilePath}", agentKeyFilePath);
|
||||
Logger.Fatal(e.Message);
|
||||
Logger.Fatal("{}", e.Message);
|
||||
return null;
|
||||
} catch (Exception) {
|
||||
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 {
|
||||
return LoadFromBytes(TokenGenerator.DecodeBytes(agentKey));
|
||||
} catch (Exception) {
|
||||
@@ -50,11 +51,9 @@ static class AgentKey {
|
||||
}
|
||||
}
|
||||
|
||||
private static (NetMQCertificate, AuthToken)? LoadFromBytes(byte[] agentKey) {
|
||||
var (publicKey, agentToken) = ConnectionCommonKey.FromBytes(agentKey);
|
||||
var controllerCertificate = NetMQCertificate.FromPublicKey(publicKey);
|
||||
|
||||
private static ConnectionKey? LoadFromBytes(byte[] agentKey) {
|
||||
var connectionKey = ConnectionKey.FromBytes(agentKey);
|
||||
Logger.Information("Loaded agent key.");
|
||||
return (controllerCertificate, agentToken);
|
||||
return connectionKey;
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,5 @@
|
||||
using System.Reflection;
|
||||
using NetMQ;
|
||||
using Phantom.Agent;
|
||||
using Phantom.Agent.Rpc;
|
||||
using Phantom.Agent.Services;
|
||||
using Phantom.Agent.Services.Rpc;
|
||||
using Phantom.Common.Data.Agent;
|
||||
@@ -9,9 +7,9 @@ using Phantom.Common.Messages.Agent;
|
||||
using Phantom.Common.Messages.Agent.ToController;
|
||||
using Phantom.Utils.Actor;
|
||||
using Phantom.Utils.Logging;
|
||||
using Phantom.Utils.Rpc;
|
||||
using Phantom.Utils.Rpc.Sockets;
|
||||
using Phantom.Utils.Rpc.Runtime.Client;
|
||||
using Phantom.Utils.Runtime;
|
||||
using Phantom.Utils.Threading;
|
||||
|
||||
const int ProtocolVersion = 1;
|
||||
|
||||
@@ -48,33 +46,55 @@ try {
|
||||
return 1;
|
||||
}
|
||||
|
||||
var (controllerCertificate, agentToken) = agentKey.Value;
|
||||
var agentInfo = new AgentInfo(agentGuid.Value, agentName, ProtocolVersion, fullVersion, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts);
|
||||
var rpcClientConnectionParameters = new RpcClientConnectionParameters(
|
||||
Host: controllerHost,
|
||||
Port: controllerPort,
|
||||
DistinguishedName: "phantom-controller",
|
||||
CertificateThumbprint: agentKey.Value.CertificateThumbprint,
|
||||
AuthToken: agentKey.Value.AuthToken,
|
||||
SendQueueCapacity: 500,
|
||||
PingInterval: TimeSpan.FromSeconds(10)
|
||||
);
|
||||
|
||||
using var rpcClient = await RpcClient<IMessageToController, IMessageToAgent>.Connect("Controller", rpcClientConnectionParameters, AgentMessageRegistries.Definitions, shutdownCancellationToken);
|
||||
if (rpcClient == null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
var controllerConnection = new ControllerConnection(rpcClient.SendChannel);
|
||||
|
||||
Task? rpcClientListener = null;
|
||||
try {
|
||||
PhantomLogger.Root.InformationHeading("Launching Phantom Panel agent...");
|
||||
|
||||
var rpcConfiguration = new RpcConfiguration("Agent", controllerHost, controllerPort, controllerCertificate);
|
||||
var rpcSocket = RpcClientSocket.Connect(rpcConfiguration, AgentMessageRegistries.Definitions, new RegisterAgentMessage(agentToken, agentInfo));
|
||||
|
||||
var agentServices = new AgentServices(agentInfo, folders, new AgentServiceConfiguration(maxConcurrentBackupCompressionTasks), new ControllerConnection(rpcSocket.Connection));
|
||||
var agentInfo = new AgentInfo(agentGuid.Value, agentName, ProtocolVersion, fullVersion, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts);
|
||||
var agentServices = new AgentServices(agentInfo, folders, new AgentServiceConfiguration(maxConcurrentBackupCompressionTasks), controllerConnection);
|
||||
await agentServices.Initialize();
|
||||
|
||||
var rpcMessageHandlerInit = new ControllerMessageHandlerActor.Init(rpcSocket.Connection, agentServices, shutdownCancellationTokenSource);
|
||||
var rpcMessageHandlerInit = new ControllerMessageHandlerActor.Init(agentServices);
|
||||
var rpcMessageHandlerActor = agentServices.ActorSystem.ActorOf(ControllerMessageHandlerActor.Factory(rpcMessageHandlerInit), "ControllerMessageHandler");
|
||||
|
||||
var rpcDisconnectSemaphore = new SemaphoreSlim(0, 1);
|
||||
var rpcTask = RpcClientRuntime.Launch(rpcSocket, rpcMessageHandlerActor, rpcDisconnectSemaphore, shutdownCancellationToken);
|
||||
try {
|
||||
await rpcTask.WaitAsync(shutdownCancellationToken);
|
||||
} finally {
|
||||
shutdownCancellationTokenSource.Cancel();
|
||||
rpcClientListener = rpcClient.Listen(rpcMessageHandlerActor);
|
||||
|
||||
if (await agentServices.Register(controllerConnection, shutdownCancellationToken)) {
|
||||
PhantomLogger.Root.Information("Phantom Panel agent is ready.");
|
||||
await shutdownCancellationToken.WaitHandle.WaitOneAsync();
|
||||
}
|
||||
|
||||
await agentServices.Shutdown();
|
||||
} finally {
|
||||
try {
|
||||
await controllerConnection.Send(new UnregisterAgentMessage(), CancellationToken.None);
|
||||
// TODO wait for acknowledgment
|
||||
} catch (Exception e) {
|
||||
PhantomLogger.Root.Warning(e, "Could not unregister agent after shutdown.");
|
||||
} finally {
|
||||
await rpcClient.Shutdown();
|
||||
|
||||
rpcDisconnectSemaphore.Release();
|
||||
await rpcTask;
|
||||
rpcDisconnectSemaphore.Dispose();
|
||||
|
||||
NetMQConfig.Cleanup();
|
||||
if (rpcClientListener != null) {
|
||||
await rpcClientListener;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
@@ -8,9 +8,10 @@ public sealed partial record Instance(
|
||||
[property: MemoryPackOrder(0)] Guid InstanceGuid,
|
||||
[property: MemoryPackOrder(1)] InstanceConfiguration Configuration,
|
||||
[property: MemoryPackOrder(2)] IInstanceStatus Status,
|
||||
[property: MemoryPackOrder(3)] bool LaunchAutomatically
|
||||
[property: MemoryPackOrder(3)] InstancePlayerCounts? PlayerCounts,
|
||||
[property: MemoryPackOrder(4)] bool LaunchAutomatically
|
||||
) {
|
||||
public static Instance Offline(Guid instanceGuid, InstanceConfiguration configuration, bool launchAutomatically = false) {
|
||||
return new Instance(instanceGuid, configuration, InstanceStatus.Offline, launchAutomatically);
|
||||
return new Instance(instanceGuid, configuration, InstanceStatus.Offline, PlayerCounts: null, launchAutomatically);
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using MemoryPack;
|
||||
using System.Collections.Immutable;
|
||||
using MemoryPack;
|
||||
|
||||
namespace Phantom.Common.Data.Web.Users;
|
||||
|
||||
@@ -6,5 +7,18 @@ namespace Phantom.Common.Data.Web.Users;
|
||||
public sealed partial record AuthenticatedUserInfo(
|
||||
[property: MemoryPackOrder(0)] Guid Guid,
|
||||
[property: MemoryPackOrder(1)] string Name,
|
||||
[property: MemoryPackOrder(2)] PermissionSet Permissions
|
||||
);
|
||||
[property: MemoryPackOrder(2)] PermissionSet Permissions,
|
||||
[property: MemoryPackOrder(3)] ImmutableHashSet<Guid> ManagedAgentGuids
|
||||
) {
|
||||
public bool CheckPermission(Permission permission) {
|
||||
return Permissions.Check(permission);
|
||||
}
|
||||
|
||||
public bool HasAccessToAgent(Guid agentGuid) {
|
||||
return ManagedAgentGuids.Contains(agentGuid) || Permissions.Check(Permission.ManageAllAgents);
|
||||
}
|
||||
|
||||
public ImmutableHashSet<Guid> FilterAccessibleAgentGuids(ImmutableHashSet<Guid> agentGuids) {
|
||||
return Permissions.Check(Permission.ManageAllAgents) ? agentGuids : agentGuids.Intersect(ManagedAgentGuids);
|
||||
}
|
||||
}
|
||||
|
@@ -6,5 +6,5 @@ namespace Phantom.Common.Data.Web.Users;
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record LogInSuccess(
|
||||
[property: MemoryPackOrder(0)] AuthenticatedUserInfo UserInfo,
|
||||
[property: MemoryPackOrder(1)] ImmutableArray<byte> Token
|
||||
[property: MemoryPackOrder(1)] ImmutableArray<byte> AuthToken
|
||||
);
|
||||
|
@@ -14,6 +14,9 @@ public sealed record Permission(string Id, Permission? Parent) {
|
||||
return Register(id, this);
|
||||
}
|
||||
|
||||
public const string ManageAllAgentsPolicy = "Agents.ManageAll";
|
||||
public static readonly Permission ManageAllAgents = Register(ManageAllAgentsPolicy);
|
||||
|
||||
public const string ViewInstancesPolicy = "Instances.View";
|
||||
public static readonly Permission ViewInstances = Register(ViewInstancesPolicy);
|
||||
|
||||
|
@@ -0,0 +1,5 @@
|
||||
namespace Phantom.Common.Data.Web.Users;
|
||||
|
||||
public enum UserActionFailure {
|
||||
NotAuthorized
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
using MemoryPack;
|
||||
using Phantom.Common.Data.Replies;
|
||||
|
||||
namespace Phantom.Common.Data.Web.Users;
|
||||
|
||||
[MemoryPackable]
|
||||
[MemoryPackUnion(0, typeof(OfUserActionFailure))]
|
||||
[MemoryPackUnion(1, typeof(OfInstanceActionFailure))]
|
||||
public abstract partial record UserInstanceActionFailure {
|
||||
internal UserInstanceActionFailure() {}
|
||||
|
||||
public static implicit operator UserInstanceActionFailure(UserActionFailure failure) {
|
||||
return new OfUserActionFailure(failure);
|
||||
}
|
||||
|
||||
public static implicit operator UserInstanceActionFailure(InstanceActionFailure failure) {
|
||||
return new OfInstanceActionFailure(failure);
|
||||
}
|
||||
}
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record OfUserActionFailure([property: MemoryPackOrder(0)] UserActionFailure Failure) : UserInstanceActionFailure;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record OfInstanceActionFailure([property: MemoryPackOrder(0)] InstanceActionFailure Failure) : UserInstanceActionFailure;
|
@@ -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,15 +1,16 @@
|
||||
namespace Phantom.Common.Data.Backups;
|
||||
|
||||
public enum BackupCreationResultKind : byte {
|
||||
UnknownError,
|
||||
Success,
|
||||
InstanceNotRunning,
|
||||
BackupCancelled,
|
||||
BackupAlreadyRunning,
|
||||
BackupFileAlreadyExists,
|
||||
CouldNotCreateBackupFolder,
|
||||
CouldNotCopyWorldToTemporaryFolder,
|
||||
CouldNotCreateWorldArchive
|
||||
UnknownError = 0,
|
||||
Success = 1,
|
||||
InstanceNotRunning = 2,
|
||||
BackupTimedOut = 3,
|
||||
BackupCancelled = 4,
|
||||
BackupAlreadyRunning = 5,
|
||||
BackupFileAlreadyExists = 6,
|
||||
CouldNotCreateBackupFolder = 7,
|
||||
CouldNotCopyWorldToTemporaryFolder = 8,
|
||||
CouldNotCreateWorldArchive = 9
|
||||
}
|
||||
|
||||
public static class BackupCreationResultSummaryExtensions {
|
||||
|
@@ -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;
|
||||
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);
|
||||
}
|
||||
}
|
@@ -9,9 +9,10 @@ namespace Phantom.Common.Data.Instance;
|
||||
[MemoryPackUnion(3, typeof(InstanceIsDownloading))]
|
||||
[MemoryPackUnion(4, typeof(InstanceIsLaunching))]
|
||||
[MemoryPackUnion(5, typeof(InstanceIsRunning))]
|
||||
[MemoryPackUnion(6, typeof(InstanceIsRestarting))]
|
||||
[MemoryPackUnion(7, typeof(InstanceIsStopping))]
|
||||
[MemoryPackUnion(8, typeof(InstanceIsFailed))]
|
||||
[MemoryPackUnion(6, typeof(InstanceIsBackingUp))]
|
||||
[MemoryPackUnion(7, typeof(InstanceIsRestarting))]
|
||||
[MemoryPackUnion(8, typeof(InstanceIsStopping))]
|
||||
[MemoryPackUnion(9, typeof(InstanceIsFailed))]
|
||||
public partial interface IInstanceStatus {}
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
@@ -32,6 +33,9 @@ public sealed partial record InstanceIsLaunching : IInstanceStatus;
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record InstanceIsRunning : IInstanceStatus;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record InstanceIsBackingUp : IInstanceStatus;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record InstanceIsRestarting : IInstanceStatus;
|
||||
|
||||
@@ -46,6 +50,7 @@ public static class InstanceStatus {
|
||||
public static readonly IInstanceStatus NotRunning = new InstanceIsNotRunning();
|
||||
public static readonly IInstanceStatus Launching = new InstanceIsLaunching();
|
||||
public static readonly IInstanceStatus Running = new InstanceIsRunning();
|
||||
public static readonly IInstanceStatus BackingUp = new InstanceIsBackingUp();
|
||||
public static readonly IInstanceStatus Restarting = new InstanceIsRestarting();
|
||||
public static readonly IInstanceStatus Stopping = new InstanceIsStopping();
|
||||
|
||||
@@ -58,7 +63,7 @@ public static class InstanceStatus {
|
||||
}
|
||||
|
||||
public static bool IsRunning(this IInstanceStatus status) {
|
||||
return status is InstanceIsRunning;
|
||||
return status is InstanceIsRunning or InstanceIsBackingUp;
|
||||
}
|
||||
|
||||
public static bool IsStopping(this IInstanceStatus status) {
|
||||
@@ -70,10 +75,10 @@ public static class InstanceStatus {
|
||||
}
|
||||
|
||||
public static bool CanStop(this IInstanceStatus status) {
|
||||
return status is InstanceIsDownloading or InstanceIsLaunching or InstanceIsRunning;
|
||||
return status.IsRunning() || status.IsLaunching();
|
||||
}
|
||||
|
||||
public static bool CanSendCommand(this IInstanceStatus status) {
|
||||
return status is InstanceIsRunning;
|
||||
return status.IsRunning();
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,9 @@
|
||||
using MemoryPack;
|
||||
|
||||
namespace Phantom.Common.Data.Instance;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public readonly partial record struct InstancePlayerCounts(
|
||||
[property: MemoryPackOrder(0)] int Online,
|
||||
[property: MemoryPackOrder(1)] int Maximum
|
||||
);
|
@@ -11,6 +11,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Utils\Phantom.Utils\Phantom.Utils.csproj" />
|
||||
<ProjectReference Include="..\..\Utils\Phantom.Utils.Rpc\Phantom.Utils.Rpc.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
20
Common/Phantom.Common.Data/Replies/InstanceActionFailure.cs
Normal file
20
Common/Phantom.Common.Data/Replies/InstanceActionFailure.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace Phantom.Common.Data.Replies;
|
||||
|
||||
public enum InstanceActionFailure : byte {
|
||||
AgentDoesNotExist,
|
||||
AgentShuttingDown,
|
||||
AgentIsNotResponding,
|
||||
InstanceDoesNotExist
|
||||
}
|
||||
|
||||
public static class InstanceActionFailureExtensions {
|
||||
public static string ToSentence(this InstanceActionFailure failure) {
|
||||
return failure switch {
|
||||
InstanceActionFailure.AgentDoesNotExist => "Agent does not exist.",
|
||||
InstanceActionFailure.AgentShuttingDown => "Agent is shutting down.",
|
||||
InstanceActionFailure.AgentIsNotResponding => "Agent is not responding.",
|
||||
InstanceActionFailure.InstanceDoesNotExist => "Instance does not exist.",
|
||||
_ => "Unknown error."
|
||||
};
|
||||
}
|
||||
}
|
@@ -1,9 +0,0 @@
|
||||
namespace Phantom.Common.Data.Replies;
|
||||
|
||||
public enum InstanceActionGeneralResult : byte {
|
||||
None,
|
||||
AgentDoesNotExist,
|
||||
AgentShuttingDown,
|
||||
AgentIsNotResponding,
|
||||
InstanceDoesNotExist
|
||||
}
|
@@ -1,42 +0,0 @@
|
||||
using MemoryPack;
|
||||
|
||||
namespace Phantom.Common.Data.Replies;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record InstanceActionResult<T>(
|
||||
[property: MemoryPackOrder(0)] InstanceActionGeneralResult GeneralResult,
|
||||
[property: MemoryPackOrder(1)] T? ConcreteResult
|
||||
) {
|
||||
public bool Is(T? concreteResult) {
|
||||
return GeneralResult == InstanceActionGeneralResult.None && EqualityComparer<T>.Default.Equals(ConcreteResult, concreteResult);
|
||||
}
|
||||
|
||||
public InstanceActionResult<T2> Map<T2>(Func<T, T2> mapper) {
|
||||
return new InstanceActionResult<T2>(GeneralResult, ConcreteResult is not null ? mapper(ConcreteResult) : default);
|
||||
}
|
||||
|
||||
public string ToSentence(Func<T, string> concreteResultToSentence) {
|
||||
return GeneralResult switch {
|
||||
InstanceActionGeneralResult.None => concreteResultToSentence(ConcreteResult!),
|
||||
InstanceActionGeneralResult.AgentDoesNotExist => "Agent does not exist.",
|
||||
InstanceActionGeneralResult.AgentShuttingDown => "Agent is shutting down.",
|
||||
InstanceActionGeneralResult.AgentIsNotResponding => "Agent is not responding.",
|
||||
InstanceActionGeneralResult.InstanceDoesNotExist => "Instance does not exist.",
|
||||
_ => "Unknown result."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class InstanceActionResult {
|
||||
public static InstanceActionResult<T> General<T>(InstanceActionGeneralResult generalResult) {
|
||||
return new InstanceActionResult<T>(generalResult, default);
|
||||
}
|
||||
|
||||
public static InstanceActionResult<T> Concrete<T>(T? concreteResult) {
|
||||
return new InstanceActionResult<T>(InstanceActionGeneralResult.None, concreteResult);
|
||||
}
|
||||
|
||||
public static InstanceActionResult<T> DidNotReplyIfNull<T>(this InstanceActionResult<T>? result) {
|
||||
return result ?? General<T>(InstanceActionGeneralResult.AgentIsNotResponding);
|
||||
}
|
||||
}
|
@@ -1,6 +0,0 @@
|
||||
namespace Phantom.Common.Data.Replies;
|
||||
|
||||
public enum RegisterAgentFailure : byte {
|
||||
ConnectionAlreadyHasAnAgent,
|
||||
InvalidToken
|
||||
}
|
112
Common/Phantom.Common.Data/Result.cs
Normal file
112
Common/Phantom.Common.Data/Result.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using MemoryPack;
|
||||
using Phantom.Utils.Monads;
|
||||
using Phantom.Utils.Result;
|
||||
|
||||
namespace Phantom.Common.Data;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial class Result<TValue, TError> {
|
||||
[MemoryPackOrder(0)]
|
||||
[MemoryPackInclude]
|
||||
private readonly bool hasValue;
|
||||
|
||||
[MemoryPackOrder(1)]
|
||||
[MemoryPackInclude]
|
||||
private readonly TValue? value;
|
||||
|
||||
[MemoryPackOrder(2)]
|
||||
[MemoryPackInclude]
|
||||
private readonly TError? error;
|
||||
|
||||
[MemoryPackIgnore]
|
||||
public TValue Value => hasValue ? value! : throw new InvalidOperationException("Attempted to get value from an error result.");
|
||||
|
||||
[MemoryPackIgnore]
|
||||
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) {
|
||||
this.hasValue = hasValue;
|
||||
this.value = value;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public bool Is(TValue expectedValue) {
|
||||
return hasValue && EqualityComparer<TValue>.Default.Equals(value, expectedValue);
|
||||
}
|
||||
|
||||
public TOutput Into<TOutput>(Func<TValue, TOutput> valueConverter, Func<TError, TOutput> errorConverter) {
|
||||
return hasValue ? valueConverter(value!) : errorConverter(error!);
|
||||
}
|
||||
|
||||
public Result<TValue, TNewError> MapError<TNewError>(Func<TError, TNewError> errorConverter) {
|
||||
return hasValue ? value! : errorConverter(error!);
|
||||
}
|
||||
|
||||
public Utils.Result.Result Variant() {
|
||||
return hasValue ? new Ok<TValue>(Value) : new Err<TError>(Error);
|
||||
}
|
||||
|
||||
public static implicit operator Result<TValue, TError>(TValue value) {
|
||||
return new Result<TValue, TError>(hasValue: true, value, default);
|
||||
}
|
||||
|
||||
public static implicit operator Result<TValue, TError>(TError error) {
|
||||
return new Result<TValue, TError>(hasValue: false, default, error);
|
||||
}
|
||||
|
||||
public static implicit operator bool(Result<TValue, TError> result) {
|
||||
return result.hasValue;
|
||||
}
|
||||
}
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial class Result<TError> {
|
||||
[MemoryPackOrder(0)]
|
||||
[MemoryPackInclude]
|
||||
private readonly bool hasValue;
|
||||
|
||||
[MemoryPackOrder(1)]
|
||||
[MemoryPackInclude]
|
||||
private readonly TError? error;
|
||||
|
||||
[MemoryPackIgnore]
|
||||
public TError Error => !hasValue ? error! : throw new InvalidOperationException("Attempted to get error from a success result.");
|
||||
|
||||
private Result(bool hasValue, TError? error) {
|
||||
this.hasValue = hasValue;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public bool TryGetError([MaybeNullWhen(false)] out TError error) {
|
||||
if (hasValue) {
|
||||
error = default;
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
error = this.error!;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static implicit operator Result<TError>([SuppressMessage("ReSharper", "UnusedParameter.Global")] Result.OkType _) {
|
||||
return new Result<TError>(hasValue: true, default);
|
||||
}
|
||||
|
||||
public static implicit operator Result<TError>(TError error) {
|
||||
return new Result<TError>(hasValue: false, error);
|
||||
}
|
||||
|
||||
public static implicit operator bool(Result<TError> result) {
|
||||
return result.hasValue;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Result {
|
||||
public static OkType Ok { get; } = new ();
|
||||
|
||||
public readonly record struct OkType;
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Messages.Agent.BiDirectional;
|
||||
using System.Collections.Immutable;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Messages.Agent.ToAgent;
|
||||
using Phantom.Common.Messages.Agent.ToController;
|
||||
using Phantom.Utils.Logging;
|
||||
@@ -11,18 +12,15 @@ public static class AgentMessageRegistries {
|
||||
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 IMessageDefinitions<IMessageToAgent, IMessageToController, ReplyMessage> Definitions { get; } = new MessageDefinitions();
|
||||
public static IMessageDefinitions<IMessageToController, IMessageToAgent> Definitions { get; } = new MessageDefinitions();
|
||||
|
||||
static AgentMessageRegistries() {
|
||||
ToAgent.Add<RegisterAgentSuccessMessage>(0);
|
||||
ToAgent.Add<RegisterAgentFailureMessage>(1);
|
||||
ToAgent.Add<ConfigureInstanceMessage, InstanceActionResult<ConfigureInstanceResult>>(2);
|
||||
ToAgent.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(3);
|
||||
ToAgent.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(4);
|
||||
ToAgent.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(5);
|
||||
ToAgent.Add<ReplyMessage>(127);
|
||||
ToAgent.Add<ConfigureInstanceMessage, Result<ConfigureInstanceResult, InstanceActionFailure>>(2);
|
||||
ToAgent.Add<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(3);
|
||||
ToAgent.Add<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(4);
|
||||
ToAgent.Add<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(5);
|
||||
|
||||
ToController.Add<RegisterAgentMessage>(0);
|
||||
ToController.Add<RegisterAgentMessage, ImmutableArray<ConfigureInstanceMessage>>(0);
|
||||
ToController.Add<UnregisterAgentMessage>(1);
|
||||
ToController.Add<AgentIsAliveMessage>(2);
|
||||
ToController.Add<AdvertiseJavaRuntimesMessage>(3);
|
||||
@@ -30,15 +28,11 @@ public static class AgentMessageRegistries {
|
||||
ToController.Add<InstanceOutputMessage>(5);
|
||||
ToController.Add<ReportAgentStatusMessage>(6);
|
||||
ToController.Add<ReportInstanceEventMessage>(7);
|
||||
ToController.Add<ReplyMessage>(127);
|
||||
ToController.Add<ReportInstancePlayerCountsMessage>(8);
|
||||
}
|
||||
|
||||
private sealed class MessageDefinitions : IMessageDefinitions<IMessageToAgent, IMessageToController, ReplyMessage> {
|
||||
private sealed class MessageDefinitions : IMessageDefinitions<IMessageToController, IMessageToAgent> {
|
||||
public MessageRegistry<IMessageToAgent> ToClient => ToAgent;
|
||||
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,10 @@
|
||||
using MemoryPack;
|
||||
|
||||
namespace Phantom.Common.Messages.Agent.Handshake;
|
||||
|
||||
[MemoryPackable]
|
||||
[MemoryPackUnion(tag: 0, typeof(InvalidAuthToken))]
|
||||
public partial interface IAgentHandshakeResult;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record InvalidAuthToken : IAgentHandshakeResult;
|
@@ -1,4 +1,5 @@
|
||||
using MemoryPack;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Instance;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Utils.Actor;
|
||||
@@ -11,4 +12,4 @@ public sealed partial record ConfigureInstanceMessage(
|
||||
[property: MemoryPackOrder(1)] InstanceConfiguration Configuration,
|
||||
[property: MemoryPackOrder(2)] InstanceLaunchProperties LaunchProperties,
|
||||
[property: MemoryPackOrder(3)] bool LaunchNow = false
|
||||
) : IMessageToAgent, ICanReply<InstanceActionResult<ConfigureInstanceResult>>;
|
||||
) : IMessageToAgent, ICanReply<Result<ConfigureInstanceResult, InstanceActionFailure>>;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using MemoryPack;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Utils.Actor;
|
||||
|
||||
@@ -7,4 +8,4 @@ namespace Phantom.Common.Messages.Agent.ToAgent;
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record LaunchInstanceMessage(
|
||||
[property: MemoryPackOrder(0)] Guid InstanceGuid
|
||||
) : IMessageToAgent, ICanReply<InstanceActionResult<LaunchInstanceResult>>;
|
||||
) : IMessageToAgent, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>;
|
||||
|
@@ -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,4 +1,5 @@
|
||||
using MemoryPack;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Utils.Actor;
|
||||
|
||||
@@ -8,4 +9,4 @@ namespace Phantom.Common.Messages.Agent.ToAgent;
|
||||
public sealed partial record SendCommandToInstanceMessage(
|
||||
[property: MemoryPackOrder(0)] Guid InstanceGuid,
|
||||
[property: MemoryPackOrder(1)] string Command
|
||||
) : IMessageToAgent, ICanReply<InstanceActionResult<SendCommandToInstanceResult>>;
|
||||
) : IMessageToAgent, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using MemoryPack;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Minecraft;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Utils.Actor;
|
||||
@@ -9,4 +10,4 @@ namespace Phantom.Common.Messages.Agent.ToAgent;
|
||||
public sealed partial record StopInstanceMessage(
|
||||
[property: MemoryPackOrder(0)] Guid InstanceGuid,
|
||||
[property: MemoryPackOrder(1)] MinecraftStopStrategy StopStrategy
|
||||
) : IMessageToAgent, ICanReply<InstanceActionResult<StopInstanceResult>>;
|
||||
) : IMessageToAgent, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>;
|
||||
|
@@ -1,11 +1,12 @@
|
||||
using MemoryPack;
|
||||
using Phantom.Common.Data;
|
||||
using System.Collections.Immutable;
|
||||
using MemoryPack;
|
||||
using Phantom.Common.Data.Agent;
|
||||
using Phantom.Common.Messages.Agent.ToAgent;
|
||||
using Phantom.Utils.Actor;
|
||||
|
||||
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;
|
||||
[property: MemoryPackOrder(0)] AgentInfo AgentInfo
|
||||
) : IMessageToController, ICanReply<ImmutableArray<ConfigureInstanceMessage>>;
|
||||
|
@@ -0,0 +1,10 @@
|
||||
using MemoryPack;
|
||||
using Phantom.Common.Data.Instance;
|
||||
|
||||
namespace Phantom.Common.Messages.Agent.ToController;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record ReportInstancePlayerCountsMessage(
|
||||
[property: MemoryPackOrder(0)] Guid InstanceGuid,
|
||||
[property: MemoryPackOrder(1)] InstancePlayerCounts? PlayerCounts
|
||||
) : 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,5 +1,6 @@
|
||||
using System.Collections.Immutable;
|
||||
using MemoryPack;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Utils.Actor;
|
||||
|
||||
@@ -7,8 +8,8 @@ namespace Phantom.Common.Messages.Web.ToController;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record ChangeUserRolesMessage(
|
||||
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
|
||||
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
||||
[property: MemoryPackOrder(1)] Guid SubjectUserGuid,
|
||||
[property: MemoryPackOrder(2)] ImmutableHashSet<Guid> AddToRoleGuids,
|
||||
[property: MemoryPackOrder(3)] ImmutableHashSet<Guid> RemoveFromRoleGuids
|
||||
) : IMessageToController, ICanReply<ChangeUserRolesResult>;
|
||||
) : IMessageToController, ICanReply<Result<ChangeUserRolesResult, UserActionFailure>>;
|
||||
|
@@ -1,14 +1,16 @@
|
||||
using MemoryPack;
|
||||
using System.Collections.Immutable;
|
||||
using MemoryPack;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Instance;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Data.Web.Instance;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Utils.Actor;
|
||||
|
||||
namespace Phantom.Common.Messages.Web.ToController;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record CreateOrUpdateInstanceMessage(
|
||||
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
|
||||
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
||||
[property: MemoryPackOrder(1)] Guid InstanceGuid,
|
||||
[property: MemoryPackOrder(2)] InstanceConfiguration Configuration
|
||||
) : IMessageToController, ICanReply<InstanceActionResult<CreateOrUpdateInstanceResult>>;
|
||||
) : IMessageToController, ICanReply<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>;
|
||||
|
@@ -1,4 +1,6 @@
|
||||
using MemoryPack;
|
||||
using System.Collections.Immutable;
|
||||
using MemoryPack;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Utils.Actor;
|
||||
|
||||
@@ -6,7 +8,7 @@ namespace Phantom.Common.Messages.Web.ToController;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record CreateUserMessage(
|
||||
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
|
||||
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
||||
[property: MemoryPackOrder(1)] string Username,
|
||||
[property: MemoryPackOrder(2)] string Password
|
||||
) : IMessageToController, ICanReply<CreateUserResult>;
|
||||
) : IMessageToController, ICanReply<Result<CreateUserResult, UserActionFailure>>;
|
||||
|
@@ -1,4 +1,6 @@
|
||||
using MemoryPack;
|
||||
using System.Collections.Immutable;
|
||||
using MemoryPack;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Utils.Actor;
|
||||
|
||||
@@ -6,6 +8,6 @@ namespace Phantom.Common.Messages.Web.ToController;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record DeleteUserMessage(
|
||||
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
|
||||
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
||||
[property: MemoryPackOrder(1)] Guid SubjectUserGuid
|
||||
) : IMessageToController, ICanReply<DeleteUserResult>;
|
||||
) : IMessageToController, ICanReply<Result<DeleteUserResult, UserActionFailure>>;
|
||||
|
@@ -1,11 +1,14 @@
|
||||
using System.Collections.Immutable;
|
||||
using MemoryPack;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Web.AuditLog;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Utils.Actor;
|
||||
|
||||
namespace Phantom.Common.Messages.Web.ToController;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record GetAuditLogMessage(
|
||||
[property: MemoryPackOrder(0)] int Count
|
||||
) : IMessageToController, ICanReply<ImmutableArray<AuditLogItem>>;
|
||||
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
||||
[property: MemoryPackOrder(1)] int Count
|
||||
) : IMessageToController, ICanReply<Result<ImmutableArray<AuditLogItem>, UserActionFailure>>;
|
||||
|
@@ -9,5 +9,5 @@ namespace Phantom.Common.Messages.Web.ToController;
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record GetAuthenticatedUser(
|
||||
[property: MemoryPackOrder(0)] Guid UserGuid,
|
||||
[property: MemoryPackOrder(1)] ImmutableArray<byte> SessionToken
|
||||
[property: MemoryPackOrder(1)] ImmutableArray<byte> AuthToken
|
||||
) : IMessageToController, ICanReply<Optional<AuthenticatedUserInfo>>;
|
||||
|
@@ -1,11 +1,14 @@
|
||||
using System.Collections.Immutable;
|
||||
using MemoryPack;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Web.EventLog;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Utils.Actor;
|
||||
|
||||
namespace Phantom.Common.Messages.Web.ToController;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record GetEventLogMessage(
|
||||
[property: MemoryPackOrder(0)] int Count
|
||||
) : IMessageToController, ICanReply<ImmutableArray<EventLogItem>>;
|
||||
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
||||
[property: MemoryPackOrder(1)] int Count
|
||||
) : IMessageToController, ICanReply<Result<ImmutableArray<EventLogItem>, UserActionFailure>>;
|
||||
|
@@ -1,12 +1,15 @@
|
||||
using MemoryPack;
|
||||
using System.Collections.Immutable;
|
||||
using MemoryPack;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Utils.Actor;
|
||||
|
||||
namespace Phantom.Common.Messages.Web.ToController;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record LaunchInstanceMessage(
|
||||
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
|
||||
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
||||
[property: MemoryPackOrder(1)] Guid AgentGuid,
|
||||
[property: MemoryPackOrder(2)] Guid InstanceGuid
|
||||
) : IMessageToController, ICanReply<InstanceActionResult<LaunchInstanceResult>>;
|
||||
) : IMessageToController, ICanReply<Result<LaunchInstanceResult, UserInstanceActionFailure>>;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using MemoryPack;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Utils.Actor;
|
||||
|
||||
@@ -8,4 +9,4 @@ namespace Phantom.Common.Messages.Web.ToController;
|
||||
public sealed partial record LogInMessage(
|
||||
[property: MemoryPackOrder(0)] string Username,
|
||||
[property: MemoryPackOrder(1)] string Password
|
||||
) : IMessageToController, ICanReply<LogInSuccess?>;
|
||||
) : IMessageToController, ICanReply<Optional<LogInSuccess>>;
|
||||
|
@@ -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,13 +1,16 @@
|
||||
using MemoryPack;
|
||||
using System.Collections.Immutable;
|
||||
using MemoryPack;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Utils.Actor;
|
||||
|
||||
namespace Phantom.Common.Messages.Web.ToController;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record SendCommandToInstanceMessage(
|
||||
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
|
||||
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
||||
[property: MemoryPackOrder(1)] Guid AgentGuid,
|
||||
[property: MemoryPackOrder(2)] Guid InstanceGuid,
|
||||
[property: MemoryPackOrder(3)] string Command
|
||||
) : IMessageToController, ICanReply<InstanceActionResult<SendCommandToInstanceResult>>;
|
||||
) : IMessageToController, ICanReply<Result<SendCommandToInstanceResult, UserInstanceActionFailure>>;
|
||||
|
@@ -1,14 +1,17 @@
|
||||
using MemoryPack;
|
||||
using System.Collections.Immutable;
|
||||
using MemoryPack;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Minecraft;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Utils.Actor;
|
||||
|
||||
namespace Phantom.Common.Messages.Web.ToController;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record StopInstanceMessage(
|
||||
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
|
||||
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
||||
[property: MemoryPackOrder(1)] Guid AgentGuid,
|
||||
[property: MemoryPackOrder(2)] Guid InstanceGuid,
|
||||
[property: MemoryPackOrder(3)] MinecraftStopStrategy StopStrategy
|
||||
) : IMessageToController, ICanReply<InstanceActionResult<StopInstanceResult>>;
|
||||
) : IMessageToController, ICanReply<Result<StopInstanceResult, UserInstanceActionFailure>>;
|
||||
|
@@ -0,0 +1,8 @@
|
||||
using MemoryPack;
|
||||
|
||||
namespace Phantom.Common.Messages.Web.ToWeb;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record RefreshUserSessionMessage(
|
||||
[property: MemoryPackOrder(0)] Guid UserGuid
|
||||
) : IMessageToWeb;
|
@@ -7,7 +7,6 @@ using Phantom.Common.Data.Web.AuditLog;
|
||||
using Phantom.Common.Data.Web.EventLog;
|
||||
using Phantom.Common.Data.Web.Instance;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Common.Messages.Web.BiDirectional;
|
||||
using Phantom.Common.Messages.Web.ToController;
|
||||
using Phantom.Common.Messages.Web.ToWeb;
|
||||
using Phantom.Utils.Logging;
|
||||
@@ -19,44 +18,37 @@ public static class WebMessageRegistries {
|
||||
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 IMessageDefinitions<IMessageToWeb, IMessageToController, ReplyMessage> Definitions { get; } = new MessageDefinitions();
|
||||
public static IMessageDefinitions<IMessageToController, IMessageToWeb> Definitions { get; } = new MessageDefinitions();
|
||||
|
||||
static WebMessageRegistries() {
|
||||
ToController.Add<RegisterWebMessage>(0);
|
||||
ToController.Add<UnregisterWebMessage>(1);
|
||||
ToController.Add<LogInMessage, LogInSuccess?>(2);
|
||||
ToController.Add<LogInMessage, Optional<LogInSuccess>>(2);
|
||||
ToController.Add<LogOutMessage>(3);
|
||||
ToController.Add<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(4);
|
||||
ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(5);
|
||||
ToController.Add<CreateUserMessage, CreateUserResult>(6);
|
||||
ToController.Add<DeleteUserMessage, DeleteUserResult>(7);
|
||||
ToController.Add<CreateUserMessage, Result<CreateUserResult, UserActionFailure>>(6);
|
||||
ToController.Add<DeleteUserMessage, Result<DeleteUserResult, UserActionFailure>>(7);
|
||||
ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(8);
|
||||
ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(9);
|
||||
ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(10);
|
||||
ToController.Add<ChangeUserRolesMessage, ChangeUserRolesResult>(11);
|
||||
ToController.Add<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(12);
|
||||
ToController.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(13);
|
||||
ToController.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(14);
|
||||
ToController.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(15);
|
||||
ToController.Add<ChangeUserRolesMessage, Result<ChangeUserRolesResult, UserActionFailure>>(11);
|
||||
ToController.Add<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(12);
|
||||
ToController.Add<LaunchInstanceMessage, Result<LaunchInstanceResult, UserInstanceActionFailure>>(13);
|
||||
ToController.Add<StopInstanceMessage, Result<StopInstanceResult, UserInstanceActionFailure>>(14);
|
||||
ToController.Add<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>(15);
|
||||
ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(16);
|
||||
ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(17);
|
||||
ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(18);
|
||||
ToController.Add<GetEventLogMessage, ImmutableArray<EventLogItem>>(19);
|
||||
ToController.Add<ReplyMessage>(127);
|
||||
ToController.Add<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(18);
|
||||
ToController.Add<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(19);
|
||||
|
||||
ToWeb.Add<RegisterWebResultMessage>(0);
|
||||
ToWeb.Add<RefreshAgentsMessage>(1);
|
||||
ToWeb.Add<RefreshInstancesMessage>(2);
|
||||
ToWeb.Add<InstanceOutputMessage>(3);
|
||||
ToWeb.Add<ReplyMessage>(127);
|
||||
ToWeb.Add<RefreshUserSessionMessage>(4);
|
||||
}
|
||||
|
||||
private sealed class MessageDefinitions : IMessageDefinitions<IMessageToWeb, IMessageToController, ReplyMessage> {
|
||||
private sealed class MessageDefinitions : IMessageDefinitions<IMessageToController, IMessageToWeb> {
|
||||
public MessageRegistry<IMessageToWeb> ToClient => ToWeb;
|
||||
public MessageRegistry<IMessageToController> ToServer => ToController;
|
||||
|
||||
public ReplyMessage CreateReplyMessage(uint sequenceId, byte[] serializedReply) {
|
||||
return new ReplyMessage(sequenceId, serializedReply);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
353
Controller/Phantom.Controller.Database.Postgres/Migrations/20240407211636_UserAgentAccess.Designer.cs
generated
Normal file
353
Controller/Phantom.Controller.Database.Postgres/Migrations/20240407211636_UserAgentAccess.Designer.cs
generated
Normal file
@@ -0,0 +1,353 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Phantom.Controller.Database;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Phantom.Controller.Database.Postgres.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240407211636_UserAgentAccess")]
|
||||
partial class UserAgentAccess
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.0")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Phantom.Controller.Database.Entities.AgentEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("AgentGuid")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("BuildVersion")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("MaxInstances")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<ushort>("MaxMemory")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("ProtocolVersion")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("AgentGuid");
|
||||
|
||||
b.ToTable("Agents", "agents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Controller.Database.Entities.AuditLogEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<JsonDocument>("Data")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SubjectType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("UserGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("UtcTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserGuid");
|
||||
|
||||
b.ToTable("AuditLog", "system");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Controller.Database.Entities.EventLogEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("EventGuid")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AgentGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<JsonDocument>("Data")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SubjectType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("UtcTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("EventGuid");
|
||||
|
||||
b.ToTable("EventLog", "system");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Controller.Database.Entities.InstanceEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("InstanceGuid")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AgentGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("InstanceName")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("JavaRuntimeGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("JvmArguments")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("LaunchAutomatically")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<ushort>("MemoryAllocation")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("MinecraftServerKind")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MinecraftVersion")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("RconPort")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ServerPort")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("InstanceGuid");
|
||||
|
||||
b.ToTable("Instances", "agents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Controller.Database.Entities.PermissionEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Permissions", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Controller.Database.Entities.RoleEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("RoleGuid")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("RoleGuid");
|
||||
|
||||
b.ToTable("Roles", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Controller.Database.Entities.RolePermissionEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("RoleGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PermissionId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("RoleGuid", "PermissionId");
|
||||
|
||||
b.HasIndex("PermissionId");
|
||||
|
||||
b.ToTable("RolePermissions", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserAgentAccessEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("UserGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AgentGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("UserGuid", "AgentGuid");
|
||||
|
||||
b.HasIndex("AgentGuid");
|
||||
|
||||
b.ToTable("UserAgentAccess", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("UserGuid")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserGuid");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserPermissionEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("UserGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PermissionId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserGuid", "PermissionId");
|
||||
|
||||
b.HasIndex("PermissionId");
|
||||
|
||||
b.ToTable("UserPermissions", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserRoleEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("UserGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("RoleGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("UserGuid", "RoleGuid");
|
||||
|
||||
b.HasIndex("RoleGuid");
|
||||
|
||||
b.ToTable("UserRoles", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Controller.Database.Entities.AuditLogEntity", b =>
|
||||
{
|
||||
b.HasOne("Phantom.Controller.Database.Entities.UserEntity", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserGuid")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Controller.Database.Entities.RolePermissionEntity", b =>
|
||||
{
|
||||
b.HasOne("Phantom.Controller.Database.Entities.PermissionEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("PermissionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Phantom.Controller.Database.Entities.RoleEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleGuid")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserAgentAccessEntity", b =>
|
||||
{
|
||||
b.HasOne("Phantom.Controller.Database.Entities.AgentEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AgentGuid")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Phantom.Controller.Database.Entities.UserEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserGuid")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserPermissionEntity", b =>
|
||||
{
|
||||
b.HasOne("Phantom.Controller.Database.Entities.PermissionEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("PermissionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Phantom.Controller.Database.Entities.UserEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserGuid")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserRoleEntity", b =>
|
||||
{
|
||||
b.HasOne("Phantom.Controller.Database.Entities.RoleEntity", "Role")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleGuid")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Phantom.Controller.Database.Entities.UserEntity", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserGuid")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Role");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Phantom.Controller.Database.Postgres.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class UserAgentAccess : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserAgentAccess",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
UserGuid = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
AgentGuid = table.Column<Guid>(type: "uuid", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserAgentAccess", x => new { x.UserGuid, x.AgentGuid });
|
||||
table.ForeignKey(
|
||||
name: "FK_UserAgentAccess_Agents_AgentGuid",
|
||||
column: x => x.AgentGuid,
|
||||
principalSchema: "agents",
|
||||
principalTable: "Agents",
|
||||
principalColumn: "AgentGuid",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserAgentAccess_Users_UserGuid",
|
||||
column: x => x.UserGuid,
|
||||
principalSchema: "identity",
|
||||
principalTable: "Users",
|
||||
principalColumn: "UserGuid",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserAgentAccess_AgentGuid",
|
||||
schema: "identity",
|
||||
table: "UserAgentAccess",
|
||||
column: "AgentGuid");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserAgentAccess",
|
||||
schema: "identity");
|
||||
}
|
||||
}
|
||||
}
|
@@ -18,7 +18,7 @@ namespace Phantom.Controller.Database.Postgres.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "7.0.11")
|
||||
.HasAnnotation("ProductVersion", "8.0.0")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
@@ -204,6 +204,21 @@ namespace Phantom.Controller.Database.Postgres.Migrations
|
||||
b.ToTable("RolePermissions", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserAgentAccessEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("UserGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AgentGuid")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("UserGuid", "AgentGuid");
|
||||
|
||||
b.HasIndex("AgentGuid");
|
||||
|
||||
b.ToTable("UserAgentAccess", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("UserGuid")
|
||||
@@ -281,6 +296,21 @@ namespace Phantom.Controller.Database.Postgres.Migrations
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserAgentAccessEntity", b =>
|
||||
{
|
||||
b.HasOne("Phantom.Controller.Database.Entities.AgentEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AgentGuid")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Phantom.Controller.Database.Entities.UserEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserGuid")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserPermissionEntity", b =>
|
||||
{
|
||||
b.HasOne("Phantom.Controller.Database.Entities.PermissionEntity", null)
|
||||
|
@@ -20,6 +20,7 @@ public class ApplicationDbContext : DbContext {
|
||||
public DbSet<UserRoleEntity> UserRoles { get; init; } = null!;
|
||||
public DbSet<UserPermissionEntity> UserPermissions { get; init; } = null!;
|
||||
public DbSet<RolePermissionEntity> RolePermissions { get; init; } = null!;
|
||||
public DbSet<UserAgentAccessEntity> UserAgentAccess { get; init; } = null!;
|
||||
|
||||
public DbSet<AgentEntity> Agents { get; init; } = null!;
|
||||
public DbSet<InstanceEntity> Instances { get; init; } = null!;
|
||||
@@ -62,6 +63,12 @@ public class ApplicationDbContext : DbContext {
|
||||
b.HasOne<RoleEntity>().WithMany().HasForeignKey(static e => e.RoleGuid).IsRequired().OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasOne<PermissionEntity>().WithMany().HasForeignKey(static e => e.PermissionId).IsRequired().OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
builder.Entity<UserAgentAccessEntity>(static b => {
|
||||
b.HasKey(static e => new { UserId = e.UserGuid, AgentId = e.AgentGuid });
|
||||
b.HasOne<UserEntity>().WithMany().HasForeignKey(static e => e.UserGuid).IsRequired().OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasOne<AgentEntity>().WithMany().HasForeignKey(static e => e.AgentGuid).IsRequired().OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void ConfigureConventions(ModelConfigurationBuilder builder) {
|
||||
|
@@ -0,0 +1,14 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Phantom.Controller.Database.Entities;
|
||||
|
||||
[Table("UserAgentAccess", Schema = "identity")]
|
||||
public sealed class UserAgentAccessEntity {
|
||||
public Guid UserGuid { get; init; }
|
||||
public Guid AgentGuid { get; init; }
|
||||
|
||||
public UserAgentAccessEntity(Guid userGuid, Guid agentGuid) {
|
||||
UserGuid = userGuid;
|
||||
AgentGuid = agentGuid;
|
||||
}
|
||||
}
|
@@ -17,11 +17,12 @@ public sealed class EventLogRepository {
|
||||
db.Ctx.EventLog.Add(new EventLogEntity(eventGuid, utcTime, agentGuid, eventType, subjectId, extra));
|
||||
}
|
||||
|
||||
public Task<ImmutableArray<EventLogItem>> GetMostRecentItems(int count, CancellationToken cancellationToken) {
|
||||
public Task<ImmutableArray<EventLogItem>> GetMostRecentItems(ImmutableHashSet<Guid> agentGuids, int count, CancellationToken cancellationToken) {
|
||||
return db.Ctx
|
||||
.EventLog
|
||||
.AsQueryable()
|
||||
.OrderByDescending(static entity => entity.UtcTime)
|
||||
.Where(entity => entity.AgentGuid == null || agentGuids.Contains(entity.AgentGuid.Value))
|
||||
.Take(count)
|
||||
.AsAsyncEnumerable()
|
||||
.Select(static entity => new EventLogItem(entity.UtcTime, entity.AgentGuid, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data?.RootElement.ToString()))
|
||||
|
@@ -0,0 +1,35 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Controller.Database.Entities;
|
||||
using Phantom.Utils.Collections;
|
||||
|
||||
namespace Phantom.Controller.Database.Repositories;
|
||||
|
||||
public sealed class PermissionRepository {
|
||||
private readonly ILazyDbContext db;
|
||||
|
||||
public PermissionRepository(ILazyDbContext db) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
public async Task<PermissionSet> GetAllUserPermissions(UserEntity user) {
|
||||
var userPermissions = db.Ctx.UserPermissions
|
||||
.Where(up => up.UserGuid == user.UserGuid)
|
||||
.Select(static up => up.PermissionId);
|
||||
|
||||
var rolePermissions = db.Ctx.UserRoles
|
||||
.Where(ur => ur.UserGuid == user.UserGuid)
|
||||
.Join(db.Ctx.RolePermissions, static ur => ur.RoleGuid, static rp => rp.RoleGuid, static (ur, rp) => rp.PermissionId);
|
||||
|
||||
return new PermissionSet(await userPermissions.Union(rolePermissions).AsAsyncEnumerable().ToImmutableSetAsync());
|
||||
}
|
||||
|
||||
public Task<ImmutableHashSet<Guid>> GetManagedAgentGuids(UserEntity user) {
|
||||
return db.Ctx.UserAgentAccess
|
||||
.Where(ua => ua.UserGuid == user.UserGuid)
|
||||
.Select(static ua => ua.AgentGuid)
|
||||
.AsAsyncEnumerable()
|
||||
.ToImmutableSetAsync();
|
||||
}
|
||||
}
|
@@ -1,9 +1,9 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Controller.Database.Entities;
|
||||
using Phantom.Utils.Collections;
|
||||
using Phantom.Utils.Tasks;
|
||||
|
||||
namespace Phantom.Controller.Database.Repositories;
|
||||
|
||||
|
@@ -1,12 +1,12 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Common.Data.Web.Users.AddUserErrors;
|
||||
using Phantom.Common.Data.Web.Users.PasswordRequirementViolations;
|
||||
using Phantom.Common.Data.Web.Users.UsernameRequirementViolations;
|
||||
using Phantom.Controller.Database.Entities;
|
||||
using Phantom.Utils.Collections;
|
||||
using Phantom.Utils.Tasks;
|
||||
|
||||
namespace Phantom.Controller.Database.Repositories;
|
||||
|
||||
|
@@ -13,13 +13,15 @@ using Phantom.Common.Data.Web.Minecraft;
|
||||
using Phantom.Common.Messages.Agent;
|
||||
using Phantom.Common.Messages.Agent.ToAgent;
|
||||
using Phantom.Controller.Database;
|
||||
using Phantom.Controller.Database.Entities;
|
||||
using Phantom.Controller.Minecraft;
|
||||
using Phantom.Controller.Services.Instances;
|
||||
using Phantom.Utils.Actor;
|
||||
using Phantom.Utils.Actor.Mailbox;
|
||||
using Phantom.Utils.Actor.Tasks;
|
||||
using Phantom.Utils.Collections;
|
||||
using Phantom.Utils.Logging;
|
||||
using Phantom.Utils.Rpc.Runtime;
|
||||
using Phantom.Utils.Rpc.Runtime.Server;
|
||||
using Serilog;
|
||||
|
||||
namespace Phantom.Controller.Services.Agents;
|
||||
@@ -92,11 +94,12 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
||||
Receive<NotifyIsAliveCommand>(NotifyIsAlive);
|
||||
Receive<UpdateStatsCommand>(UpdateStats);
|
||||
Receive<UpdateJavaRuntimesCommand>(UpdateJavaRuntimes);
|
||||
ReceiveAndReplyLater<CreateOrUpdateInstanceCommand, InstanceActionResult<CreateOrUpdateInstanceResult>>(CreateOrUpdateInstance);
|
||||
ReceiveAndReplyLater<CreateOrUpdateInstanceCommand, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstance);
|
||||
Receive<UpdateInstanceStatusCommand>(UpdateInstanceStatus);
|
||||
ReceiveAndReplyLater<LaunchInstanceCommand, InstanceActionResult<LaunchInstanceResult>>(LaunchInstance);
|
||||
ReceiveAndReplyLater<StopInstanceCommand, InstanceActionResult<StopInstanceResult>>(StopInstance);
|
||||
ReceiveAndReplyLater<SendCommandToInstanceCommand, InstanceActionResult<SendCommandToInstanceResult>>(SendMinecraftCommand);
|
||||
Receive<UpdateInstancePlayerCountsCommand>(UpdateInstancePlayerCounts);
|
||||
ReceiveAndReplyLater<LaunchInstanceCommand, Result<LaunchInstanceResult, InstanceActionFailure>>(LaunchInstance);
|
||||
ReceiveAndReplyLater<StopInstanceCommand, Result<StopInstanceResult, InstanceActionFailure>>(StopInstance);
|
||||
ReceiveAndReplyLater<SendCommandToInstanceCommand, Result<SendCommandToInstanceResult, InstanceActionFailure>>(SendMinecraftCommand);
|
||||
Receive<ReceiveInstanceDataCommand>(ReceiveInstanceData);
|
||||
}
|
||||
|
||||
@@ -144,20 +147,20 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
||||
}
|
||||
}
|
||||
|
||||
private Task<InstanceActionResult<TReply>> RequestInstance<TCommand, TReply>(Guid instanceGuid, TCommand command) where TCommand : InstanceActor.ICommand, ICanReply<InstanceActionResult<TReply>> {
|
||||
private async Task<Result<TReply, InstanceActionFailure>> RequestInstance<TCommand, TReply>(Guid instanceGuid, TCommand command) where TCommand : InstanceActor.ICommand, ICanReply<Result<TReply, InstanceActionFailure>> {
|
||||
if (instanceActorByGuid.TryGetValue(instanceGuid, out var instance)) {
|
||||
return instance.Request(command, cancellationToken);
|
||||
return await instance.Request(command, cancellationToken);
|
||||
}
|
||||
else {
|
||||
Logger.Warning("Could not deliver command {CommandType} to instance {InstanceGuid}, instance not found.", command.GetType().Name, instanceGuid);
|
||||
return Task.FromResult(InstanceActionResult.General<TReply>(InstanceActionGeneralResult.InstanceDoesNotExist));
|
||||
return InstanceActionFailure.InstanceDoesNotExist;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ImmutableArray<ConfigureInstanceMessage>> PrepareInitialConfigurationMessages() {
|
||||
var configurationMessages = ImmutableArray.CreateBuilder<ConfigureInstanceMessage>();
|
||||
|
||||
foreach (var (instanceGuid, instanceConfiguration, _, launchAutomatically) in instanceDataByGuid.Values.ToImmutableArray()) {
|
||||
foreach (var (instanceGuid, instanceConfiguration, _, _, launchAutomatically) in instanceDataByGuid.Values.ToImmutableArray()) {
|
||||
var serverExecutableInfo = await minecraftVersions.GetServerExecutableInfo(instanceConfiguration.MinecraftVersion, cancellationToken);
|
||||
configurationMessages.Add(new ConfigureInstanceMessage(instanceGuid, instanceConfiguration, new InstanceLaunchProperties(serverExecutableInfo), launchAutomatically));
|
||||
}
|
||||
@@ -165,13 +168,13 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
||||
return configurationMessages.ToImmutable();
|
||||
}
|
||||
|
||||
public interface ICommand {}
|
||||
public interface 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, RpcServerToClientConnection<IMessageToController, IMessageToAgent> Connection) : ICommand, ICanReply<ImmutableArray<ConfigureInstanceMessage>>;
|
||||
|
||||
public sealed record UnregisterCommand(RpcConnectionToClient<IMessageToAgent> Connection) : ICommand;
|
||||
public sealed record UnregisterCommand(RpcServerToClientConnection<IMessageToController, IMessageToAgent> Connection) : ICommand;
|
||||
|
||||
private sealed record RefreshConnectionStatusCommand : ICommand;
|
||||
|
||||
@@ -181,34 +184,44 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
||||
|
||||
public sealed record UpdateJavaRuntimesCommand(ImmutableArray<TaggedJavaRuntime> JavaRuntimes) : ICommand;
|
||||
|
||||
public sealed record CreateOrUpdateInstanceCommand(Guid AuditLogUserGuid, Guid InstanceGuid, InstanceConfiguration Configuration) : ICommand, ICanReply<InstanceActionResult<CreateOrUpdateInstanceResult>>;
|
||||
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 LaunchInstanceCommand(Guid InstanceGuid, Guid AuditLogUserGuid) : ICommand, ICanReply<InstanceActionResult<LaunchInstanceResult>>;
|
||||
public sealed record UpdateInstancePlayerCountsCommand(Guid InstanceGuid, InstancePlayerCounts? PlayerCounts) : ICommand;
|
||||
|
||||
public sealed record StopInstanceCommand(Guid InstanceGuid, Guid AuditLogUserGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<InstanceActionResult<StopInstanceResult>>;
|
||||
public sealed record LaunchInstanceCommand(Guid LoggedInUserGuid, Guid InstanceGuid) : ICommand, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>;
|
||||
|
||||
public sealed record SendCommandToInstanceCommand(Guid InstanceGuid, Guid AuditLogUserGuid, string Command) : ICommand, ICanReply<InstanceActionResult<SendCommandToInstanceResult>>;
|
||||
public sealed record StopInstanceCommand(Guid LoggedInUserGuid, Guid InstanceGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>;
|
||||
|
||||
public sealed record SendCommandToInstanceCommand(Guid LoggedInUserGuid, Guid InstanceGuid, string Command) : ICommand, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>;
|
||||
|
||||
public sealed record ReceiveInstanceDataCommand(Instance Instance) : ICommand, IJumpAhead;
|
||||
|
||||
private async Task Initialize(InitializeCommand command) {
|
||||
await using var ctx = dbProvider.Eager();
|
||||
await foreach (var entity in ctx.Instances.Where(instance => instance.AgentGuid == agentGuid).AsAsyncEnumerable().WithCancellation(cancellationToken)) {
|
||||
ImmutableArray<InstanceEntity> instanceEntities;
|
||||
await using (var ctx = dbProvider.Eager()) {
|
||||
instanceEntities = await ctx.Instances.Where(instance => instance.AgentGuid == agentGuid).AsAsyncEnumerable().ToImmutableArrayCatchingExceptionsAsync(OnException, cancellationToken);
|
||||
}
|
||||
|
||||
static void OnException(Exception e) {
|
||||
Logger.Error(e, "Could not load instance from database.");
|
||||
}
|
||||
|
||||
foreach (var instanceEntity in instanceEntities) {
|
||||
var instanceConfiguration = new InstanceConfiguration(
|
||||
entity.AgentGuid,
|
||||
entity.InstanceName,
|
||||
entity.ServerPort,
|
||||
entity.RconPort,
|
||||
entity.MinecraftVersion,
|
||||
entity.MinecraftServerKind,
|
||||
entity.MemoryAllocation,
|
||||
entity.JavaRuntimeGuid,
|
||||
JvmArgumentsHelper.Split(entity.JvmArguments)
|
||||
instanceEntity.AgentGuid,
|
||||
instanceEntity.InstanceName,
|
||||
instanceEntity.ServerPort,
|
||||
instanceEntity.RconPort,
|
||||
instanceEntity.MinecraftVersion,
|
||||
instanceEntity.MinecraftServerKind,
|
||||
instanceEntity.MemoryAllocation,
|
||||
instanceEntity.JavaRuntimeGuid,
|
||||
JvmArgumentsHelper.Split(instanceEntity.JvmArguments)
|
||||
);
|
||||
|
||||
CreateNewInstance(Instance.Offline(entity.InstanceGuid, instanceConfiguration, entity.LaunchAutomatically));
|
||||
CreateNewInstance(Instance.Offline(instanceEntity.InstanceGuid, instanceConfiguration, instanceEntity.LaunchAutomatically));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,15 +283,15 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
||||
controllerState.UpdateAgentJavaRuntimes(agentGuid, javaRuntimes);
|
||||
}
|
||||
|
||||
private Task<InstanceActionResult<CreateOrUpdateInstanceResult>> CreateOrUpdateInstance(CreateOrUpdateInstanceCommand command) {
|
||||
private Task<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>> CreateOrUpdateInstance(CreateOrUpdateInstanceCommand command) {
|
||||
var instanceConfiguration = command.Configuration;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(instanceConfiguration.InstanceName)) {
|
||||
return Task.FromResult(InstanceActionResult.Concrete(CreateOrUpdateInstanceResult.InstanceNameMustNotBeEmpty));
|
||||
return Task.FromResult<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstanceResult.InstanceNameMustNotBeEmpty);
|
||||
}
|
||||
|
||||
if (instanceConfiguration.MemoryAllocation <= RamAllocationUnits.Zero) {
|
||||
return Task.FromResult(InstanceActionResult.Concrete(CreateOrUpdateInstanceResult.InstanceMemoryMustNotBeZero));
|
||||
return Task.FromResult<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstanceResult.InstanceMemoryMustNotBeZero);
|
||||
}
|
||||
|
||||
return minecraftVersions.GetServerExecutableInfo(instanceConfiguration.MinecraftVersion, cancellationToken)
|
||||
@@ -286,9 +299,9 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
||||
.Unwrap();
|
||||
}
|
||||
|
||||
private Task<InstanceActionResult<CreateOrUpdateInstanceResult>> CreateOrUpdateInstance1(FileDownloadInfo? serverExecutableInfo, CreateOrUpdateInstanceCommand command) {
|
||||
private Task<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>> CreateOrUpdateInstance1(FileDownloadInfo? serverExecutableInfo, CreateOrUpdateInstanceCommand command) {
|
||||
if (serverExecutableInfo == null) {
|
||||
return Task.FromResult(InstanceActionResult.Concrete(CreateOrUpdateInstanceResult.MinecraftVersionDownloadInfoNotFound));
|
||||
return Task.FromResult<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstanceResult.MinecraftVersionDownloadInfoNotFound);
|
||||
}
|
||||
|
||||
var instanceConfiguration = command.Configuration;
|
||||
@@ -298,13 +311,13 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
||||
instanceActorRef = CreateNewInstance(Instance.Offline(command.InstanceGuid, instanceConfiguration));
|
||||
}
|
||||
|
||||
var configureInstanceCommand = new InstanceActor.ConfigureInstanceCommand(command.AuditLogUserGuid, command.InstanceGuid, instanceConfiguration, new InstanceLaunchProperties(serverExecutableInfo), isCreatingInstance);
|
||||
var configureInstanceCommand = new InstanceActor.ConfigureInstanceCommand(command.LoggedInUserGuid, command.InstanceGuid, instanceConfiguration, new InstanceLaunchProperties(serverExecutableInfo), isCreatingInstance);
|
||||
|
||||
return instanceActorRef.Request(configureInstanceCommand, cancellationToken)
|
||||
.ContinueOnActor(CreateOrUpdateInstance2, configureInstanceCommand);
|
||||
}
|
||||
|
||||
private InstanceActionResult<CreateOrUpdateInstanceResult> CreateOrUpdateInstance2(InstanceActionResult<ConfigureInstanceResult> result, InstanceActor.ConfigureInstanceCommand command) {
|
||||
private Result<CreateOrUpdateInstanceResult, InstanceActionFailure> CreateOrUpdateInstance2(Result<ConfigureInstanceResult, InstanceActionFailure> result, InstanceActor.ConfigureInstanceCommand command) {
|
||||
var instanceGuid = command.InstanceGuid;
|
||||
var instanceName = command.Configuration.InstanceName;
|
||||
var isCreating = command.IsCreatingInstance;
|
||||
@@ -312,34 +325,40 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
||||
if (result.Is(ConfigureInstanceResult.Success)) {
|
||||
string action = isCreating ? "Added" : "Edited";
|
||||
string relation = isCreating ? "to agent" : "in agent";
|
||||
|
||||
Logger.Information(action + " instance \"{InstanceName}\" (GUID {InstanceGuid}) " + relation + " \"{AgentName}\".", instanceName, instanceGuid, configuration.AgentName);
|
||||
|
||||
return CreateOrUpdateInstanceResult.Success;
|
||||
}
|
||||
else {
|
||||
string action = isCreating ? "adding" : "editing";
|
||||
string relation = isCreating ? "to agent" : "in agent";
|
||||
Logger.Information("Failed " + action + " instance \"{InstanceName}\" (GUID {InstanceGuid}) " + relation + " \"{AgentName}\". {ErrorMessage}", instanceName, instanceGuid, configuration.AgentName, result.ToSentence(ConfigureInstanceResultExtensions.ToSentence));
|
||||
}
|
||||
string reason = result.Into(ConfigureInstanceResultExtensions.ToSentence, InstanceActionFailureExtensions.ToSentence);
|
||||
|
||||
return result.Map(static result => result switch {
|
||||
ConfigureInstanceResult.Success => CreateOrUpdateInstanceResult.Success,
|
||||
_ => CreateOrUpdateInstanceResult.UnknownError
|
||||
});
|
||||
Logger.Information("Failed " + action + " instance \"{InstanceName}\" (GUID {InstanceGuid}) " + relation + " \"{AgentName}\". {ErrorMessage}", instanceName, instanceGuid, configuration.AgentName, reason);
|
||||
|
||||
return CreateOrUpdateInstanceResult.UnknownError;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateInstanceStatus(UpdateInstanceStatusCommand command) {
|
||||
TellInstance(command.InstanceGuid, new InstanceActor.SetStatusCommand(command.Status));
|
||||
}
|
||||
|
||||
private Task<InstanceActionResult<LaunchInstanceResult>> LaunchInstance(LaunchInstanceCommand command) {
|
||||
return RequestInstance<InstanceActor.LaunchInstanceCommand, LaunchInstanceResult>(command.InstanceGuid, new InstanceActor.LaunchInstanceCommand(command.AuditLogUserGuid));
|
||||
private void UpdateInstancePlayerCounts(UpdateInstancePlayerCountsCommand command) {
|
||||
TellInstance(command.InstanceGuid, new InstanceActor.SetPlayerCountsCommand(command.PlayerCounts));
|
||||
}
|
||||
|
||||
private Task<InstanceActionResult<StopInstanceResult>> StopInstance(StopInstanceCommand command) {
|
||||
return RequestInstance<InstanceActor.StopInstanceCommand, StopInstanceResult>(command.InstanceGuid, new InstanceActor.StopInstanceCommand(command.AuditLogUserGuid, command.StopStrategy));
|
||||
private Task<Result<LaunchInstanceResult, InstanceActionFailure>> LaunchInstance(LaunchInstanceCommand command) {
|
||||
return RequestInstance<InstanceActor.LaunchInstanceCommand, LaunchInstanceResult>(command.InstanceGuid, new InstanceActor.LaunchInstanceCommand(command.LoggedInUserGuid));
|
||||
}
|
||||
|
||||
private Task<InstanceActionResult<SendCommandToInstanceResult>> SendMinecraftCommand(SendCommandToInstanceCommand command) {
|
||||
return RequestInstance<InstanceActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(command.InstanceGuid, new InstanceActor.SendCommandToInstanceCommand(command.AuditLogUserGuid, command.Command));
|
||||
private Task<Result<StopInstanceResult, InstanceActionFailure>> StopInstance(StopInstanceCommand command) {
|
||||
return RequestInstance<InstanceActor.StopInstanceCommand, StopInstanceResult>(command.InstanceGuid, new InstanceActor.StopInstanceCommand(command.LoggedInUserGuid, command.StopStrategy));
|
||||
}
|
||||
|
||||
private Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> SendMinecraftCommand(SendCommandToInstanceCommand command) {
|
||||
return RequestInstance<InstanceActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(command.InstanceGuid, new InstanceActor.SendCommandToInstanceCommand(command.LoggedInUserGuid, command.Command));
|
||||
}
|
||||
|
||||
private void ReceiveInstanceData(ReceiveInstanceDataCommand command) {
|
||||
|
@@ -1,36 +1,29 @@
|
||||
using Phantom.Common.Messages.Agent;
|
||||
using Phantom.Utils.Actor;
|
||||
using Phantom.Utils.Logging;
|
||||
using Phantom.Utils.Rpc.Runtime;
|
||||
using Phantom.Utils.Rpc.Runtime.Server;
|
||||
using Serilog;
|
||||
|
||||
namespace Phantom.Controller.Services.Agents;
|
||||
|
||||
sealed class AgentConnection {
|
||||
sealed class AgentConnection(Guid agentGuid, string agentName) {
|
||||
private static readonly ILogger Logger = PhantomLogger.Create<AgentConnection>();
|
||||
|
||||
private readonly Guid agentGuid;
|
||||
private string agentName;
|
||||
private string agentName = agentName;
|
||||
private RpcServerToClientConnection<IMessageToController, IMessageToAgent>? connection;
|
||||
|
||||
private RpcConnectionToClient<IMessageToAgent>? connection;
|
||||
|
||||
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, string newAgentName) {
|
||||
lock (this) {
|
||||
connection?.Close();
|
||||
connection?.CloseSession();
|
||||
connection = newConnection;
|
||||
agentName = newAgentName;
|
||||
}
|
||||
}
|
||||
|
||||
public bool CloseIfSame(RpcConnectionToClient<IMessageToAgent> expected) {
|
||||
public bool CloseIfSame(RpcServerToClientConnection<IMessageToController, IMessageToAgent> expected) {
|
||||
lock (this) {
|
||||
if (connection != null && connection.IsSame(expected)) {
|
||||
connection.Close();
|
||||
connection.CloseSession();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -45,7 +38,7 @@ sealed class AgentConnection {
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return connection.Send(message);
|
||||
return connection.SendChannel.SendMessage(message).AsTask();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,10 +46,10 @@ sealed class AgentConnection {
|
||||
lock (this) {
|
||||
if (connection == null) {
|
||||
LogAgentOffline();
|
||||
return Task.FromResult<TReply?>(default);
|
||||
return Task.FromResult<TReply?>(null);
|
||||
}
|
||||
|
||||
return connection.Send<TMessage, TReply>(message, waitForReplyTime, waitForReplyCancellationToken)!;
|
||||
return connection.SendChannel.SendMessage<TMessage, TReply>(message, waitForReplyTime, waitForReplyCancellationToken)!;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,16 +1,19 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using Akka.Actor;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Agent;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Data.Web.Agent;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Common.Messages.Agent;
|
||||
using Phantom.Common.Messages.Agent.ToAgent;
|
||||
using Phantom.Controller.Database;
|
||||
using Phantom.Controller.Minecraft;
|
||||
using Phantom.Controller.Services.Users.Sessions;
|
||||
using Phantom.Utils.Actor;
|
||||
using Phantom.Utils.Logging;
|
||||
using Phantom.Utils.Rpc.Runtime;
|
||||
using Phantom.Utils.Rpc.Runtime.Server;
|
||||
using Serilog;
|
||||
|
||||
namespace Phantom.Controller.Services.Agents;
|
||||
@@ -19,20 +22,20 @@ sealed class AgentManager {
|
||||
private static readonly ILogger Logger = PhantomLogger.Create<AgentManager>();
|
||||
|
||||
private readonly IActorRefFactory actorSystem;
|
||||
private readonly AuthToken authToken;
|
||||
private readonly ControllerState controllerState;
|
||||
private readonly MinecraftVersions minecraftVersions;
|
||||
private readonly UserLoginManager userLoginManager;
|
||||
private readonly IDbContextProvider dbProvider;
|
||||
private readonly CancellationToken cancellationToken;
|
||||
|
||||
private readonly ConcurrentDictionary<Guid, ActorRef<AgentActor.ICommand>> agentsByGuid = new ();
|
||||
private readonly Func<Guid, AgentConfiguration, ActorRef<AgentActor.ICommand>> addAgentActorFactory;
|
||||
|
||||
public AgentManager(IActorRefFactory actorSystem, AuthToken authToken, ControllerState controllerState, MinecraftVersions minecraftVersions, IDbContextProvider dbProvider, CancellationToken cancellationToken) {
|
||||
public AgentManager(IActorRefFactory actorSystem, ControllerState controllerState, MinecraftVersions minecraftVersions, UserLoginManager userLoginManager, IDbContextProvider dbProvider, CancellationToken cancellationToken) {
|
||||
this.actorSystem = actorSystem;
|
||||
this.authToken = authToken;
|
||||
this.controllerState = controllerState;
|
||||
this.minecraftVersions = minecraftVersions;
|
||||
this.userLoginManager = userLoginManager;
|
||||
this.dbProvider = dbProvider;
|
||||
this.cancellationToken = cancellationToken;
|
||||
|
||||
@@ -58,18 +61,10 @@ sealed class AgentManager {
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> RegisterAgent(AuthToken authToken, AgentInfo agentInfo, RpcConnectionToClient<IMessageToAgent> connection) {
|
||||
if (!this.authToken.FixedTimeEquals(authToken)) {
|
||||
await connection.Send(new RegisterAgentFailureMessage(RegisterAgentFailure.InvalidToken));
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<ImmutableArray<ConfigureInstanceMessage>> RegisterAgent(AgentInfo agentInfo, RpcServerToClientConnection<IMessageToController, IMessageToAgent> connection) {
|
||||
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;
|
||||
return await agentActor.Request(new AgentActor.RegisterCommand(agentProperties, connection), cancellationToken);
|
||||
}
|
||||
|
||||
public bool TellAgent(Guid agentGuid, AgentActor.ICommand command) {
|
||||
@@ -83,12 +78,18 @@ sealed class AgentManager {
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<InstanceActionResult<TReply>> DoInstanceAction<TCommand, TReply>(Guid agentGuid, TCommand command) where TCommand : class, AgentActor.ICommand, ICanReply<InstanceActionResult<TReply>> {
|
||||
if (agentsByGuid.TryGetValue(agentGuid, out var agent)) {
|
||||
return await agent.Request(command, cancellationToken);
|
||||
public async Task<Result<TReply, UserInstanceActionFailure>> DoInstanceAction<TCommand, TReply>(Permission requiredPermission, ImmutableArray<byte> authToken, Guid agentGuid, Func<Guid, TCommand> commandFactoryFromLoggedInUserGuid) where TCommand : class, AgentActor.ICommand, ICanReply<Result<TReply, InstanceActionFailure>> {
|
||||
var loggedInUser = userLoginManager.GetLoggedInUser(authToken);
|
||||
if (!loggedInUser.HasAccessToAgent(agentGuid) || !loggedInUser.CheckPermission(requiredPermission)) {
|
||||
return (UserInstanceActionFailure) UserActionFailure.NotAuthorized;
|
||||
}
|
||||
else {
|
||||
return InstanceActionResult.General<TReply>(InstanceActionGeneralResult.AgentDoesNotExist);
|
||||
|
||||
if (!agentsByGuid.TryGetValue(agentGuid, out var agent)) {
|
||||
return (UserInstanceActionFailure) InstanceActionFailure.AgentDoesNotExist;
|
||||
}
|
||||
|
||||
var command = commandFactoryFromLoggedInUserGuid(loggedInUser.Guid!.Value);
|
||||
var result = await agent.Request(command, cancellationToken);
|
||||
return result.MapError(static error => (UserInstanceActionFailure) error);
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,4 @@
|
||||
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.Minecraft;
|
||||
using Phantom.Controller.Services.Agents;
|
||||
@@ -11,10 +6,11 @@ using Phantom.Controller.Services.Events;
|
||||
using Phantom.Controller.Services.Instances;
|
||||
using Phantom.Controller.Services.Rpc;
|
||||
using Phantom.Controller.Services.Users;
|
||||
using Phantom.Controller.Services.Users.Sessions;
|
||||
using Phantom.Utils.Actor;
|
||||
using Phantom.Utils.Rpc.Runtime;
|
||||
using IMessageFromAgentToController = Phantom.Common.Messages.Agent.IMessageToController;
|
||||
using IMessageFromWebToController = Phantom.Common.Messages.Web.IMessageToController;
|
||||
using Phantom.Utils.Rpc;
|
||||
using IRpcAgentRegistrar = Phantom.Utils.Rpc.Runtime.Server.IRpcServerClientRegistrar<Phantom.Common.Messages.Agent.IMessageToController, Phantom.Common.Messages.Agent.IMessageToAgent>;
|
||||
using IRpcWebRegistrar = Phantom.Utils.Rpc.Runtime.Server.IRpcServerClientRegistrar<Phantom.Common.Messages.Web.IMessageToController, Phantom.Common.Messages.Web.IMessageToWeb>;
|
||||
|
||||
namespace Phantom.Controller.Services;
|
||||
|
||||
@@ -24,25 +20,26 @@ public sealed class ControllerServices : IDisposable {
|
||||
private ControllerState ControllerState { get; }
|
||||
private MinecraftVersions MinecraftVersions { get; }
|
||||
|
||||
private AgentManager AgentManager { get; }
|
||||
private InstanceLogManager InstanceLogManager { get; }
|
||||
private EventLogManager EventLogManager { get; }
|
||||
|
||||
private AuthenticatedUserCache AuthenticatedUserCache { get; }
|
||||
private UserManager UserManager { get; }
|
||||
private RoleManager RoleManager { get; }
|
||||
private PermissionManager PermissionManager { get; }
|
||||
|
||||
private UserRoleManager UserRoleManager { get; }
|
||||
private UserLoginManager UserLoginManager { get; }
|
||||
private AuditLogManager AuditLogManager { get; }
|
||||
private PermissionManager PermissionManager { get; }
|
||||
|
||||
public IRegistrationHandler<IMessageToAgent, IMessageFromAgentToController, RegisterAgentMessage> AgentRegistrationHandler { get; }
|
||||
public IRegistrationHandler<IMessageToWeb, IMessageFromWebToController, RegisterWebMessage> WebRegistrationHandler { get; }
|
||||
private AgentManager AgentManager { get; }
|
||||
private InstanceLogManager InstanceLogManager { get; }
|
||||
|
||||
private AuditLogManager AuditLogManager { get; }
|
||||
private EventLogManager EventLogManager { get; }
|
||||
|
||||
public IRpcAgentRegistrar AgentRegistrar { get; }
|
||||
public IRpcWebRegistrar WebRegistrar { get; }
|
||||
|
||||
private readonly IDbContextProvider dbProvider;
|
||||
private readonly CancellationToken cancellationToken;
|
||||
|
||||
public ControllerServices(IDbContextProvider dbProvider, AuthToken agentAuthToken, AuthToken webAuthToken, CancellationToken shutdownCancellationToken) {
|
||||
public ControllerServices(IDbContextProvider dbProvider, AuthToken agentAuthToken, CancellationToken shutdownCancellationToken) {
|
||||
this.dbProvider = dbProvider;
|
||||
this.cancellationToken = shutdownCancellationToken;
|
||||
|
||||
@@ -51,20 +48,21 @@ public sealed class ControllerServices : IDisposable {
|
||||
this.ControllerState = new ControllerState();
|
||||
this.MinecraftVersions = new MinecraftVersions();
|
||||
|
||||
this.AgentManager = new AgentManager(ActorSystem, agentAuthToken, ControllerState, MinecraftVersions, dbProvider, cancellationToken);
|
||||
this.InstanceLogManager = new InstanceLogManager();
|
||||
|
||||
this.UserManager = new UserManager(dbProvider);
|
||||
this.AuthenticatedUserCache = new AuthenticatedUserCache();
|
||||
this.UserManager = new UserManager(AuthenticatedUserCache, ControllerState, dbProvider);
|
||||
this.RoleManager = new RoleManager(dbProvider);
|
||||
this.UserRoleManager = new UserRoleManager(AuthenticatedUserCache, ControllerState, dbProvider);
|
||||
this.UserLoginManager = new UserLoginManager(AuthenticatedUserCache, UserManager, dbProvider);
|
||||
this.PermissionManager = new PermissionManager(dbProvider);
|
||||
|
||||
this.UserRoleManager = new UserRoleManager(dbProvider);
|
||||
this.UserLoginManager = new UserLoginManager(UserManager, PermissionManager, dbProvider);
|
||||
this.AuditLogManager = new AuditLogManager(dbProvider);
|
||||
this.EventLogManager = new EventLogManager(ActorSystem, dbProvider, shutdownCancellationToken);
|
||||
this.AgentManager = new AgentManager(ActorSystem, ControllerState, MinecraftVersions, UserLoginManager, dbProvider, cancellationToken);
|
||||
this.InstanceLogManager = new InstanceLogManager();
|
||||
|
||||
this.AgentRegistrationHandler = new AgentRegistrationHandler(AgentManager, InstanceLogManager, EventLogManager);
|
||||
this.WebRegistrationHandler = new WebRegistrationHandler(webAuthToken, ControllerState, InstanceLogManager, UserManager, RoleManager, UserRoleManager, UserLoginManager, AuditLogManager, AgentManager, MinecraftVersions, EventLogManager);
|
||||
this.AuditLogManager = new AuditLogManager(dbProvider);
|
||||
this.EventLogManager = new EventLogManager(ControllerState, ActorSystem, dbProvider, shutdownCancellationToken);
|
||||
|
||||
this.AgentRegistrar = new AgentClientRegistrar(ActorSystem, AgentManager, InstanceLogManager, EventLogManager);
|
||||
this.WebRegistrar = new WebClientRegistrar(ActorSystem, ControllerState, InstanceLogManager, UserManager, RoleManager, UserRoleManager, UserLoginManager, AuditLogManager, AgentManager, MinecraftVersions, EventLogManager);
|
||||
}
|
||||
|
||||
public async Task Initialize() {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user