mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2025-09-16 18:24:48 +02:00
Compare commits
4 Commits
wip-forge
...
820852d096
Author | SHA1 | Date | |
---|---|---|---|
820852d096
|
|||
94c0af9f64
|
|||
374bcd21f4
|
|||
68459461c4
|
@@ -3,7 +3,7 @@
|
|||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"dotnet-ef": {
|
"dotnet-ef": {
|
||||||
"version": "8.0.3",
|
"version": "7.0.0-rc.1.22426.7",
|
||||||
"commands": [
|
"commands": [
|
||||||
"dotnet-ef"
|
"dotnet-ef"
|
||||||
]
|
]
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
using Phantom.Utils.Collections;
|
using Phantom.Utils.Collections;
|
||||||
using Phantom.Utils.Processes;
|
using Phantom.Utils.Processes;
|
||||||
using Phantom.Utils.Tasks;
|
|
||||||
|
|
||||||
namespace Phantom.Agent.Minecraft.Instance;
|
namespace Phantom.Agent.Minecraft.Instance;
|
||||||
|
|
||||||
@@ -14,7 +13,6 @@ public sealed class InstanceProcess : IDisposable {
|
|||||||
public bool HasEnded { get; private set; }
|
public bool HasEnded { get; private set; }
|
||||||
|
|
||||||
private readonly Process process;
|
private readonly Process process;
|
||||||
private readonly TaskCompletionSource processExited = AsyncTasks.CreateCompletionSource();
|
|
||||||
|
|
||||||
internal InstanceProcess(InstanceProperties instanceProperties, Process process) {
|
internal InstanceProcess(InstanceProperties instanceProperties, Process process) {
|
||||||
this.InstanceProperties = instanceProperties;
|
this.InstanceProperties = instanceProperties;
|
||||||
@@ -48,15 +46,16 @@ public sealed class InstanceProcess : IDisposable {
|
|||||||
OutputEvent = null;
|
OutputEvent = null;
|
||||||
HasEnded = true;
|
HasEnded = true;
|
||||||
Ended?.Invoke(this, EventArgs.Empty);
|
Ended?.Invoke(this, EventArgs.Empty);
|
||||||
processExited.SetResult();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Kill() {
|
public void Kill() {
|
||||||
process.Kill(true);
|
process.Kill(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task WaitForExit(TimeSpan timeout) {
|
public async Task WaitForExit(CancellationToken cancellationToken) {
|
||||||
await processExited.Task.WaitAsync(timeout);
|
if (!HasEnded) {
|
||||||
|
await process.WaitForExitAsync(cancellationToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Text;
|
||||||
using System.Text;
|
|
||||||
using Phantom.Agent.Minecraft.Instance;
|
using Phantom.Agent.Minecraft.Instance;
|
||||||
using Phantom.Agent.Minecraft.Java;
|
using Phantom.Agent.Minecraft.Java;
|
||||||
using Phantom.Agent.Minecraft.Server;
|
using Phantom.Agent.Minecraft.Server;
|
||||||
@@ -12,7 +11,6 @@ public abstract class BaseLauncher : IServerLauncher {
|
|||||||
private readonly InstanceProperties instanceProperties;
|
private readonly InstanceProperties instanceProperties;
|
||||||
|
|
||||||
protected string MinecraftVersion => instanceProperties.ServerVersion;
|
protected string MinecraftVersion => instanceProperties.ServerVersion;
|
||||||
protected string InstanceFolder => instanceProperties.InstanceFolder;
|
|
||||||
|
|
||||||
private protected BaseLauncher(InstanceProperties instanceProperties) {
|
private protected BaseLauncher(InstanceProperties instanceProperties) {
|
||||||
this.instanceProperties = instanceProperties;
|
this.instanceProperties = instanceProperties;
|
||||||
@@ -53,14 +51,16 @@ public abstract class BaseLauncher : IServerLauncher {
|
|||||||
|
|
||||||
var processConfigurator = new ProcessConfigurator {
|
var processConfigurator = new ProcessConfigurator {
|
||||||
FileName = javaRuntimeExecutable.ExecutablePath,
|
FileName = javaRuntimeExecutable.ExecutablePath,
|
||||||
WorkingDirectory = InstanceFolder,
|
WorkingDirectory = instanceProperties.InstanceFolder,
|
||||||
RedirectInput = true,
|
RedirectInput = true,
|
||||||
UseShellExecute = false
|
UseShellExecute = false
|
||||||
};
|
};
|
||||||
|
|
||||||
var processArguments = processConfigurator.ArgumentList;
|
var processArguments = processConfigurator.ArgumentList;
|
||||||
PrepareJvmArguments(serverJar).Build(processArguments);
|
PrepareJvmArguments(serverJar).Build(processArguments);
|
||||||
PrepareJavaProcessArguments(processArguments, serverJar.FilePath);
|
processArguments.Add("-jar");
|
||||||
|
processArguments.Add(serverJar.FilePath);
|
||||||
|
processArguments.Add("nogui");
|
||||||
|
|
||||||
var process = processConfigurator.CreateProcess();
|
var process = processConfigurator.CreateProcess();
|
||||||
var instanceProcess = new InstanceProcess(instanceProperties, process);
|
var instanceProcess = new InstanceProcess(instanceProperties, process);
|
||||||
@@ -99,12 +99,6 @@ public abstract class BaseLauncher : IServerLauncher {
|
|||||||
|
|
||||||
private protected virtual void CustomizeJvmArguments(JvmArgumentBuilder arguments) {}
|
private protected virtual void CustomizeJvmArguments(JvmArgumentBuilder arguments) {}
|
||||||
|
|
||||||
protected virtual void PrepareJavaProcessArguments(Collection<string> processArguments, string serverJarFilePath) {
|
|
||||||
processArguments.Add("-jar");
|
|
||||||
processArguments.Add(serverJarFilePath);
|
|
||||||
processArguments.Add("nogui");
|
|
||||||
}
|
|
||||||
|
|
||||||
private protected virtual Task<ServerJarInfo> PrepareServerJar(ILogger logger, string serverJarPath, CancellationToken cancellationToken) {
|
private protected virtual Task<ServerJarInfo> PrepareServerJar(ILogger logger, string serverJarPath, CancellationToken cancellationToken) {
|
||||||
return Task.FromResult(new ServerJarInfo(serverJarPath));
|
return Task.FromResult(new ServerJarInfo(serverJarPath));
|
||||||
}
|
}
|
||||||
|
@@ -1,29 +0,0 @@
|
|||||||
using System.Collections.ObjectModel;
|
|
||||||
using Phantom.Agent.Minecraft.Instance;
|
|
||||||
using Phantom.Agent.Minecraft.Java;
|
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
namespace Phantom.Agent.Minecraft.Launcher.Types;
|
|
||||||
|
|
||||||
public sealed class ForgeLauncher : BaseLauncher {
|
|
||||||
public ForgeLauncher(InstanceProperties instanceProperties) : base(instanceProperties) {}
|
|
||||||
|
|
||||||
private protected override void CustomizeJvmArguments(JvmArgumentBuilder arguments) {
|
|
||||||
arguments.AddProperty("terminal.ansi", "true"); // TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void PrepareJavaProcessArguments(Collection<string> processArguments, string serverJarFilePath) {
|
|
||||||
if (OperatingSystem.IsWindows()) {
|
|
||||||
processArguments.Add("@libraries/net/minecraftforge/forge/1.20.1-47.2.0/win_args.txt");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
processArguments.Add("@libraries/net/minecraftforge/forge/1.20.1-47.2.0/unix_args.txt");
|
|
||||||
}
|
|
||||||
|
|
||||||
processArguments.Add("nogui");
|
|
||||||
}
|
|
||||||
|
|
||||||
private protected override Task<ServerJarInfo> PrepareServerJar(ILogger logger, string serverJarPath, CancellationToken cancellationToken) {
|
|
||||||
return Task.FromResult(new ServerJarInfo(Path.Combine(InstanceFolder, "run.sh")));
|
|
||||||
}
|
|
||||||
}
|
|
@@ -18,5 +18,4 @@ static class MinecraftServerProperties {
|
|||||||
public static readonly MinecraftServerProperty<ushort> ServerPort = new UnsignedShort("server-port");
|
public static readonly MinecraftServerProperty<ushort> ServerPort = new UnsignedShort("server-port");
|
||||||
public static readonly MinecraftServerProperty<ushort> RconPort = new UnsignedShort("rcon.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> EnableRcon = new Boolean("enable-rcon");
|
||||||
public static readonly MinecraftServerProperty<bool> SyncChunkWrites = new Boolean("sync-chunk-writes");
|
|
||||||
}
|
}
|
||||||
|
@@ -5,13 +5,11 @@ namespace Phantom.Agent.Minecraft.Properties;
|
|||||||
public sealed record ServerProperties(
|
public sealed record ServerProperties(
|
||||||
ushort ServerPort,
|
ushort ServerPort,
|
||||||
ushort RconPort,
|
ushort RconPort,
|
||||||
bool EnableRcon = true,
|
bool EnableRcon = true
|
||||||
bool SyncChunkWrites = false
|
|
||||||
) {
|
) {
|
||||||
internal void SetTo(JavaPropertiesFileEditor properties) {
|
internal void SetTo(JavaPropertiesFileEditor properties) {
|
||||||
MinecraftServerProperties.ServerPort.Set(properties, ServerPort);
|
MinecraftServerProperties.ServerPort.Set(properties, ServerPort);
|
||||||
MinecraftServerProperties.RconPort.Set(properties, RconPort);
|
MinecraftServerProperties.RconPort.Set(properties, RconPort);
|
||||||
MinecraftServerProperties.EnableRcon.Set(properties, EnableRcon);
|
MinecraftServerProperties.EnableRcon.Set(properties, EnableRcon);
|
||||||
MinecraftServerProperties.SyncChunkWrites.Set(properties, SyncChunkWrites);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,47 +16,32 @@ sealed class MinecraftServerExecutableDownloader {
|
|||||||
public event EventHandler? Completed;
|
public event EventHandler? Completed;
|
||||||
|
|
||||||
private readonly CancellationTokenSource cancellationTokenSource = new ();
|
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) {
|
public MinecraftServerExecutableDownloader(FileDownloadInfo fileDownloadInfo, string minecraftVersion, string filePath, MinecraftServerExecutableDownloadListener listener) {
|
||||||
Register(listener);
|
Register(listener);
|
||||||
Task = DownloadAndGetPath(fileDownloadInfo, minecraftVersion, filePath, new DownloadProgressCallback(this), cancellationTokenSource.Token);
|
Task = DownloadAndGetPath(fileDownloadInfo, minecraftVersion, filePath);
|
||||||
Task.ContinueWith(OnCompleted, TaskScheduler.Default);
|
Task.ContinueWith(OnCompleted, TaskScheduler.Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Register(MinecraftServerExecutableDownloadListener listener) {
|
public void Register(MinecraftServerExecutableDownloadListener listener) {
|
||||||
int newListenerCount;
|
++listeners;
|
||||||
|
Logger.Debug("Registered download listener, current listener count: {Listeners}", listeners);
|
||||||
|
|
||||||
lock (this) {
|
DownloadProgress += listener.DownloadProgressEventHandler;
|
||||||
newListenerCount = ++listenerCount;
|
listener.CancellationToken.Register(Unregister, listener);
|
||||||
|
|
||||||
DownloadProgress += listener.DownloadProgressEventHandler;
|
|
||||||
listenerCancellationRegistrations.Add(listener.CancellationToken.Register(Unregister, listener));
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.Debug("Registered download listener, current listener count: {Listeners}", newListenerCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Unregister(object? listenerObject) {
|
private void Unregister(object? listenerObject) {
|
||||||
int newListenerCount;
|
MinecraftServerExecutableDownloadListener listener = (MinecraftServerExecutableDownloadListener) listenerObject!;
|
||||||
|
DownloadProgress -= listener.DownloadProgressEventHandler;
|
||||||
lock (this) {
|
|
||||||
MinecraftServerExecutableDownloadListener listener = (MinecraftServerExecutableDownloadListener) listenerObject!;
|
|
||||||
DownloadProgress -= listener.DownloadProgressEventHandler;
|
|
||||||
|
|
||||||
newListenerCount = --listenerCount;
|
if (--listeners <= 0) {
|
||||||
if (newListenerCount <= 0) {
|
|
||||||
cancellationTokenSource.Cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newListenerCount <= 0) {
|
|
||||||
Logger.Debug("Unregistered last download listener, cancelling download.");
|
Logger.Debug("Unregistered last download listener, cancelling download.");
|
||||||
|
cancellationTokenSource.Cancel();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Logger.Debug("Unregistered download listener, current listener count: {Listeners}", newListenerCount);
|
Logger.Debug("Unregistered download listener, current listener count: {Listeners}", listeners);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,19 +51,9 @@ sealed class MinecraftServerExecutableDownloader {
|
|||||||
|
|
||||||
private void OnCompleted(Task task) {
|
private void OnCompleted(Task task) {
|
||||||
Logger.Debug("Download task completed.");
|
Logger.Debug("Download task completed.");
|
||||||
|
Completed?.Invoke(this, EventArgs.Empty);
|
||||||
lock (this) {
|
Completed = null;
|
||||||
Completed?.Invoke(this, EventArgs.Empty);
|
DownloadProgress = null;
|
||||||
Completed = null;
|
|
||||||
DownloadProgress = null;
|
|
||||||
|
|
||||||
foreach (var registration in listenerCancellationRegistrations) {
|
|
||||||
registration.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
listenerCancellationRegistrations.Clear();
|
|
||||||
cancellationTokenSource.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class DownloadProgressCallback {
|
private sealed class DownloadProgressCallback {
|
||||||
@@ -93,14 +68,15 @@ sealed class MinecraftServerExecutableDownloader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<string?> DownloadAndGetPath(FileDownloadInfo fileDownloadInfo, string minecraftVersion, string filePath, DownloadProgressCallback progressCallback, CancellationToken cancellationToken) {
|
private async Task<string?> DownloadAndGetPath(FileDownloadInfo fileDownloadInfo, string minecraftVersion, string filePath) {
|
||||||
string tmpFilePath = filePath + ".tmp";
|
string tmpFilePath = filePath + ".tmp";
|
||||||
|
|
||||||
|
var cancellationToken = cancellationTokenSource.Token;
|
||||||
try {
|
try {
|
||||||
Logger.Information("Downloading server version {Version} from: {Url} ({Size})", minecraftVersion, fileDownloadInfo.DownloadUrl, fileDownloadInfo.Size.ToHumanReadable(decimalPlaces: 1));
|
Logger.Information("Downloading server version {Version} from: {Url} ({Size})", minecraftVersion, fileDownloadInfo.DownloadUrl, fileDownloadInfo.Size.ToHumanReadable(decimalPlaces: 1));
|
||||||
try {
|
try {
|
||||||
using var http = new HttpClient();
|
using var http = new HttpClient();
|
||||||
await FetchServerExecutableFile(http, progressCallback, fileDownloadInfo, tmpFilePath, cancellationToken);
|
await FetchServerExecutableFile(http, new DownloadProgressCallback(this), fileDownloadInfo, tmpFilePath, cancellationToken);
|
||||||
} catch (Exception) {
|
} catch (Exception) {
|
||||||
TryDeleteExecutableAfterFailure(tmpFilePath);
|
TryDeleteExecutableAfterFailure(tmpFilePath);
|
||||||
throw;
|
throw;
|
||||||
@@ -118,6 +94,8 @@ sealed class MinecraftServerExecutableDownloader {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Logger.Error(e, "An unexpected error occurred.");
|
Logger.Error(e, "An unexpected error occurred.");
|
||||||
return null;
|
return null;
|
||||||
|
} finally {
|
||||||
|
cancellationTokenSource.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,12 +3,28 @@ using System.Buffers.Binary;
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Phantom.Common.Data.Instance;
|
using Phantom.Utils.Logging;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
namespace Phantom.Agent.Minecraft.Server;
|
namespace Phantom.Agent.Minecraft.Server;
|
||||||
|
|
||||||
public static class ServerStatusProtocol {
|
public sealed class ServerStatusProtocol {
|
||||||
public static async Task<InstancePlayerCounts> GetPlayerCounts(ushort serverPort, CancellationToken cancellationToken) {
|
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) {
|
||||||
using var tcpClient = new TcpClient();
|
using var tcpClient = new TcpClient();
|
||||||
await tcpClient.ConnectAsync(IPAddress.Loopback, serverPort, cancellationToken);
|
await tcpClient.ConnectAsync(IPAddress.Loopback, serverPort, cancellationToken);
|
||||||
var tcpStream = tcpClient.GetStream();
|
var tcpStream = tcpClient.GetStream();
|
||||||
@@ -17,22 +33,24 @@ public static class ServerStatusProtocol {
|
|||||||
tcpStream.WriteByte(0xFE);
|
tcpStream.WriteByte(0xFE);
|
||||||
await tcpStream.FlushAsync(cancellationToken);
|
await tcpStream.FlushAsync(cancellationToken);
|
||||||
|
|
||||||
short messageLength = await ReadStreamHeader(tcpStream, cancellationToken);
|
short? messageLength = await ReadStreamHeader(tcpStream, cancellationToken);
|
||||||
return await ReadPlayerCounts(tcpStream, messageLength * 2, cancellationToken);
|
return messageLength == null ? null : await ReadOnlinePlayerCount(tcpStream, messageLength.Value * 2, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<short> ReadStreamHeader(NetworkStream tcpStream, CancellationToken cancellationToken) {
|
private async Task<short?> ReadStreamHeader(NetworkStream tcpStream, CancellationToken cancellationToken) {
|
||||||
var headerBuffer = ArrayPool<byte>.Shared.Rent(3);
|
var headerBuffer = ArrayPool<byte>.Shared.Rent(3);
|
||||||
try {
|
try {
|
||||||
await tcpStream.ReadExactlyAsync(headerBuffer, 0, 3, cancellationToken);
|
await tcpStream.ReadExactlyAsync(headerBuffer, 0, 3, cancellationToken);
|
||||||
|
|
||||||
if (headerBuffer[0] != 0xFF) {
|
if (headerBuffer[0] != 0xFF) {
|
||||||
throw new ProtocolException("Unexpected first byte in response from server: " + headerBuffer[0]);
|
logger.Error("Unexpected first byte in response from server: {FirstByte}.", headerBuffer[0]);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
short messageLength = BinaryPrimitives.ReadInt16BigEndian(headerBuffer.AsSpan(1));
|
short messageLength = BinaryPrimitives.ReadInt16BigEndian(headerBuffer.AsSpan(1));
|
||||||
if (messageLength <= 0) {
|
if (messageLength <= 0) {
|
||||||
throw new ProtocolException("Unexpected message length in response from server: " + messageLength);
|
logger.Error("Unexpected message length in response from server: {MessageLength}.", messageLength);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return messageLength;
|
return messageLength;
|
||||||
@@ -41,54 +59,35 @@ public static class ServerStatusProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<InstancePlayerCounts> ReadPlayerCounts(NetworkStream tcpStream, int messageLength, CancellationToken cancellationToken) {
|
private async Task<int?> ReadOnlinePlayerCount(NetworkStream tcpStream, int messageLength, CancellationToken cancellationToken) {
|
||||||
var messageBuffer = ArrayPool<byte>.Shared.Rent(messageLength);
|
var messageBuffer = ArrayPool<byte>.Shared.Rent(messageLength);
|
||||||
try {
|
try {
|
||||||
await tcpStream.ReadExactlyAsync(messageBuffer, 0, messageLength, cancellationToken);
|
await tcpStream.ReadExactlyAsync(messageBuffer, 0, messageLength, cancellationToken);
|
||||||
return ReadPlayerCountsFromResponse(messageBuffer.AsSpan(0, messageLength));
|
|
||||||
|
// 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;
|
||||||
} finally {
|
} finally {
|
||||||
ArrayPool<byte>.Shared.Return(messageBuffer);
|
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) {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -6,6 +6,7 @@ using Phantom.Agent.Services.Instances;
|
|||||||
using Phantom.Common.Data.Agent;
|
using Phantom.Common.Data.Agent;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
|
using Phantom.Utils.Tasks;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
namespace Phantom.Agent.Services;
|
namespace Phantom.Agent.Services;
|
||||||
@@ -17,24 +18,22 @@ public sealed class AgentServices {
|
|||||||
|
|
||||||
private AgentFolders AgentFolders { get; }
|
private AgentFolders AgentFolders { get; }
|
||||||
private AgentState AgentState { get; }
|
private AgentState AgentState { get; }
|
||||||
|
private TaskManager TaskManager { get; }
|
||||||
private BackupManager BackupManager { get; }
|
private BackupManager BackupManager { get; }
|
||||||
|
|
||||||
internal JavaRuntimeRepository JavaRuntimeRepository { get; }
|
internal JavaRuntimeRepository JavaRuntimeRepository { get; }
|
||||||
internal InstanceTicketManager InstanceTicketManager { get; }
|
internal InstanceSessionManager InstanceSessionManager { get; }
|
||||||
internal ActorRef<InstanceManagerActor.ICommand> InstanceManager { get; }
|
|
||||||
|
|
||||||
public AgentServices(AgentInfo agentInfo, AgentFolders agentFolders, AgentServiceConfiguration serviceConfiguration, ControllerConnection controllerConnection) {
|
public AgentServices(AgentInfo agentInfo, AgentFolders agentFolders, AgentServiceConfiguration serviceConfiguration, ControllerConnection controllerConnection) {
|
||||||
this.ActorSystem = ActorSystemFactory.Create("Agent");
|
this.ActorSystem = ActorSystemFactory.Create("Agent");
|
||||||
|
|
||||||
this.AgentFolders = agentFolders;
|
this.AgentFolders = agentFolders;
|
||||||
this.AgentState = new AgentState();
|
this.AgentState = new AgentState();
|
||||||
|
this.TaskManager = new TaskManager(PhantomLogger.Create<TaskManager, AgentServices>());
|
||||||
this.BackupManager = new BackupManager(agentFolders, serviceConfiguration.MaxConcurrentCompressionTasks);
|
this.BackupManager = new BackupManager(agentFolders, serviceConfiguration.MaxConcurrentCompressionTasks);
|
||||||
|
|
||||||
this.JavaRuntimeRepository = new JavaRuntimeRepository();
|
this.JavaRuntimeRepository = new JavaRuntimeRepository();
|
||||||
this.InstanceTicketManager = new InstanceTicketManager(agentInfo, controllerConnection);
|
this.InstanceSessionManager = new InstanceSessionManager(ActorSystem, controllerConnection, agentInfo, agentFolders, AgentState, JavaRuntimeRepository, TaskManager, BackupManager);
|
||||||
|
|
||||||
var instanceManagerInit = new InstanceManagerActor.Init(controllerConnection, agentFolders, AgentState, JavaRuntimeRepository, InstanceTicketManager, BackupManager);
|
|
||||||
this.InstanceManager = ActorSystem.ActorOf(InstanceManagerActor.Factory(instanceManagerInit), "InstanceManager");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Initialize() {
|
public async Task Initialize() {
|
||||||
@@ -46,7 +45,8 @@ public sealed class AgentServices {
|
|||||||
public async Task Shutdown() {
|
public async Task Shutdown() {
|
||||||
Logger.Information("Stopping services...");
|
Logger.Information("Stopping services...");
|
||||||
|
|
||||||
await InstanceManager.Stop(new InstanceManagerActor.ShutdownCommand());
|
await InstanceSessionManager.DisposeAsync();
|
||||||
|
await TaskManager.Stop();
|
||||||
|
|
||||||
BackupManager.Dispose();
|
BackupManager.Dispose();
|
||||||
|
|
||||||
|
@@ -25,7 +25,7 @@ sealed class BackupArchiver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private bool IsFolderSkipped(ImmutableList<string> relativePath) {
|
private bool IsFolderSkipped(ImmutableList<string> relativePath) {
|
||||||
return relativePath is ["cache" or "crash-reports" or "debug" or "libraries" or "logs" or "mods" or "servermods" or "versions"];
|
return relativePath is ["cache" or "crash-reports" or "debug" or "libraries" or "logs" or "mods" or "versions"];
|
||||||
}
|
}
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "ConvertIfStatementToReturnStatement")]
|
[SuppressMessage("ReSharper", "ConvertIfStatementToReturnStatement")]
|
||||||
|
@@ -67,10 +67,6 @@ sealed class BackupManager : IDisposable {
|
|||||||
resultBuilder.Kind = BackupCreationResultKind.BackupCancelled;
|
resultBuilder.Kind = BackupCreationResultKind.BackupCancelled;
|
||||||
logger.Warning("Backup creation was cancelled.");
|
logger.Warning("Backup creation was cancelled.");
|
||||||
return null;
|
return null;
|
||||||
} catch (TimeoutException) {
|
|
||||||
resultBuilder.Kind = BackupCreationResultKind.BackupTimedOut;
|
|
||||||
logger.Warning("Backup creation timed out.");
|
|
||||||
return null;
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
resultBuilder.Kind = BackupCreationResultKind.UnknownError;
|
resultBuilder.Kind = BackupCreationResultKind.UnknownError;
|
||||||
logger.Error(e, "Caught exception while creating an instance backup.");
|
logger.Error(e, "Caught exception while creating an instance backup.");
|
||||||
@@ -80,9 +76,6 @@ sealed class BackupManager : IDisposable {
|
|||||||
await dispatcher.EnableAutomaticSaving();
|
await dispatcher.EnableAutomaticSaving();
|
||||||
} catch (OperationCanceledException) {
|
} catch (OperationCanceledException) {
|
||||||
// Ignore.
|
// Ignore.
|
||||||
} catch (TimeoutException) {
|
|
||||||
resultBuilder.Warnings |= BackupCreationWarnings.CouldNotRestoreAutomaticSaving;
|
|
||||||
logger.Warning("Timed out waiting for automatic saving to be re-enabled.");
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
resultBuilder.Warnings |= BackupCreationWarnings.CouldNotRestoreAutomaticSaving;
|
resultBuilder.Warnings |= BackupCreationWarnings.CouldNotRestoreAutomaticSaving;
|
||||||
logger.Error(e, "Caught exception while enabling automatic saving after creating an instance backup.");
|
logger.Error(e, "Caught exception while enabling automatic saving after creating an instance backup.");
|
||||||
@@ -127,7 +120,6 @@ sealed class BackupManager : IDisposable {
|
|||||||
BackupCreationResultKind.Success => "Backup created successfully.",
|
BackupCreationResultKind.Success => "Backup created successfully.",
|
||||||
BackupCreationResultKind.InstanceNotRunning => "Instance is not running.",
|
BackupCreationResultKind.InstanceNotRunning => "Instance is not running.",
|
||||||
BackupCreationResultKind.BackupCancelled => "Backup cancelled.",
|
BackupCreationResultKind.BackupCancelled => "Backup cancelled.",
|
||||||
BackupCreationResultKind.BackupTimedOut => "Backup timed out.",
|
|
||||||
BackupCreationResultKind.BackupAlreadyRunning => "A backup is already being created.",
|
BackupCreationResultKind.BackupAlreadyRunning => "A backup is already being created.",
|
||||||
BackupCreationResultKind.BackupFileAlreadyExists => "Backup with the same name already exists.",
|
BackupCreationResultKind.BackupFileAlreadyExists => "Backup with the same name already exists.",
|
||||||
BackupCreationResultKind.CouldNotCreateBackupFolder => "Could not create backup folder.",
|
BackupCreationResultKind.CouldNotCreateBackupFolder => "Could not create backup folder.",
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
using Phantom.Agent.Services.Instances;
|
using Phantom.Agent.Minecraft.Instance;
|
||||||
using Phantom.Agent.Services.Instances.State;
|
using Phantom.Agent.Minecraft.Server;
|
||||||
|
using Phantom.Agent.Services.Instances;
|
||||||
using Phantom.Common.Data.Backups;
|
using Phantom.Common.Data.Backups;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
using Phantom.Utils.Tasks;
|
using Phantom.Utils.Tasks;
|
||||||
|
using Phantom.Utils.Threading;
|
||||||
|
|
||||||
namespace Phantom.Agent.Services.Backups;
|
namespace Phantom.Agent.Services.Backups;
|
||||||
|
|
||||||
@@ -14,23 +16,27 @@ sealed class BackupScheduler : CancellableBackgroundTask {
|
|||||||
|
|
||||||
private readonly BackupManager backupManager;
|
private readonly BackupManager backupManager;
|
||||||
private readonly InstanceContext context;
|
private readonly InstanceContext context;
|
||||||
|
private readonly InstanceProcess process;
|
||||||
private readonly SemaphoreSlim backupSemaphore = new (1, 1);
|
private readonly SemaphoreSlim backupSemaphore = new (1, 1);
|
||||||
|
private readonly int serverPort;
|
||||||
|
private readonly ServerStatusProtocol serverStatusProtocol;
|
||||||
private readonly ManualResetEventSlim serverOutputWhileWaitingForOnlinePlayers = new ();
|
private readonly ManualResetEventSlim serverOutputWhileWaitingForOnlinePlayers = new ();
|
||||||
private readonly InstancePlayerCountTracker playerCountTracker;
|
|
||||||
|
|
||||||
public event EventHandler<BackupCreationResult>? BackupCompleted;
|
public event EventHandler<BackupCreationResult>? BackupCompleted;
|
||||||
|
|
||||||
public BackupScheduler(InstanceContext context, InstancePlayerCountTracker playerCountTracker) : base(PhantomLogger.Create<BackupScheduler>(context.ShortName)) {
|
public BackupScheduler(InstanceContext context, InstanceProcess process, int serverPort) : base(PhantomLogger.Create<BackupScheduler>(context.ShortName), context.Services.TaskManager, "Backup scheduler for " + context.ShortName) {
|
||||||
this.backupManager = context.Services.BackupManager;
|
this.backupManager = context.Services.BackupManager;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.playerCountTracker = playerCountTracker;
|
this.process = process;
|
||||||
|
this.serverPort = serverPort;
|
||||||
|
this.serverStatusProtocol = new ServerStatusProtocol(context.ShortName);
|
||||||
Start();
|
Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task RunTask() {
|
protected override async Task RunTask() {
|
||||||
await Task.Delay(InitialDelay, CancellationToken);
|
await Task.Delay(InitialDelay, CancellationToken);
|
||||||
Logger.Information("Starting a new backup after server launched.");
|
Logger.Information("Starting a new backup after server launched.");
|
||||||
|
|
||||||
while (!CancellationToken.IsCancellationRequested) {
|
while (!CancellationToken.IsCancellationRequested) {
|
||||||
var result = await CreateBackup();
|
var result = await CreateBackup();
|
||||||
BackupCompleted?.Invoke(this, result);
|
BackupCompleted?.Invoke(this, result);
|
||||||
@@ -53,28 +59,50 @@ sealed class BackupScheduler : CancellableBackgroundTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
context.ActorCancellationToken.ThrowIfCancellationRequested();
|
return await context.Actor.Request(new InstanceActor.BackupInstanceCommand(backupManager, CancellationToken.None /* TODO */));
|
||||||
return await context.Actor.Request(new InstanceActor.BackupInstanceCommand(backupManager), context.ActorCancellationToken);
|
|
||||||
} catch (OperationCanceledException) {
|
|
||||||
return new BackupCreationResult(BackupCreationResultKind.InstanceNotRunning);
|
|
||||||
} finally {
|
} finally {
|
||||||
backupSemaphore.Release();
|
backupSemaphore.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task WaitForOnlinePlayers() {
|
private async Task WaitForOnlinePlayers() {
|
||||||
var task = playerCountTracker.WaitForOnlinePlayers(CancellationToken);
|
bool needsToLogOfflinePlayersMessage = true;
|
||||||
if (!task.IsCompleted) {
|
|
||||||
Logger.Information("Waiting for someone to join before starting a new backup.");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
process.AddOutputListener(ServerOutputListener, maxLinesToReadFromHistory: 0);
|
||||||
try {
|
try {
|
||||||
await task;
|
while (!CancellationToken.IsCancellationRequested) {
|
||||||
Logger.Information("Players are online, starting a new backup.");
|
serverOutputWhileWaitingForOnlinePlayers.Reset();
|
||||||
} catch (OperationCanceledException) {
|
|
||||||
throw;
|
var onlinePlayerCount = await serverStatusProtocol.GetOnlinePlayerCount(serverPort, CancellationToken);
|
||||||
} catch (Exception) {
|
if (onlinePlayerCount == null) {
|
||||||
Logger.Warning("Could not detect whether any players are online, starting a new backup.");
|
Logger.Warning("Could not detect whether any players are online, starting a new backup.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onlinePlayerCount > 0) {
|
||||||
|
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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Text.RegularExpressions;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Phantom.Agent.Minecraft.Command;
|
using Phantom.Agent.Minecraft.Command;
|
||||||
using Phantom.Agent.Minecraft.Instance;
|
using Phantom.Agent.Minecraft.Instance;
|
||||||
using Phantom.Utils.Tasks;
|
using Phantom.Utils.Tasks;
|
||||||
@@ -8,26 +7,8 @@ using Serilog;
|
|||||||
namespace Phantom.Agent.Services.Backups;
|
namespace Phantom.Agent.Services.Backups;
|
||||||
|
|
||||||
sealed partial class BackupServerCommandDispatcher : IDisposable {
|
sealed partial class BackupServerCommandDispatcher : IDisposable {
|
||||||
[GeneratedRegex(@"^(?:(?:\[.*?\] \[Server thread/INFO\].*?:)|(?:[\d-]+? [\d:]+? \[INFO\])) (.*?)$", RegexOptions.NonBacktracking)]
|
[GeneratedRegex(@"^\[(?:.*?)\] \[Server thread/INFO\]: (.*?)$", RegexOptions.NonBacktracking)]
|
||||||
private static partial Regex ServerThreadInfoRegex();
|
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 ILogger logger;
|
||||||
private readonly InstanceProcess process;
|
private readonly InstanceProcess process;
|
||||||
@@ -51,17 +32,18 @@ sealed partial class BackupServerCommandDispatcher : IDisposable {
|
|||||||
|
|
||||||
public async Task DisableAutomaticSaving() {
|
public async Task DisableAutomaticSaving() {
|
||||||
await process.SendCommand(MinecraftCommand.SaveOff, cancellationToken);
|
await process.SendCommand(MinecraftCommand.SaveOff, cancellationToken);
|
||||||
await automaticSavingDisabled.Task.WaitAsync(TimeSpan.FromSeconds(30), cancellationToken);
|
await automaticSavingDisabled.Task.WaitAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveAllChunks() {
|
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 process.SendCommand(MinecraftCommand.SaveAll(flush: true), cancellationToken);
|
||||||
await savedTheGame.Task.WaitAsync(TimeSpan.FromMinutes(1), cancellationToken);
|
await savedTheGame.Task.WaitAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task EnableAutomaticSaving() {
|
public async Task EnableAutomaticSaving() {
|
||||||
await process.SendCommand(MinecraftCommand.SaveOn, cancellationToken);
|
await process.SendCommand(MinecraftCommand.SaveOn, cancellationToken);
|
||||||
await automaticSavingEnabled.Task.WaitAsync(TimeSpan.FromMinutes(1), cancellationToken);
|
await automaticSavingEnabled.Task.WaitAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnOutput(object? sender, string? line) {
|
private void OnOutput(object? sender, string? line) {
|
||||||
@@ -77,19 +59,19 @@ sealed partial class BackupServerCommandDispatcher : IDisposable {
|
|||||||
string info = match.Groups[1].Value;
|
string info = match.Groups[1].Value;
|
||||||
|
|
||||||
if (!automaticSavingDisabled.Task.IsCompleted) {
|
if (!automaticSavingDisabled.Task.IsCompleted) {
|
||||||
if (AutomaticSavingDisabledMessages.Contains(info)) {
|
if (info == "Automatic saving is now disabled") {
|
||||||
logger.Debug("Detected that automatic saving is disabled.");
|
logger.Debug("Detected that automatic saving is disabled.");
|
||||||
automaticSavingDisabled.SetResult();
|
automaticSavingDisabled.SetResult();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (!savedTheGame.Task.IsCompleted) {
|
else if (!savedTheGame.Task.IsCompleted) {
|
||||||
if (SavedTheGameMessages.Contains(info)) {
|
if (info == "Saved the game") {
|
||||||
logger.Debug("Detected that the game is saved.");
|
logger.Debug("Detected that the game is saved.");
|
||||||
savedTheGame.SetResult();
|
savedTheGame.SetResult();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (!automaticSavingEnabled.Task.IsCompleted) {
|
else if (!automaticSavingEnabled.Task.IsCompleted) {
|
||||||
if (AutomaticSavingEnabledMessages.Contains(info)) {
|
if (info == "Automatic saving is now enabled") {
|
||||||
logger.Debug("Detected that automatic saving is enabled.");
|
logger.Debug("Detected that automatic saving is enabled.");
|
||||||
automaticSavingEnabled.SetResult();
|
automaticSavingEnabled.SetResult();
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
using Phantom.Agent.Minecraft.Launcher;
|
using Phantom.Agent.Minecraft.Launcher;
|
||||||
using Phantom.Agent.Services.Backups;
|
using Phantom.Agent.Services.Backups;
|
||||||
using Phantom.Agent.Services.Instances.State;
|
using Phantom.Agent.Services.Instances.Procedures;
|
||||||
|
using Phantom.Agent.Services.Instances.States;
|
||||||
using Phantom.Common.Data.Backups;
|
using Phantom.Common.Data.Backups;
|
||||||
using Phantom.Common.Data.Instance;
|
using Phantom.Common.Data.Instance;
|
||||||
using Phantom.Common.Data.Minecraft;
|
using Phantom.Common.Data.Minecraft;
|
||||||
@@ -9,38 +10,34 @@ using Phantom.Common.Messages.Agent.ToController;
|
|||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
using Phantom.Utils.Actor.Mailbox;
|
using Phantom.Utils.Actor.Mailbox;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
namespace Phantom.Agent.Services.Instances;
|
namespace Phantom.Agent.Services.Instances;
|
||||||
|
|
||||||
sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
|
sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
|
||||||
public readonly record struct Init(AgentState AgentState, Guid InstanceGuid, string ShortName, InstanceServices InstanceServices, InstanceTicketManager InstanceTicketManager, CancellationToken ShutdownCancellationToken);
|
public readonly record struct Init(Guid InstanceGuid, string ShortName, InstanceServices Services, AgentState AgentState);
|
||||||
|
|
||||||
public static Props<ICommand> Factory(Init init) {
|
public static Props<ICommand> Factory(Init init) {
|
||||||
return Props<ICommand>.Create(() => new InstanceActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume, MailboxType = UnboundedJumpAheadMailbox.Name });
|
return Props<ICommand>.Create(() => new InstanceActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume, MailboxType = UnboundedJumpAheadMailbox.Name });
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly AgentState agentState;
|
|
||||||
private readonly CancellationToken shutdownCancellationToken;
|
|
||||||
|
|
||||||
private readonly Guid instanceGuid;
|
private readonly Guid instanceGuid;
|
||||||
private readonly InstanceServices instanceServices;
|
private readonly InstanceServices services;
|
||||||
private readonly InstanceTicketManager instanceTicketManager;
|
private readonly AgentState agentState;
|
||||||
private readonly InstanceContext context;
|
|
||||||
|
|
||||||
private readonly CancellationTokenSource actorCancellationTokenSource = new ();
|
private readonly ILogger logger;
|
||||||
|
private readonly InstanceContext context;
|
||||||
|
|
||||||
private IInstanceStatus currentStatus = InstanceStatus.NotRunning;
|
private IInstanceStatus currentStatus = InstanceStatus.NotRunning;
|
||||||
private InstanceRunningState? runningState = null;
|
private IInstanceState currentState = new InstanceNotRunningState();
|
||||||
|
|
||||||
private InstanceActor(Init init) {
|
private InstanceActor(Init init) {
|
||||||
this.agentState = init.AgentState;
|
|
||||||
this.instanceGuid = init.InstanceGuid;
|
this.instanceGuid = init.InstanceGuid;
|
||||||
this.instanceServices = init.InstanceServices;
|
this.services = init.Services;
|
||||||
this.instanceTicketManager = init.InstanceTicketManager;
|
this.agentState = init.AgentState;
|
||||||
this.shutdownCancellationToken = init.ShutdownCancellationToken;
|
|
||||||
|
|
||||||
var logger = PhantomLogger.Create<InstanceActor>(init.ShortName);
|
this.logger = PhantomLogger.Create<InstanceActor>(init.ShortName);
|
||||||
this.context = new InstanceContext(instanceGuid, init.ShortName, logger, instanceServices, SelfTyped, actorCancellationTokenSource.Token);
|
this.context = new InstanceContext(instanceGuid, init.ShortName, logger, services, SelfTyped);
|
||||||
|
|
||||||
Receive<ReportInstanceStatusCommand>(ReportInstanceStatus);
|
Receive<ReportInstanceStatusCommand>(ReportInstanceStatus);
|
||||||
ReceiveAsync<LaunchInstanceCommand>(LaunchInstance);
|
ReceiveAsync<LaunchInstanceCommand>(LaunchInstance);
|
||||||
@@ -58,30 +55,35 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
|
|||||||
|
|
||||||
private void ReportCurrentStatus() {
|
private void ReportCurrentStatus() {
|
||||||
agentState.UpdateInstance(new Instance(instanceGuid, currentStatus));
|
agentState.UpdateInstance(new Instance(instanceGuid, currentStatus));
|
||||||
instanceServices.ControllerConnection.Send(new ReportInstanceStatusMessage(instanceGuid, currentStatus));
|
services.ControllerConnection.Send(new ReportInstanceStatusMessage(instanceGuid, currentStatus));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TransitionState(InstanceRunningState? newState) {
|
private void TransitionState(IInstanceState newState) {
|
||||||
if (runningState == newState) {
|
if (currentState == newState) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
runningState?.Dispose();
|
if (currentState is IDisposable disposable) {
|
||||||
runningState = newState;
|
disposable.Dispose();
|
||||||
runningState?.Initialize();
|
}
|
||||||
|
|
||||||
|
logger.Debug("Transitioning instance state to: {NewState}", newState.GetType().Name);
|
||||||
|
|
||||||
|
currentState = newState;
|
||||||
|
currentState.Initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface ICommand {}
|
public interface ICommand {}
|
||||||
|
|
||||||
public sealed record ReportInstanceStatusCommand : ICommand;
|
public sealed record ReportInstanceStatusCommand : ICommand;
|
||||||
|
|
||||||
public sealed record LaunchInstanceCommand(InstanceConfiguration Configuration, IServerLauncher Launcher, InstanceTicketManager.Ticket Ticket, bool IsRestarting) : ICommand;
|
public sealed record LaunchInstanceCommand(InstanceConfiguration Configuration, IServerLauncher Launcher, InstanceTicketManager.Ticket Ticket, bool IsRestarting, CancellationToken CancellationToken) : ICommand;
|
||||||
|
|
||||||
public sealed record StopInstanceCommand(MinecraftStopStrategy StopStrategy) : ICommand;
|
public sealed record StopInstanceCommand(MinecraftStopStrategy StopStrategy, CancellationToken CancellationToken) : ICommand;
|
||||||
|
|
||||||
public sealed record SendCommandToInstanceCommand(string Command) : ICommand, ICanReply<SendCommandToInstanceResult>;
|
public sealed record SendCommandToInstanceCommand(string Command, CancellationToken CancellationToken) : ICommand, ICanReply<SendCommandToInstanceResult>;
|
||||||
|
|
||||||
public sealed record BackupInstanceCommand(BackupManager BackupManager) : ICommand, ICanReply<BackupCreationResult>;
|
public sealed record BackupInstanceCommand(BackupManager BackupManager, CancellationToken CancellationToken) : ICommand, ICanReply<BackupCreationResult>;
|
||||||
|
|
||||||
public sealed record HandleProcessEndedCommand(IInstanceStatus Status) : ICommand, IJumpAhead;
|
public sealed record HandleProcessEndedCommand(IInstanceStatus Status) : ICommand, IJumpAhead;
|
||||||
|
|
||||||
@@ -92,29 +94,23 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task LaunchInstance(LaunchInstanceCommand command) {
|
private async Task LaunchInstance(LaunchInstanceCommand command) {
|
||||||
if (command.IsRestarting || runningState is null) {
|
if (command.IsRestarting || currentState is not InstanceRunningState) {
|
||||||
SetAndReportStatus(command.IsRestarting ? InstanceStatus.Restarting : InstanceStatus.Launching);
|
SetAndReportStatus(command.IsRestarting ? InstanceStatus.Restarting : InstanceStatus.Launching);
|
||||||
|
TransitionState(await InstanceLaunchProcedure.Run(context, command.Configuration, command.Launcher, command.Ticket, SetAndReportStatus, command.CancellationToken));
|
||||||
var newState = await InstanceLaunchProcedure.Run(context, command.Configuration, command.Launcher, instanceTicketManager, command.Ticket, SetAndReportStatus, shutdownCancellationToken);
|
|
||||||
if (newState is null) {
|
|
||||||
instanceTicketManager.Release(command.Ticket);
|
|
||||||
}
|
|
||||||
|
|
||||||
TransitionState(newState);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task StopInstance(StopInstanceCommand command) {
|
private async Task StopInstance(StopInstanceCommand command) {
|
||||||
if (runningState is null) {
|
if (currentState is not InstanceRunningState runningState) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
IInstanceStatus oldStatus = currentStatus;
|
IInstanceStatus oldStatus = currentStatus;
|
||||||
SetAndReportStatus(InstanceStatus.Stopping);
|
SetAndReportStatus(InstanceStatus.Stopping);
|
||||||
|
|
||||||
if (await InstanceStopProcedure.Run(context, command.StopStrategy, runningState, SetAndReportStatus, shutdownCancellationToken)) {
|
var newState = await InstanceStopProcedure.Run(context, command.StopStrategy, runningState, SetAndReportStatus, command.CancellationToken);
|
||||||
instanceTicketManager.Release(runningState.Ticket);
|
if (newState is not null) {
|
||||||
TransitionState(null);
|
TransitionState(newState);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
SetAndReportStatus(oldStatus);
|
SetAndReportStatus(oldStatus);
|
||||||
@@ -122,40 +118,33 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task<SendCommandToInstanceResult> SendCommandToInstance(SendCommandToInstanceCommand command) {
|
private async Task<SendCommandToInstanceResult> SendCommandToInstance(SendCommandToInstanceCommand command) {
|
||||||
if (runningState is null) {
|
if (currentState is InstanceRunningState runningState) {
|
||||||
return SendCommandToInstanceResult.InstanceNotRunning;
|
return await runningState.SendCommand(command.Command, command.CancellationToken);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return await runningState.SendCommand(command.Command, shutdownCancellationToken);
|
return SendCommandToInstanceResult.InstanceNotRunning;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<BackupCreationResult> BackupInstance(BackupInstanceCommand command) {
|
private async Task<BackupCreationResult> BackupInstance(BackupInstanceCommand command) {
|
||||||
if (runningState is null || runningState.Process.HasEnded) {
|
if (currentState is not InstanceRunningState runningState || runningState.Process.HasEnded) {
|
||||||
return new BackupCreationResult(BackupCreationResultKind.InstanceNotRunning);
|
return new BackupCreationResult(BackupCreationResultKind.InstanceNotRunning);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
SetAndReportStatus(InstanceStatus.BackingUp);
|
return await command.BackupManager.CreateBackup(context.ShortName, runningState.Process, command.CancellationToken);
|
||||||
try {
|
|
||||||
return await command.BackupManager.CreateBackup(context.ShortName, runningState.Process, shutdownCancellationToken);
|
|
||||||
} finally {
|
|
||||||
SetAndReportStatus(InstanceStatus.Running);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleProcessEnded(HandleProcessEndedCommand command) {
|
private void HandleProcessEnded(HandleProcessEndedCommand command) {
|
||||||
if (runningState is { Process.HasEnded: true }) {
|
if (currentState is InstanceRunningState { Process.HasEnded: true }) {
|
||||||
SetAndReportStatus(command.Status);
|
SetAndReportStatus(command.Status);
|
||||||
context.ReportEvent(InstanceEvent.Stopped);
|
context.ReportEvent(InstanceEvent.Stopped);
|
||||||
instanceTicketManager.Release(runningState.Ticket);
|
TransitionState(new InstanceNotRunningState());
|
||||||
TransitionState(null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Shutdown(ShutdownCommand command) {
|
private async Task Shutdown(ShutdownCommand command) {
|
||||||
await StopInstance(new StopInstanceCommand(MinecraftStopStrategy.Instant));
|
await StopInstance(new StopInstanceCommand(MinecraftStopStrategy.Instant, CancellationToken.None));
|
||||||
await actorCancellationTokenSource.CancelAsync();
|
|
||||||
Context.Stop(Self);
|
Context.Stop(Self);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,7 @@ using Serilog;
|
|||||||
|
|
||||||
namespace Phantom.Agent.Services.Instances;
|
namespace Phantom.Agent.Services.Instances;
|
||||||
|
|
||||||
sealed record InstanceContext(Guid InstanceGuid, string ShortName, ILogger Logger, InstanceServices Services, ActorRef<InstanceActor.ICommand> Actor, CancellationToken ActorCancellationToken) {
|
sealed record InstanceContext(Guid InstanceGuid, string ShortName, ILogger Logger, InstanceServices Services, ActorRef<InstanceActor.ICommand> Actor) {
|
||||||
public void ReportEvent(IInstanceEvent instanceEvent) {
|
public void ReportEvent(IInstanceEvent instanceEvent) {
|
||||||
Services.ControllerConnection.Send(new ReportInstanceEventMessage(Guid.NewGuid(), DateTime.UtcNow, InstanceGuid, instanceEvent));
|
Services.ControllerConnection.Send(new ReportInstanceEventMessage(Guid.NewGuid(), DateTime.UtcNow, InstanceGuid, instanceEvent));
|
||||||
}
|
}
|
||||||
|
@@ -5,10 +5,10 @@ using Phantom.Common.Messages.Agent.ToController;
|
|||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
using Phantom.Utils.Tasks;
|
using Phantom.Utils.Tasks;
|
||||||
|
|
||||||
namespace Phantom.Agent.Services.Instances.State;
|
namespace Phantom.Agent.Services.Instances;
|
||||||
|
|
||||||
sealed class InstanceLogSender : CancellableBackgroundTask {
|
sealed class InstanceLogSender : CancellableBackgroundTask {
|
||||||
private static readonly BoundedChannelOptions BufferOptions = new (capacity: 100) {
|
private static readonly BoundedChannelOptions BufferOptions = new (capacity: 64) {
|
||||||
SingleReader = true,
|
SingleReader = true,
|
||||||
SingleWriter = true,
|
SingleWriter = true,
|
||||||
FullMode = BoundedChannelFullMode.DropNewest
|
FullMode = BoundedChannelFullMode.DropNewest
|
||||||
@@ -22,7 +22,7 @@ sealed class InstanceLogSender : CancellableBackgroundTask {
|
|||||||
|
|
||||||
private int droppedLinesSinceLastSend;
|
private int droppedLinesSinceLastSend;
|
||||||
|
|
||||||
public InstanceLogSender(ControllerConnection controllerConnection, Guid instanceGuid, string loggerName) : base(PhantomLogger.Create<InstanceLogSender>(loggerName)) {
|
public InstanceLogSender(ControllerConnection controllerConnection, TaskManager taskManager, Guid instanceGuid, string loggerName) : base(PhantomLogger.Create<InstanceLogSender>(loggerName), taskManager, "Instance log sender for " + loggerName) {
|
||||||
this.controllerConnection = controllerConnection;
|
this.controllerConnection = controllerConnection;
|
||||||
this.instanceGuid = instanceGuid;
|
this.instanceGuid = instanceGuid;
|
||||||
this.outputChannel = Channel.CreateBounded<string>(BufferOptions, OnLineDropped);
|
this.outputChannel = Channel.CreateBounded<string>(BufferOptions, OnLineDropped);
|
@@ -1,209 +0,0 @@
|
|||||||
using Phantom.Agent.Minecraft.Instance;
|
|
||||||
using Phantom.Agent.Minecraft.Java;
|
|
||||||
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.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 Serilog;
|
|
||||||
|
|
||||||
namespace Phantom.Agent.Services.Instances;
|
|
||||||
|
|
||||||
sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand> {
|
|
||||||
private static readonly ILogger Logger = PhantomLogger.Create<InstanceManagerActor>();
|
|
||||||
|
|
||||||
public readonly record struct Init(ControllerConnection ControllerConnection, AgentFolders AgentFolders, AgentState AgentState, JavaRuntimeRepository JavaRuntimeRepository, InstanceTicketManager InstanceTicketManager, BackupManager BackupManager);
|
|
||||||
|
|
||||||
public static Props<ICommand> Factory(Init init) {
|
|
||||||
return Props<ICommand>.Create(() => new InstanceManagerActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume });
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly AgentState agentState;
|
|
||||||
private readonly string basePath;
|
|
||||||
|
|
||||||
private readonly InstanceServices instanceServices;
|
|
||||||
private readonly InstanceTicketManager instanceTicketManager;
|
|
||||||
private readonly Dictionary<Guid, InstanceInfo> instances = new ();
|
|
||||||
|
|
||||||
private readonly CancellationTokenSource shutdownCancellationTokenSource = new ();
|
|
||||||
private readonly CancellationToken shutdownCancellationToken;
|
|
||||||
|
|
||||||
private uint instanceLoggerSequenceId = 0;
|
|
||||||
|
|
||||||
private InstanceManagerActor(Init init) {
|
|
||||||
this.agentState = init.AgentState;
|
|
||||||
this.basePath = init.AgentFolders.InstancesFolderPath;
|
|
||||||
this.instanceTicketManager = init.InstanceTicketManager;
|
|
||||||
this.shutdownCancellationToken = shutdownCancellationTokenSource.Token;
|
|
||||||
|
|
||||||
var minecraftServerExecutables = new MinecraftServerExecutables(init.AgentFolders.ServerExecutableFolderPath);
|
|
||||||
var launchServices = new LaunchServices(minecraftServerExecutables, init.JavaRuntimeRepository);
|
|
||||||
|
|
||||||
this.instanceServices = new InstanceServices(init.ControllerConnection, init.BackupManager, launchServices);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetInstanceLoggerName(Guid guid) {
|
|
||||||
var prefix = guid.ToString();
|
|
||||||
return prefix[..prefix.IndexOf('-')] + "/" + Interlocked.Increment(ref instanceLoggerSequenceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed record InstanceInfo(ActorRef<InstanceActor.ICommand> Actor, InstanceConfiguration Configuration, IServerLauncher Launcher);
|
|
||||||
|
|
||||||
public interface ICommand {}
|
|
||||||
|
|
||||||
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<Result<LaunchInstanceResult, InstanceActionFailure>>;
|
|
||||||
|
|
||||||
public sealed record StopInstanceCommand(Guid InstanceGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>;
|
|
||||||
|
|
||||||
public sealed record SendCommandToInstanceCommand(Guid InstanceGuid, string Command) : ICommand, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>;
|
|
||||||
|
|
||||||
public sealed record ShutdownCommand : ICommand;
|
|
||||||
|
|
||||||
private Result<ConfigureInstanceResult, InstanceActionFailure> ConfigureInstance(ConfigureInstanceCommand command) {
|
|
||||||
var instanceGuid = command.InstanceGuid;
|
|
||||||
var configuration = command.Configuration;
|
|
||||||
|
|
||||||
var instanceFolder = Path.Combine(basePath, instanceGuid.ToString());
|
|
||||||
Directories.Create(instanceFolder, Chmod.URWX_GRX);
|
|
||||||
|
|
||||||
var heapMegabytes = configuration.MemoryAllocation.InMegabytes;
|
|
||||||
var jvmProperties = new JvmProperties(
|
|
||||||
InitialHeapMegabytes: heapMegabytes / 2,
|
|
||||||
MaximumHeapMegabytes: heapMegabytes
|
|
||||||
);
|
|
||||||
|
|
||||||
var properties = new InstanceProperties(
|
|
||||||
instanceGuid,
|
|
||||||
configuration.JavaRuntimeGuid,
|
|
||||||
jvmProperties,
|
|
||||||
configuration.JvmArguments,
|
|
||||||
instanceFolder,
|
|
||||||
configuration.MinecraftVersion,
|
|
||||||
new ServerProperties(configuration.ServerPort, configuration.RconPort),
|
|
||||||
command.LaunchProperties
|
|
||||||
);
|
|
||||||
|
|
||||||
IServerLauncher launcher = configuration.MinecraftServerKind switch {
|
|
||||||
MinecraftServerKind.Vanilla => new VanillaLauncher(properties),
|
|
||||||
MinecraftServerKind.Fabric => new FabricLauncher(properties),
|
|
||||||
MinecraftServerKind.Forge => new ForgeLauncher(properties),
|
|
||||||
_ => InvalidLauncher.Instance
|
|
||||||
};
|
|
||||||
|
|
||||||
if (instances.TryGetValue(instanceGuid, out var instance)) {
|
|
||||||
instances[instanceGuid] = instance with {
|
|
||||||
Configuration = configuration,
|
|
||||||
Launcher = launcher
|
|
||||||
};
|
|
||||||
|
|
||||||
Logger.Information("Reconfigured instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, instanceGuid);
|
|
||||||
|
|
||||||
if (command.AlwaysReportStatus) {
|
|
||||||
instance.Actor.Tell(new InstanceActor.ReportInstanceStatusCommand());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var instanceInit = new InstanceActor.Init(agentState, instanceGuid, GetInstanceLoggerName(instanceGuid), instanceServices, instanceTicketManager, shutdownCancellationToken);
|
|
||||||
instances[instanceGuid] = instance = new InstanceInfo(Context.ActorOf(InstanceActor.Factory(instanceInit), "Instance-" + instanceGuid), configuration, launcher);
|
|
||||||
|
|
||||||
Logger.Information("Created instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, instanceGuid);
|
|
||||||
|
|
||||||
instance.Actor.Tell(new InstanceActor.ReportInstanceStatusCommand());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.LaunchNow) {
|
|
||||||
LaunchInstance(new LaunchInstanceCommand(instanceGuid));
|
|
||||||
}
|
|
||||||
|
|
||||||
return ConfigureInstanceResult.Success;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Result<LaunchInstanceResult, InstanceActionFailure> LaunchInstance(LaunchInstanceCommand command) {
|
|
||||||
var instanceGuid = command.InstanceGuid;
|
|
||||||
if (!instances.TryGetValue(instanceGuid, out var instanceInfo)) {
|
|
||||||
return InstanceActionFailure.InstanceDoesNotExist;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ticket = instanceTicketManager.Reserve(instanceInfo.Configuration);
|
|
||||||
if (!ticket) {
|
|
||||||
return ticket.Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (agentState.InstancesByGuid.TryGetValue(instanceGuid, out var instance)) {
|
|
||||||
var status = instance.Status;
|
|
||||||
if (status.IsRunning()) {
|
|
||||||
return LaunchInstanceResult.InstanceAlreadyRunning;
|
|
||||||
}
|
|
||||||
else if (status.IsLaunching()) {
|
|
||||||
return LaunchInstanceResult.InstanceAlreadyLaunching;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
instanceInfo.Actor.Tell(new InstanceActor.LaunchInstanceCommand(instanceInfo.Configuration, instanceInfo.Launcher, ticket.Value, IsRestarting: false));
|
|
||||||
return LaunchInstanceResult.LaunchInitiated;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Result<StopInstanceResult, InstanceActionFailure> StopInstance(StopInstanceCommand command) {
|
|
||||||
var instanceGuid = command.InstanceGuid;
|
|
||||||
if (!instances.TryGetValue(instanceGuid, out var instanceInfo)) {
|
|
||||||
return InstanceActionFailure.InstanceDoesNotExist;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (agentState.InstancesByGuid.TryGetValue(instanceGuid, out var instance)) {
|
|
||||||
var status = instance.Status;
|
|
||||||
if (status.IsStopping()) {
|
|
||||||
return StopInstanceResult.InstanceAlreadyStopping;
|
|
||||||
}
|
|
||||||
else if (!status.CanStop()) {
|
|
||||||
return StopInstanceResult.InstanceAlreadyStopped;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
instanceInfo.Actor.Tell(new InstanceActor.StopInstanceCommand(command.StopStrategy));
|
|
||||||
return StopInstanceResult.StopInitiated;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> SendCommandToInstance(SendCommandToInstanceCommand command) {
|
|
||||||
var instanceGuid = command.InstanceGuid;
|
|
||||||
if (!instances.TryGetValue(instanceGuid, out var instanceInfo)) {
|
|
||||||
return InstanceActionFailure.InstanceDoesNotExist;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await instanceInfo.Actor.Request(new InstanceActor.SendCommandToInstanceCommand(command.Command), shutdownCancellationToken);
|
|
||||||
} catch (OperationCanceledException) {
|
|
||||||
return InstanceActionFailure.AgentShuttingDown;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task Shutdown(ShutdownCommand command) {
|
|
||||||
Logger.Information("Stopping all instances...");
|
|
||||||
|
|
||||||
await shutdownCancellationTokenSource.CancelAsync();
|
|
||||||
|
|
||||||
await Task.WhenAll(instances.Values.Select(static instance => instance.Actor.Stop(new InstanceActor.ShutdownCommand())));
|
|
||||||
instances.Clear();
|
|
||||||
|
|
||||||
shutdownCancellationTokenSource.Dispose();
|
|
||||||
|
|
||||||
Logger.Information("All instances stopped.");
|
|
||||||
|
|
||||||
Context.Stop(Self);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,7 +1,8 @@
|
|||||||
using Phantom.Agent.Minecraft.Launcher;
|
using Phantom.Agent.Minecraft.Launcher;
|
||||||
using Phantom.Agent.Rpc;
|
using Phantom.Agent.Rpc;
|
||||||
using Phantom.Agent.Services.Backups;
|
using Phantom.Agent.Services.Backups;
|
||||||
|
using Phantom.Utils.Tasks;
|
||||||
|
|
||||||
namespace Phantom.Agent.Services.Instances;
|
namespace Phantom.Agent.Services.Instances;
|
||||||
|
|
||||||
sealed record InstanceServices(ControllerConnection ControllerConnection, BackupManager BackupManager, LaunchServices LaunchServices);
|
sealed record InstanceServices(ControllerConnection ControllerConnection, TaskManager TaskManager, InstanceTicketManager InstanceTicketManager, BackupManager BackupManager, LaunchServices LaunchServices);
|
||||||
|
206
Agent/Phantom.Agent.Services/Instances/InstanceSessionManager.cs
Normal file
206
Agent/Phantom.Agent.Services/Instances/InstanceSessionManager.cs
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Akka.Actor;
|
||||||
|
using Phantom.Agent.Minecraft.Instance;
|
||||||
|
using Phantom.Agent.Minecraft.Java;
|
||||||
|
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.Common.Data.Agent;
|
||||||
|
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;
|
||||||
|
|
||||||
|
sealed class InstanceSessionManager : IAsyncDisposable {
|
||||||
|
private static readonly ILogger Logger = PhantomLogger.Create<InstanceSessionManager>();
|
||||||
|
|
||||||
|
private readonly ActorSystem actorSystem;
|
||||||
|
private readonly AgentState agentState;
|
||||||
|
private readonly string basePath;
|
||||||
|
|
||||||
|
private readonly InstanceServices instanceServices;
|
||||||
|
private readonly Dictionary<Guid, InstanceInfo> instances = new ();
|
||||||
|
|
||||||
|
private readonly CancellationTokenSource shutdownCancellationTokenSource = new ();
|
||||||
|
private readonly CancellationToken shutdownCancellationToken;
|
||||||
|
private readonly SemaphoreSlim semaphore = new (1, 1);
|
||||||
|
|
||||||
|
private uint instanceLoggerSequenceId = 0;
|
||||||
|
|
||||||
|
public InstanceSessionManager(ActorSystem actorSystem, ControllerConnection controllerConnection, AgentInfo agentInfo, AgentFolders agentFolders, AgentState agentState, JavaRuntimeRepository javaRuntimeRepository, TaskManager taskManager, BackupManager backupManager) {
|
||||||
|
this.actorSystem = actorSystem;
|
||||||
|
this.agentState = agentState;
|
||||||
|
this.actorSystem = actorSystem;
|
||||||
|
this.basePath = agentFolders.InstancesFolderPath;
|
||||||
|
this.shutdownCancellationToken = shutdownCancellationTokenSource.Token;
|
||||||
|
|
||||||
|
var minecraftServerExecutables = new MinecraftServerExecutables(agentFolders.ServerExecutableFolderPath);
|
||||||
|
var launchServices = new LaunchServices(minecraftServerExecutables, javaRuntimeRepository);
|
||||||
|
var instanceTicketManager = new InstanceTicketManager(agentInfo, controllerConnection);
|
||||||
|
|
||||||
|
this.instanceServices = new InstanceServices(controllerConnection, taskManager, instanceTicketManager, backupManager, launchServices);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record InstanceInfo(Guid Guid, ActorRef<InstanceActor.ICommand> Actor, InstanceConfiguration Configuration, IServerLauncher Launcher);
|
||||||
|
|
||||||
|
private async Task<InstanceActionResult<T>> AcquireSemaphoreAndRun<T>(Func<Task<InstanceActionResult<T>>> func) {
|
||||||
|
try {
|
||||||
|
await semaphore.WaitAsync(shutdownCancellationToken);
|
||||||
|
try {
|
||||||
|
return await func();
|
||||||
|
} finally {
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
} catch (OperationCanceledException) {
|
||||||
|
return InstanceActionResult.General<T>(InstanceActionGeneralResult.AgentShuttingDown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "ConvertIfStatementToReturnStatement")]
|
||||||
|
private Task<InstanceActionResult<T>> AcquireSemaphoreAndRunWithInstance<T>(Guid instanceGuid, Func<InstanceInfo, Task<T>> func) {
|
||||||
|
return AcquireSemaphoreAndRun(async () => {
|
||||||
|
if (instances.TryGetValue(instanceGuid, out var instance)) {
|
||||||
|
return InstanceActionResult.Concrete(await func(instance));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return InstanceActionResult.General<T>(InstanceActionGeneralResult.InstanceDoesNotExist);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<InstanceActionResult<ConfigureInstanceResult>> Configure(Guid instanceGuid, InstanceConfiguration configuration, InstanceLaunchProperties launchProperties, bool launchNow, bool alwaysReportStatus) {
|
||||||
|
return await AcquireSemaphoreAndRun(async () => {
|
||||||
|
var instanceFolder = Path.Combine(basePath, instanceGuid.ToString());
|
||||||
|
Directories.Create(instanceFolder, Chmod.URWX_GRX);
|
||||||
|
|
||||||
|
var heapMegabytes = configuration.MemoryAllocation.InMegabytes;
|
||||||
|
var jvmProperties = new JvmProperties(
|
||||||
|
InitialHeapMegabytes: heapMegabytes / 2,
|
||||||
|
MaximumHeapMegabytes: heapMegabytes
|
||||||
|
);
|
||||||
|
|
||||||
|
var properties = new InstanceProperties(
|
||||||
|
instanceGuid,
|
||||||
|
configuration.JavaRuntimeGuid,
|
||||||
|
jvmProperties,
|
||||||
|
configuration.JvmArguments,
|
||||||
|
instanceFolder,
|
||||||
|
configuration.MinecraftVersion,
|
||||||
|
new ServerProperties(configuration.ServerPort, configuration.RconPort),
|
||||||
|
launchProperties
|
||||||
|
);
|
||||||
|
|
||||||
|
IServerLauncher launcher = configuration.MinecraftServerKind switch {
|
||||||
|
MinecraftServerKind.Vanilla => new VanillaLauncher(properties),
|
||||||
|
MinecraftServerKind.Fabric => new FabricLauncher(properties),
|
||||||
|
_ => InvalidLauncher.Instance
|
||||||
|
};
|
||||||
|
|
||||||
|
if (instances.TryGetValue(instanceGuid, out var instance)) {
|
||||||
|
instances[instanceGuid] = instance with {
|
||||||
|
Configuration = configuration,
|
||||||
|
Launcher = launcher
|
||||||
|
};
|
||||||
|
|
||||||
|
Logger.Information("Reconfigured instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, instanceGuid);
|
||||||
|
|
||||||
|
if (alwaysReportStatus) {
|
||||||
|
instance.Actor.Tell(new InstanceActor.ReportInstanceStatusCommand());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var instanceActorInit = new InstanceActor.Init(instanceGuid, GetInstanceLoggerName(instanceGuid), instanceServices, agentState);
|
||||||
|
instances[instanceGuid] = instance = new InstanceInfo(instanceGuid, actorSystem.ActorOf(InstanceActor.Factory(instanceActorInit), "Instance-" + instanceGuid), configuration, launcher);
|
||||||
|
|
||||||
|
Logger.Information("Created instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, instanceGuid);
|
||||||
|
|
||||||
|
instance.Actor.Tell(new InstanceActor.ReportInstanceStatusCommand());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (launchNow) {
|
||||||
|
await LaunchInternal(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
return InstanceActionResult.Concrete(ConfigureInstanceResult.Success);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetInstanceLoggerName(Guid guid) {
|
||||||
|
var prefix = guid.ToString();
|
||||||
|
return prefix[..prefix.IndexOf('-')] + "/" + Interlocked.Increment(ref instanceLoggerSequenceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<InstanceActionResult<LaunchInstanceResult>> Launch(Guid instanceGuid) {
|
||||||
|
return AcquireSemaphoreAndRunWithInstance(instanceGuid, LaunchInternal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<LaunchInstanceResult> LaunchInternal(InstanceInfo instance) {
|
||||||
|
var ticket = instanceServices.InstanceTicketManager.Reserve(instance.Guid, instance.Configuration);
|
||||||
|
if (ticket is Result<InstanceTicketManager.Ticket, LaunchInstanceResult>.Fail fail) {
|
||||||
|
return fail.Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agentState.InstancesByGuid.TryGetValue(instance.Guid, out var i)) {
|
||||||
|
var status = i.Status;
|
||||||
|
if (status.IsRunning()) {
|
||||||
|
return LaunchInstanceResult.InstanceAlreadyRunning;
|
||||||
|
}
|
||||||
|
else if (status.IsLaunching()) {
|
||||||
|
return LaunchInstanceResult.InstanceAlreadyLaunching;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO report status?
|
||||||
|
instance.Actor.Tell(new InstanceActor.LaunchInstanceCommand(instance.Configuration, instance.Launcher, ticket.Value, false, shutdownCancellationToken));
|
||||||
|
return LaunchInstanceResult.LaunchInitiated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<InstanceActionResult<StopInstanceResult>> Stop(Guid instanceGuid, MinecraftStopStrategy stopStrategy) {
|
||||||
|
return AcquireSemaphoreAndRunWithInstance(instanceGuid, async instance => {
|
||||||
|
if (agentState.InstancesByGuid.TryGetValue(instance.Guid, out var i)) {
|
||||||
|
var status = i.Status;
|
||||||
|
if (status.IsStopping()) {
|
||||||
|
return StopInstanceResult.InstanceAlreadyStopping;
|
||||||
|
}
|
||||||
|
else if (!status.IsRunning()) {
|
||||||
|
return StopInstanceResult.InstanceAlreadyStopped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.Actor.Tell(new InstanceActor.StopInstanceCommand(stopStrategy, shutdownCancellationToken));
|
||||||
|
return StopInstanceResult.StopInitiated;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommand(Guid instanceGuid, string command) {
|
||||||
|
return AcquireSemaphoreAndRunWithInstance(instanceGuid, instance => instance.Actor.Request(new InstanceActor.SendCommandToInstanceCommand(command, shutdownCancellationToken), shutdownCancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RefreshAgentStatus() {
|
||||||
|
instanceServices.InstanceTicketManager.RefreshAgentStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync() {
|
||||||
|
Logger.Information("Stopping all instances...");
|
||||||
|
|
||||||
|
shutdownCancellationTokenSource.Cancel();
|
||||||
|
|
||||||
|
await semaphore.WaitAsync(CancellationToken.None);
|
||||||
|
await Task.WhenAll(instances.Values.Select(static instance => instance.Actor.Stop(new InstanceActor.ShutdownCommand())));
|
||||||
|
instances.Clear();
|
||||||
|
|
||||||
|
shutdownCancellationTokenSource.Dispose();
|
||||||
|
semaphore.Dispose();
|
||||||
|
|
||||||
|
Logger.Information("All instances stopped.");
|
||||||
|
}
|
||||||
|
}
|
@@ -4,19 +4,15 @@ using Phantom.Common.Data.Agent;
|
|||||||
using Phantom.Common.Data.Instance;
|
using Phantom.Common.Data.Instance;
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
using Phantom.Common.Messages.Agent.ToController;
|
using Phantom.Common.Messages.Agent.ToController;
|
||||||
using Phantom.Utils.Logging;
|
|
||||||
using Phantom.Utils.Tasks;
|
using Phantom.Utils.Tasks;
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
namespace Phantom.Agent.Services.Instances;
|
namespace Phantom.Agent.Services.Instances;
|
||||||
|
|
||||||
sealed class InstanceTicketManager {
|
sealed class InstanceTicketManager {
|
||||||
private static readonly ILogger Logger = PhantomLogger.Create<InstanceTicketManager>();
|
|
||||||
|
|
||||||
private readonly AgentInfo agentInfo;
|
private readonly AgentInfo agentInfo;
|
||||||
private readonly ControllerConnection controllerConnection;
|
private readonly ControllerConnection controllerConnection;
|
||||||
|
|
||||||
private readonly HashSet<Guid> activeTicketGuids = new ();
|
private readonly HashSet<Guid> runningInstanceGuids = new ();
|
||||||
private readonly HashSet<ushort> usedPorts = new ();
|
private readonly HashSet<ushort> usedPorts = new ();
|
||||||
private RamAllocationUnits usedMemory = new ();
|
private RamAllocationUnits usedMemory = new ();
|
||||||
|
|
||||||
@@ -25,7 +21,7 @@ sealed class InstanceTicketManager {
|
|||||||
this.controllerConnection = controllerConnection;
|
this.controllerConnection = controllerConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result<Ticket, LaunchInstanceResult> Reserve(InstanceConfiguration configuration) {
|
public Result<Ticket, LaunchInstanceResult> Reserve(Guid instanceGuid, InstanceConfiguration configuration) {
|
||||||
var memoryAllocation = configuration.MemoryAllocation;
|
var memoryAllocation = configuration.MemoryAllocation;
|
||||||
var serverPort = configuration.ServerPort;
|
var serverPort = configuration.ServerPort;
|
||||||
var rconPort = configuration.RconPort;
|
var rconPort = configuration.RconPort;
|
||||||
@@ -39,7 +35,7 @@ sealed class InstanceTicketManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lock (this) {
|
lock (this) {
|
||||||
if (activeTicketGuids.Count + 1 > agentInfo.MaxInstances) {
|
if (runningInstanceGuids.Count + 1 > agentInfo.MaxInstances) {
|
||||||
return LaunchInstanceResult.InstanceLimitExceeded;
|
return LaunchInstanceResult.InstanceLimitExceeded;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,29 +51,20 @@ sealed class InstanceTicketManager {
|
|||||||
return LaunchInstanceResult.RconPortAlreadyInUse;
|
return LaunchInstanceResult.RconPortAlreadyInUse;
|
||||||
}
|
}
|
||||||
|
|
||||||
var ticket = new Ticket(Guid.NewGuid(), memoryAllocation, serverPort, rconPort);
|
runningInstanceGuids.Add(instanceGuid);
|
||||||
|
|
||||||
activeTicketGuids.Add(ticket.TicketGuid);
|
|
||||||
usedMemory += memoryAllocation;
|
usedMemory += memoryAllocation;
|
||||||
usedPorts.Add(serverPort);
|
usedPorts.Add(serverPort);
|
||||||
usedPorts.Add(rconPort);
|
usedPorts.Add(rconPort);
|
||||||
|
|
||||||
RefreshAgentStatus();
|
RefreshAgentStatus();
|
||||||
Logger.Debug("Reserved ticket {TicketGuid} (server port {ServerPort}, rcon port {RconPort}, memory allocation {MemoryAllocation} MB).", ticket.TicketGuid, ticket.ServerPort, ticket.RconPort, ticket.MemoryAllocation.InMegabytes);
|
|
||||||
|
return new Ticket(instanceGuid, memoryAllocation, serverPort, rconPort);
|
||||||
return ticket;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsValid(Ticket ticket) {
|
|
||||||
lock (this) {
|
|
||||||
return activeTicketGuids.Contains(ticket.TicketGuid);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Release(Ticket ticket) {
|
public void Release(Ticket ticket) {
|
||||||
lock (this) {
|
lock (this) {
|
||||||
if (!activeTicketGuids.Remove(ticket.TicketGuid)) {
|
if (!runningInstanceGuids.Remove(ticket.InstanceGuid)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,15 +73,14 @@ sealed class InstanceTicketManager {
|
|||||||
usedPorts.Remove(ticket.RconPort);
|
usedPorts.Remove(ticket.RconPort);
|
||||||
|
|
||||||
RefreshAgentStatus();
|
RefreshAgentStatus();
|
||||||
Logger.Debug("Released ticket {TicketGuid} (server port {ServerPort}, rcon port {RconPort}, memory allocation {MemoryAllocation} MB).", ticket.TicketGuid, ticket.ServerPort, ticket.RconPort, ticket.MemoryAllocation.InMegabytes);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RefreshAgentStatus() {
|
public void RefreshAgentStatus() {
|
||||||
lock (this) {
|
lock (this) {
|
||||||
controllerConnection.Send(new ReportAgentStatusMessage(activeTicketGuids.Count, usedMemory));
|
controllerConnection.Send(new ReportAgentStatusMessage(runningInstanceGuids.Count, usedMemory));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record Ticket(Guid TicketGuid, RamAllocationUnits MemoryAllocation, ushort ServerPort, ushort RconPort);
|
public sealed record Ticket(Guid InstanceGuid, RamAllocationUnits MemoryAllocation, ushort ServerPort, ushort RconPort);
|
||||||
}
|
}
|
||||||
|
@@ -1,46 +1,50 @@
|
|||||||
using Phantom.Agent.Minecraft.Instance;
|
using Phantom.Agent.Minecraft.Instance;
|
||||||
using Phantom.Agent.Minecraft.Launcher;
|
using Phantom.Agent.Minecraft.Launcher;
|
||||||
using Phantom.Agent.Minecraft.Server;
|
using Phantom.Agent.Minecraft.Server;
|
||||||
using Phantom.Common.Data;
|
using Phantom.Agent.Services.Instances.States;
|
||||||
using Phantom.Common.Data.Instance;
|
using Phantom.Common.Data.Instance;
|
||||||
|
using Phantom.Utils.Tasks;
|
||||||
|
|
||||||
namespace Phantom.Agent.Services.Instances.State;
|
namespace Phantom.Agent.Services.Instances.Procedures;
|
||||||
|
|
||||||
static class InstanceLaunchProcedure {
|
static class InstanceLaunchProcedure {
|
||||||
public static async Task<InstanceRunningState?> Run(InstanceContext context, InstanceConfiguration configuration, IServerLauncher launcher, InstanceTicketManager ticketManager, InstanceTicketManager.Ticket ticket, Action<IInstanceStatus> reportStatus, CancellationToken cancellationToken) {
|
public static async Task<IInstanceState> Run(InstanceContext context, InstanceConfiguration configuration, IServerLauncher launcher, InstanceTicketManager.Ticket ticket, Action<IInstanceStatus> reportStatus, CancellationToken cancellationToken) {
|
||||||
context.Logger.Information("Session starting...");
|
context.Logger.Information("Session starting...");
|
||||||
|
|
||||||
Result<InstanceProcess, InstanceLaunchFailReason> result;
|
var newState = await TryLaunchInstance(context, configuration, launcher, ticket, reportStatus, cancellationToken);
|
||||||
|
if (newState is not InstanceRunningState) {
|
||||||
if (ticketManager.IsValid(ticket)) {
|
context.Services.InstanceTicketManager.Release(ticket);
|
||||||
try {
|
|
||||||
result = await LaunchInstance(context, launcher, reportStatus, cancellationToken);
|
|
||||||
} catch (OperationCanceledException) {
|
|
||||||
reportStatus(InstanceStatus.NotRunning);
|
|
||||||
return null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
context.Logger.Error(e, "Caught exception while launching instance.");
|
|
||||||
result = InstanceLaunchFailReason.UnknownError;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
context.Logger.Error("Attempted to launch instance with an invalid ticket!");
|
return newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IInstanceState> TryLaunchInstance(InstanceContext context, InstanceConfiguration configuration, IServerLauncher launcher, InstanceTicketManager.Ticket ticket, Action<IInstanceStatus> reportStatus, CancellationToken cancellationToken) {
|
||||||
|
Result<InstanceProcess, InstanceLaunchFailReason> result;
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = await LaunchInstanceImpl(context, launcher, reportStatus, cancellationToken);
|
||||||
|
} catch (OperationCanceledException) {
|
||||||
|
reportStatus(InstanceStatus.NotRunning);
|
||||||
|
return new InstanceNotRunningState();
|
||||||
|
} catch (Exception e) {
|
||||||
|
context.Logger.Error(e, "Caught exception while launching instance.");
|
||||||
result = InstanceLaunchFailReason.UnknownError;
|
result = InstanceLaunchFailReason.UnknownError;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
reportStatus(InstanceStatus.Running);
|
reportStatus(InstanceStatus.Running);
|
||||||
context.ReportEvent(InstanceEvent.LaunchSucceeded);
|
context.ReportEvent(InstanceEvent.LaunchSucceeded);
|
||||||
return new InstanceRunningState(context, configuration, launcher, ticket, result.Value, cancellationToken);
|
return new InstanceRunningState(context, configuration, launcher, ticket, result.Value);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
reportStatus(InstanceStatus.Failed(result.Error));
|
reportStatus(InstanceStatus.Failed(result.Error));
|
||||||
context.ReportEvent(new InstanceLaunchFailedEvent(result.Error));
|
context.ReportEvent(new InstanceLaunchFailedEvent(result.Error));
|
||||||
return null;
|
return new InstanceNotRunningState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<Result<InstanceProcess, InstanceLaunchFailReason>> LaunchInstance(InstanceContext context, IServerLauncher launcher, Action<IInstanceStatus> reportStatus, CancellationToken cancellationToken) {
|
private static async Task<Result<InstanceProcess, InstanceLaunchFailReason>> LaunchInstanceImpl(InstanceContext context, IServerLauncher launcher, Action<IInstanceStatus> reportStatus, CancellationToken cancellationToken) {
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
byte lastDownloadProgress = byte.MaxValue;
|
byte lastDownloadProgress = byte.MaxValue;
|
@@ -1,15 +1,16 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using Phantom.Agent.Minecraft.Command;
|
using Phantom.Agent.Minecraft.Command;
|
||||||
using Phantom.Agent.Minecraft.Instance;
|
using Phantom.Agent.Minecraft.Instance;
|
||||||
|
using Phantom.Agent.Services.Instances.States;
|
||||||
using Phantom.Common.Data.Instance;
|
using Phantom.Common.Data.Instance;
|
||||||
using Phantom.Common.Data.Minecraft;
|
using Phantom.Common.Data.Minecraft;
|
||||||
|
|
||||||
namespace Phantom.Agent.Services.Instances.State;
|
namespace Phantom.Agent.Services.Instances.Procedures;
|
||||||
|
|
||||||
static class InstanceStopProcedure {
|
static class InstanceStopProcedure {
|
||||||
private static readonly ushort[] Stops = { 60, 30, 10, 5, 4, 3, 2, 1, 0 };
|
private static readonly ushort[] Stops = { 60, 30, 10, 5, 4, 3, 2, 1, 0 };
|
||||||
|
|
||||||
public static async Task<bool> Run(InstanceContext context, MinecraftStopStrategy stopStrategy, InstanceRunningState runningState, Action<IInstanceStatus> reportStatus, CancellationToken cancellationToken) {
|
public static async Task<IInstanceState?> Run(InstanceContext context, MinecraftStopStrategy stopStrategy, InstanceRunningState runningState, Action<IInstanceStatus> reportStatus, CancellationToken cancellationToken) {
|
||||||
var process = runningState.Process;
|
var process = runningState.Process;
|
||||||
runningState.IsStopping = true;
|
runningState.IsStopping = true;
|
||||||
|
|
||||||
@@ -19,14 +20,12 @@ static class InstanceStopProcedure {
|
|||||||
await CountDownWithAnnouncements(context, process, seconds, cancellationToken);
|
await CountDownWithAnnouncements(context, process, seconds, cancellationToken);
|
||||||
} catch (OperationCanceledException) {
|
} catch (OperationCanceledException) {
|
||||||
runningState.IsStopping = false;
|
runningState.IsStopping = false;
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Too late to cancel the stop procedure now.
|
// Too late to cancel the stop procedure now.
|
||||||
runningState.OnStopInitiated();
|
|
||||||
|
|
||||||
if (!process.HasEnded) {
|
if (!process.HasEnded) {
|
||||||
context.Logger.Information("Session stopping now.");
|
context.Logger.Information("Session stopping now.");
|
||||||
await DoStop(context, process);
|
await DoStop(context, process);
|
||||||
@@ -37,7 +36,7 @@ static class InstanceStopProcedure {
|
|||||||
context.ReportEvent(InstanceEvent.Stopped);
|
context.ReportEvent(InstanceEvent.Stopped);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return new InstanceNotRunningState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task CountDownWithAnnouncements(InstanceContext context, InstanceProcess process, ushort seconds, CancellationToken cancellationToken) {
|
private static async Task CountDownWithAnnouncements(InstanceContext context, InstanceProcess process, ushort seconds, CancellationToken cancellationToken) {
|
||||||
@@ -77,17 +76,16 @@ static class InstanceStopProcedure {
|
|||||||
// Ignore.
|
// Ignore.
|
||||||
} catch (ObjectDisposedException e) when (e.ObjectName == typeof(Process).FullName && process.HasEnded) {
|
} catch (ObjectDisposedException e) when (e.ObjectName == typeof(Process).FullName && process.HasEnded) {
|
||||||
// Ignore.
|
// Ignore.
|
||||||
} catch (IOException e) when (e.HResult == -2147024664 /* The pipe is being closed */) {
|
|
||||||
// Ignore.
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
context.Logger.Warning(e, "Caught exception while sending stop command.");
|
context.Logger.Warning(e, "Caught exception while sending stop command.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task WaitForSessionToEnd(InstanceContext context, InstanceProcess process) {
|
private static async Task WaitForSessionToEnd(InstanceContext context, InstanceProcess process) {
|
||||||
|
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(55));
|
||||||
try {
|
try {
|
||||||
await process.WaitForExit(TimeSpan.FromSeconds(55));
|
await process.WaitForExit(timeout.Token);
|
||||||
} catch (TimeoutException) {
|
} catch (OperationCanceledException) {
|
||||||
try {
|
try {
|
||||||
context.Logger.Warning("Waiting timed out, killing session...");
|
context.Logger.Warning("Waiting timed out, killing session...");
|
||||||
process.Kill();
|
process.Kill();
|
@@ -1,140 +0,0 @@
|
|||||||
using Phantom.Agent.Minecraft.Instance;
|
|
||||||
using Phantom.Agent.Minecraft.Server;
|
|
||||||
using Phantom.Agent.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.Send(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();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -0,0 +1,5 @@
|
|||||||
|
namespace Phantom.Agent.Services.Instances.States;
|
||||||
|
|
||||||
|
interface IInstanceState {
|
||||||
|
void Initialize();
|
||||||
|
}
|
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Phantom.Agent.Services.Instances.States;
|
||||||
|
|
||||||
|
sealed class InstanceNotRunningState : IInstanceState {
|
||||||
|
public void Initialize() {}
|
||||||
|
|
||||||
|
public Task<bool> SendCommand(string command, CancellationToken cancellationToken) {
|
||||||
|
return Task.FromResult(false);
|
||||||
|
}
|
||||||
|
}
|
@@ -5,10 +5,9 @@ using Phantom.Common.Data.Backups;
|
|||||||
using Phantom.Common.Data.Instance;
|
using Phantom.Common.Data.Instance;
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
|
|
||||||
namespace Phantom.Agent.Services.Instances.State;
|
namespace Phantom.Agent.Services.Instances.States;
|
||||||
|
|
||||||
sealed class InstanceRunningState : IDisposable {
|
sealed class InstanceRunningState : IInstanceState, IDisposable {
|
||||||
public InstanceTicketManager.Ticket Ticket { get; }
|
|
||||||
public InstanceProcess Process { get; }
|
public InstanceProcess Process { get; }
|
||||||
|
|
||||||
internal bool IsStopping { get; set; }
|
internal bool IsStopping { get; set; }
|
||||||
@@ -16,26 +15,23 @@ sealed class InstanceRunningState : IDisposable {
|
|||||||
private readonly InstanceContext context;
|
private readonly InstanceContext context;
|
||||||
private readonly InstanceConfiguration configuration;
|
private readonly InstanceConfiguration configuration;
|
||||||
private readonly IServerLauncher launcher;
|
private readonly IServerLauncher launcher;
|
||||||
private readonly CancellationToken cancellationToken;
|
private readonly InstanceTicketManager.Ticket ticket;
|
||||||
|
|
||||||
private readonly InstanceLogSender logSender;
|
private readonly InstanceLogSender logSender;
|
||||||
private readonly InstancePlayerCountTracker playerCountTracker;
|
|
||||||
private readonly BackupScheduler backupScheduler;
|
private readonly BackupScheduler backupScheduler;
|
||||||
|
|
||||||
private bool isDisposed;
|
private bool isDisposed;
|
||||||
|
|
||||||
public InstanceRunningState(InstanceContext context, InstanceConfiguration configuration, IServerLauncher launcher, InstanceTicketManager.Ticket ticket, InstanceProcess process, CancellationToken cancellationToken) {
|
public InstanceRunningState(InstanceContext context, InstanceConfiguration configuration, IServerLauncher launcher, InstanceTicketManager.Ticket ticket, InstanceProcess process) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.configuration = configuration;
|
this.configuration = configuration;
|
||||||
this.launcher = launcher;
|
this.launcher = launcher;
|
||||||
this.Ticket = ticket;
|
this.ticket = ticket;
|
||||||
this.Process = process;
|
this.Process = process;
|
||||||
this.cancellationToken = cancellationToken;
|
|
||||||
|
|
||||||
this.logSender = new InstanceLogSender(context.Services.ControllerConnection, context.InstanceGuid, context.ShortName);
|
this.logSender = new InstanceLogSender(context.Services.ControllerConnection, context.Services.TaskManager, context.InstanceGuid, context.ShortName);
|
||||||
this.playerCountTracker = new InstancePlayerCountTracker(context, process, configuration.ServerPort);
|
|
||||||
|
|
||||||
this.backupScheduler = new BackupScheduler(context, playerCountTracker);
|
this.backupScheduler = new BackupScheduler(context, process, configuration.ServerPort);
|
||||||
this.backupScheduler.BackupCompleted += OnScheduledBackupCompleted;
|
this.backupScheduler.BackupCompleted += OnScheduledBackupCompleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,17 +60,13 @@ sealed class InstanceRunningState : IDisposable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cancellationToken.IsCancellationRequested) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsStopping) {
|
if (IsStopping) {
|
||||||
context.Actor.Tell(new InstanceActor.HandleProcessEndedCommand(InstanceStatus.NotRunning));
|
context.Actor.Tell(new InstanceActor.HandleProcessEndedCommand(InstanceStatus.NotRunning));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
context.Logger.Information("Session ended unexpectedly, restarting...");
|
context.Logger.Information("Session ended unexpectedly, restarting...");
|
||||||
context.ReportEvent(InstanceEvent.Crashed);
|
context.ReportEvent(InstanceEvent.Crashed);
|
||||||
context.Actor.Tell(new InstanceActor.LaunchInstanceCommand(configuration, launcher, Ticket, IsRestarting: true));
|
context.Actor.Tell(new InstanceActor.LaunchInstanceCommand(configuration, launcher, ticket, IsRestarting: true, CancellationToken.None /* TODO */));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,11 +87,6 @@ sealed class InstanceRunningState : IDisposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnStopInitiated() {
|
|
||||||
backupScheduler.Stop();
|
|
||||||
playerCountTracker.Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryDispose() {
|
private bool TryDispose() {
|
||||||
lock (this) {
|
lock (this) {
|
||||||
if (isDisposed) {
|
if (isDisposed) {
|
||||||
@@ -109,10 +96,11 @@ sealed class InstanceRunningState : IDisposable {
|
|||||||
isDisposed = true;
|
isDisposed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
OnStopInitiated();
|
|
||||||
logSender.Stop();
|
logSender.Stop();
|
||||||
|
backupScheduler.Stop();
|
||||||
|
|
||||||
Process.Dispose();
|
Process.Dispose();
|
||||||
|
context.Services.InstanceTicketManager.Release(ticket);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
@@ -1,6 +1,4 @@
|
|||||||
using Phantom.Agent.Services.Instances;
|
using Phantom.Common.Data.Instance;
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Data.Instance;
|
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
using Phantom.Common.Messages.Agent;
|
using Phantom.Common.Messages.Agent;
|
||||||
using Phantom.Common.Messages.Agent.BiDirectional;
|
using Phantom.Common.Messages.Agent.BiDirectional;
|
||||||
@@ -33,10 +31,10 @@ public sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToAgent
|
|||||||
|
|
||||||
ReceiveAsync<RegisterAgentSuccessMessage>(HandleRegisterAgentSuccess);
|
ReceiveAsync<RegisterAgentSuccessMessage>(HandleRegisterAgentSuccess);
|
||||||
Receive<RegisterAgentFailureMessage>(HandleRegisterAgentFailure);
|
Receive<RegisterAgentFailureMessage>(HandleRegisterAgentFailure);
|
||||||
ReceiveAndReplyLater<ConfigureInstanceMessage, Result<ConfigureInstanceResult, InstanceActionFailure>>(HandleConfigureInstance);
|
ReceiveAndReplyLater<ConfigureInstanceMessage, InstanceActionResult<ConfigureInstanceResult>>(HandleConfigureInstance);
|
||||||
ReceiveAndReplyLater<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(HandleLaunchInstance);
|
ReceiveAndReplyLater<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(HandleLaunchInstance);
|
||||||
ReceiveAndReplyLater<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(HandleStopInstance);
|
ReceiveAndReplyLater<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(HandleStopInstance);
|
||||||
ReceiveAndReplyLater<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(HandleSendCommandToInstance);
|
ReceiveAndReplyLater<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(HandleSendCommandToInstance);
|
||||||
Receive<ReplyMessage>(HandleReply);
|
Receive<ReplyMessage>(HandleReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +57,7 @@ public sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToAgent
|
|||||||
connection.SetIsReady();
|
connection.SetIsReady();
|
||||||
|
|
||||||
await connection.Send(new AdvertiseJavaRuntimesMessage(agent.JavaRuntimeRepository.All));
|
await connection.Send(new AdvertiseJavaRuntimesMessage(agent.JavaRuntimeRepository.All));
|
||||||
agent.InstanceTicketManager.RefreshAgentStatus();
|
agent.InstanceSessionManager.RefreshAgentStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleRegisterAgentFailure(RegisterAgentFailureMessage message) {
|
private void HandleRegisterAgentFailure(RegisterAgentFailureMessage message) {
|
||||||
@@ -75,24 +73,24 @@ public sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToAgent
|
|||||||
Environment.Exit(1);
|
Environment.Exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Result<ConfigureInstanceResult, InstanceActionFailure>> HandleConfigureInstance(ConfigureInstanceMessage message, bool alwaysReportStatus) {
|
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));
|
return agent.InstanceSessionManager.Configure(message.InstanceGuid, message.Configuration, message.LaunchProperties, message.LaunchNow, alwaysReportStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Result<ConfigureInstanceResult, InstanceActionFailure>> HandleConfigureInstance(ConfigureInstanceMessage message) {
|
private async Task<InstanceActionResult<ConfigureInstanceResult>> HandleConfigureInstance(ConfigureInstanceMessage message) {
|
||||||
return await HandleConfigureInstance(message, alwaysReportStatus: false);
|
return await HandleConfigureInstance(message, alwaysReportStatus: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Result<LaunchInstanceResult, InstanceActionFailure>> HandleLaunchInstance(LaunchInstanceMessage message) {
|
private async Task<InstanceActionResult<LaunchInstanceResult>> HandleLaunchInstance(LaunchInstanceMessage message) {
|
||||||
return await agent.InstanceManager.Request(new InstanceManagerActor.LaunchInstanceCommand(message.InstanceGuid));
|
return await agent.InstanceSessionManager.Launch(message.InstanceGuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Result<StopInstanceResult, InstanceActionFailure>> HandleStopInstance(StopInstanceMessage message) {
|
private async Task<InstanceActionResult<StopInstanceResult>> HandleStopInstance(StopInstanceMessage message) {
|
||||||
return await agent.InstanceManager.Request(new InstanceManagerActor.StopInstanceCommand(message.InstanceGuid, message.StopStrategy));
|
return await agent.InstanceSessionManager.Stop(message.InstanceGuid, message.StopStrategy);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) {
|
private async Task<InstanceActionResult<SendCommandToInstanceResult>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) {
|
||||||
return await agent.InstanceManager.Request(new InstanceManagerActor.SendCommandToInstanceCommand(message.InstanceGuid, message.Command));
|
return await agent.InstanceSessionManager.SendCommand(message.InstanceGuid, message.Command);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleReply(ReplyMessage message) {
|
private void HandleReply(ReplyMessage message) {
|
||||||
|
@@ -18,13 +18,12 @@ const int ProtocolVersion = 1;
|
|||||||
var shutdownCancellationTokenSource = new CancellationTokenSource();
|
var shutdownCancellationTokenSource = new CancellationTokenSource();
|
||||||
var shutdownCancellationToken = shutdownCancellationTokenSource.Token;
|
var shutdownCancellationToken = shutdownCancellationTokenSource.Token;
|
||||||
|
|
||||||
ProgramCulture.UseInvariantCulture();
|
|
||||||
ThreadPool.SetMinThreads(workerThreads: 2, completionPortThreads: 1);
|
|
||||||
|
|
||||||
PosixSignals.RegisterCancellation(shutdownCancellationTokenSource, static () => {
|
PosixSignals.RegisterCancellation(shutdownCancellationTokenSource, static () => {
|
||||||
PhantomLogger.Root.InformationHeading("Stopping Phantom Panel agent...");
|
PhantomLogger.Root.InformationHeading("Stopping Phantom Panel agent...");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ThreadPool.SetMinThreads(workerThreads: 2, completionPortThreads: 1);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var fullVersion = AssemblyAttributes.GetFullVersion(Assembly.GetExecutingAssembly());
|
var fullVersion = AssemblyAttributes.GetFullVersion(Assembly.GetExecutingAssembly());
|
||||||
|
|
||||||
|
@@ -7,7 +7,7 @@ public enum EventLogEventType {
|
|||||||
InstanceStopped,
|
InstanceStopped,
|
||||||
InstanceBackupSucceeded,
|
InstanceBackupSucceeded,
|
||||||
InstanceBackupSucceededWithWarnings,
|
InstanceBackupSucceededWithWarnings,
|
||||||
InstanceBackupFailed
|
InstanceBackupFailed,
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class EventLogEventTypeExtensions {
|
public static class EventLogEventTypeExtensions {
|
||||||
@@ -18,7 +18,7 @@ public static class EventLogEventTypeExtensions {
|
|||||||
{ EventLogEventType.InstanceStopped, EventLogSubjectType.Instance },
|
{ EventLogEventType.InstanceStopped, EventLogSubjectType.Instance },
|
||||||
{ EventLogEventType.InstanceBackupSucceeded, EventLogSubjectType.Instance },
|
{ EventLogEventType.InstanceBackupSucceeded, EventLogSubjectType.Instance },
|
||||||
{ EventLogEventType.InstanceBackupSucceededWithWarnings, EventLogSubjectType.Instance },
|
{ EventLogEventType.InstanceBackupSucceededWithWarnings, EventLogSubjectType.Instance },
|
||||||
{ EventLogEventType.InstanceBackupFailed, EventLogSubjectType.Instance }
|
{ EventLogEventType.InstanceBackupFailed, EventLogSubjectType.Instance },
|
||||||
};
|
};
|
||||||
|
|
||||||
static EventLogEventTypeExtensions() {
|
static EventLogEventTypeExtensions() {
|
||||||
|
@@ -8,10 +8,9 @@ public sealed partial record Instance(
|
|||||||
[property: MemoryPackOrder(0)] Guid InstanceGuid,
|
[property: MemoryPackOrder(0)] Guid InstanceGuid,
|
||||||
[property: MemoryPackOrder(1)] InstanceConfiguration Configuration,
|
[property: MemoryPackOrder(1)] InstanceConfiguration Configuration,
|
||||||
[property: MemoryPackOrder(2)] IInstanceStatus Status,
|
[property: MemoryPackOrder(2)] IInstanceStatus Status,
|
||||||
[property: MemoryPackOrder(3)] InstancePlayerCounts? PlayerCounts,
|
[property: MemoryPackOrder(3)] bool LaunchAutomatically
|
||||||
[property: MemoryPackOrder(4)] bool LaunchAutomatically
|
|
||||||
) {
|
) {
|
||||||
public static Instance Offline(Guid instanceGuid, InstanceConfiguration configuration, bool launchAutomatically = false) {
|
public static Instance Offline(Guid instanceGuid, InstanceConfiguration configuration, bool launchAutomatically = false) {
|
||||||
return new Instance(instanceGuid, configuration, InstanceStatus.Offline, PlayerCounts: null, launchAutomatically);
|
return new Instance(instanceGuid, configuration, InstanceStatus.Offline, launchAutomatically);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,24 +0,0 @@
|
|||||||
using System.Collections.Immutable;
|
|
||||||
using MemoryPack;
|
|
||||||
|
|
||||||
namespace Phantom.Common.Data.Web.Users;
|
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
|
||||||
public sealed partial record AuthenticatedUserInfo(
|
|
||||||
[property: MemoryPackOrder(0)] Guid Guid,
|
|
||||||
[property: MemoryPackOrder(1)] string Name,
|
|
||||||
[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);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -4,7 +4,8 @@ using MemoryPack;
|
|||||||
namespace Phantom.Common.Data.Web.Users;
|
namespace Phantom.Common.Data.Web.Users;
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record LogInSuccess(
|
public sealed partial record LogInSuccess (
|
||||||
[property: MemoryPackOrder(0)] AuthenticatedUserInfo UserInfo,
|
[property: MemoryPackOrder(0)] Guid UserGuid,
|
||||||
[property: MemoryPackOrder(1)] ImmutableArray<byte> AuthToken
|
[property: MemoryPackOrder(1)] PermissionSet Permissions,
|
||||||
|
[property: MemoryPackOrder(2)] ImmutableArray<byte> Token
|
||||||
);
|
);
|
||||||
|
@@ -14,9 +14,6 @@ public sealed record Permission(string Id, Permission? Parent) {
|
|||||||
return Register(id, this);
|
return Register(id, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public const string ManageAllAgentsPolicy = "Agents.ManageAll";
|
|
||||||
public static readonly Permission ManageAllAgents = Register(ManageAllAgentsPolicy);
|
|
||||||
|
|
||||||
public const string ViewInstancesPolicy = "Instances.View";
|
public const string ViewInstancesPolicy = "Instances.View";
|
||||||
public static readonly Permission ViewInstances = Register(ViewInstancesPolicy);
|
public static readonly Permission ViewInstances = Register(ViewInstancesPolicy);
|
||||||
|
|
||||||
|
@@ -1,5 +0,0 @@
|
|||||||
namespace Phantom.Common.Data.Web.Users;
|
|
||||||
|
|
||||||
public enum UserActionFailure {
|
|
||||||
NotAuthorized
|
|
||||||
}
|
|
@@ -1,25 +0,0 @@
|
|||||||
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;
|
|
@@ -14,7 +14,9 @@ public sealed partial class AuthToken {
|
|||||||
private readonly byte[] bytes;
|
private readonly byte[] bytes;
|
||||||
|
|
||||||
internal AuthToken(byte[]? bytes) {
|
internal AuthToken(byte[]? bytes) {
|
||||||
ArgumentNullException.ThrowIfNull(bytes);
|
if (bytes == null) {
|
||||||
|
throw new ArgumentNullException(nameof(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
if (bytes.Length != Length) {
|
if (bytes.Length != Length) {
|
||||||
throw new ArgumentOutOfRangeException(nameof(bytes), "Invalid token length: " + bytes.Length + ". Token length must be exactly " + Length + " bytes.");
|
throw new ArgumentOutOfRangeException(nameof(bytes), "Invalid token length: " + bytes.Length + ". Token length must be exactly " + Length + " bytes.");
|
||||||
|
@@ -1,16 +1,15 @@
|
|||||||
namespace Phantom.Common.Data.Backups;
|
namespace Phantom.Common.Data.Backups;
|
||||||
|
|
||||||
public enum BackupCreationResultKind : byte {
|
public enum BackupCreationResultKind : byte {
|
||||||
UnknownError = 0,
|
UnknownError,
|
||||||
Success = 1,
|
Success,
|
||||||
InstanceNotRunning = 2,
|
InstanceNotRunning,
|
||||||
BackupTimedOut = 3,
|
BackupCancelled,
|
||||||
BackupCancelled = 4,
|
BackupAlreadyRunning,
|
||||||
BackupAlreadyRunning = 5,
|
BackupFileAlreadyExists,
|
||||||
BackupFileAlreadyExists = 6,
|
CouldNotCreateBackupFolder,
|
||||||
CouldNotCreateBackupFolder = 7,
|
CouldNotCopyWorldToTemporaryFolder,
|
||||||
CouldNotCopyWorldToTemporaryFolder = 8,
|
CouldNotCreateWorldArchive
|
||||||
CouldNotCreateWorldArchive = 9
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class BackupCreationResultSummaryExtensions {
|
public static class BackupCreationResultSummaryExtensions {
|
||||||
|
@@ -9,10 +9,9 @@ namespace Phantom.Common.Data.Instance;
|
|||||||
[MemoryPackUnion(3, typeof(InstanceIsDownloading))]
|
[MemoryPackUnion(3, typeof(InstanceIsDownloading))]
|
||||||
[MemoryPackUnion(4, typeof(InstanceIsLaunching))]
|
[MemoryPackUnion(4, typeof(InstanceIsLaunching))]
|
||||||
[MemoryPackUnion(5, typeof(InstanceIsRunning))]
|
[MemoryPackUnion(5, typeof(InstanceIsRunning))]
|
||||||
[MemoryPackUnion(6, typeof(InstanceIsBackingUp))]
|
[MemoryPackUnion(6, typeof(InstanceIsRestarting))]
|
||||||
[MemoryPackUnion(7, typeof(InstanceIsRestarting))]
|
[MemoryPackUnion(7, typeof(InstanceIsStopping))]
|
||||||
[MemoryPackUnion(8, typeof(InstanceIsStopping))]
|
[MemoryPackUnion(8, typeof(InstanceIsFailed))]
|
||||||
[MemoryPackUnion(9, typeof(InstanceIsFailed))]
|
|
||||||
public partial interface IInstanceStatus {}
|
public partial interface IInstanceStatus {}
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
@@ -33,9 +32,6 @@ public sealed partial record InstanceIsLaunching : IInstanceStatus;
|
|||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record InstanceIsRunning : IInstanceStatus;
|
public sealed partial record InstanceIsRunning : IInstanceStatus;
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
|
||||||
public sealed partial record InstanceIsBackingUp : IInstanceStatus;
|
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record InstanceIsRestarting : IInstanceStatus;
|
public sealed partial record InstanceIsRestarting : IInstanceStatus;
|
||||||
|
|
||||||
@@ -50,7 +46,6 @@ public static class InstanceStatus {
|
|||||||
public static readonly IInstanceStatus NotRunning = new InstanceIsNotRunning();
|
public static readonly IInstanceStatus NotRunning = new InstanceIsNotRunning();
|
||||||
public static readonly IInstanceStatus Launching = new InstanceIsLaunching();
|
public static readonly IInstanceStatus Launching = new InstanceIsLaunching();
|
||||||
public static readonly IInstanceStatus Running = new InstanceIsRunning();
|
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 Restarting = new InstanceIsRestarting();
|
||||||
public static readonly IInstanceStatus Stopping = new InstanceIsStopping();
|
public static readonly IInstanceStatus Stopping = new InstanceIsStopping();
|
||||||
|
|
||||||
@@ -63,7 +58,7 @@ public static class InstanceStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsRunning(this IInstanceStatus status) {
|
public static bool IsRunning(this IInstanceStatus status) {
|
||||||
return status is InstanceIsRunning or InstanceIsBackingUp;
|
return status is InstanceIsRunning;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsStopping(this IInstanceStatus status) {
|
public static bool IsStopping(this IInstanceStatus status) {
|
||||||
@@ -75,10 +70,10 @@ public static class InstanceStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static bool CanStop(this IInstanceStatus status) {
|
public static bool CanStop(this IInstanceStatus status) {
|
||||||
return status.IsRunning() || status.IsLaunching();
|
return status is InstanceIsDownloading or InstanceIsLaunching or InstanceIsRunning;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool CanSendCommand(this IInstanceStatus status) {
|
public static bool CanSendCommand(this IInstanceStatus status) {
|
||||||
return status.IsRunning();
|
return status is InstanceIsRunning;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +0,0 @@
|
|||||||
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
|
|
||||||
);
|
|
@@ -2,6 +2,5 @@
|
|||||||
|
|
||||||
public enum MinecraftServerKind : ushort {
|
public enum MinecraftServerKind : ushort {
|
||||||
Vanilla = 1,
|
Vanilla = 1,
|
||||||
Fabric = 2,
|
Fabric = 2
|
||||||
Forge = 3
|
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +0,0 @@
|
|||||||
using MemoryPack;
|
|
||||||
|
|
||||||
namespace Phantom.Common.Data;
|
|
||||||
|
|
||||||
[MemoryPackable]
|
|
||||||
public readonly partial record struct Optional<T>(T? Value) {
|
|
||||||
public static implicit operator Optional<T>(T? value) {
|
|
||||||
return new Optional<T>(value);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,20 +0,0 @@
|
|||||||
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."
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Phantom.Common.Data.Replies;
|
||||||
|
|
||||||
|
public enum InstanceActionGeneralResult : byte {
|
||||||
|
None,
|
||||||
|
AgentDoesNotExist,
|
||||||
|
AgentShuttingDown,
|
||||||
|
AgentIsNotResponding,
|
||||||
|
InstanceDoesNotExist
|
||||||
|
}
|
42
Common/Phantom.Common.Data/Replies/InstanceActionResult.cs
Normal file
42
Common/Phantom.Common.Data/Replies/InstanceActionResult.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
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,108 +0,0 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using MemoryPack;
|
|
||||||
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.");
|
|
||||||
|
|
||||||
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,4 @@
|
|||||||
using Phantom.Common.Data;
|
using Phantom.Common.Data.Replies;
|
||||||
using Phantom.Common.Data.Replies;
|
|
||||||
using Phantom.Common.Messages.Agent.BiDirectional;
|
using Phantom.Common.Messages.Agent.BiDirectional;
|
||||||
using Phantom.Common.Messages.Agent.ToAgent;
|
using Phantom.Common.Messages.Agent.ToAgent;
|
||||||
using Phantom.Common.Messages.Agent.ToController;
|
using Phantom.Common.Messages.Agent.ToController;
|
||||||
@@ -17,10 +16,10 @@ public static class AgentMessageRegistries {
|
|||||||
static AgentMessageRegistries() {
|
static AgentMessageRegistries() {
|
||||||
ToAgent.Add<RegisterAgentSuccessMessage>(0);
|
ToAgent.Add<RegisterAgentSuccessMessage>(0);
|
||||||
ToAgent.Add<RegisterAgentFailureMessage>(1);
|
ToAgent.Add<RegisterAgentFailureMessage>(1);
|
||||||
ToAgent.Add<ConfigureInstanceMessage, Result<ConfigureInstanceResult, InstanceActionFailure>>(2);
|
ToAgent.Add<ConfigureInstanceMessage, InstanceActionResult<ConfigureInstanceResult>>(2);
|
||||||
ToAgent.Add<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(3);
|
ToAgent.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(3);
|
||||||
ToAgent.Add<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(4);
|
ToAgent.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(4);
|
||||||
ToAgent.Add<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(5);
|
ToAgent.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(5);
|
||||||
ToAgent.Add<ReplyMessage>(127);
|
ToAgent.Add<ReplyMessage>(127);
|
||||||
|
|
||||||
ToController.Add<RegisterAgentMessage>(0);
|
ToController.Add<RegisterAgentMessage>(0);
|
||||||
@@ -31,7 +30,6 @@ public static class AgentMessageRegistries {
|
|||||||
ToController.Add<InstanceOutputMessage>(5);
|
ToController.Add<InstanceOutputMessage>(5);
|
||||||
ToController.Add<ReportAgentStatusMessage>(6);
|
ToController.Add<ReportAgentStatusMessage>(6);
|
||||||
ToController.Add<ReportInstanceEventMessage>(7);
|
ToController.Add<ReportInstanceEventMessage>(7);
|
||||||
ToController.Add<ReportInstancePlayerCountsMessage>(8);
|
|
||||||
ToController.Add<ReplyMessage>(127);
|
ToController.Add<ReplyMessage>(127);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
using MemoryPack;
|
using MemoryPack;
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Data.Instance;
|
using Phantom.Common.Data.Instance;
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
@@ -12,4 +11,4 @@ public sealed partial record ConfigureInstanceMessage(
|
|||||||
[property: MemoryPackOrder(1)] InstanceConfiguration Configuration,
|
[property: MemoryPackOrder(1)] InstanceConfiguration Configuration,
|
||||||
[property: MemoryPackOrder(2)] InstanceLaunchProperties LaunchProperties,
|
[property: MemoryPackOrder(2)] InstanceLaunchProperties LaunchProperties,
|
||||||
[property: MemoryPackOrder(3)] bool LaunchNow = false
|
[property: MemoryPackOrder(3)] bool LaunchNow = false
|
||||||
) : IMessageToAgent, ICanReply<Result<ConfigureInstanceResult, InstanceActionFailure>>;
|
) : IMessageToAgent, ICanReply<InstanceActionResult<ConfigureInstanceResult>>;
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
using MemoryPack;
|
using MemoryPack;
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
@@ -8,4 +7,4 @@ namespace Phantom.Common.Messages.Agent.ToAgent;
|
|||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record LaunchInstanceMessage(
|
public sealed partial record LaunchInstanceMessage(
|
||||||
[property: MemoryPackOrder(0)] Guid InstanceGuid
|
[property: MemoryPackOrder(0)] Guid InstanceGuid
|
||||||
) : IMessageToAgent, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>;
|
) : IMessageToAgent, ICanReply<InstanceActionResult<LaunchInstanceResult>>;
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
using MemoryPack;
|
using MemoryPack;
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
@@ -9,4 +8,4 @@ namespace Phantom.Common.Messages.Agent.ToAgent;
|
|||||||
public sealed partial record SendCommandToInstanceMessage(
|
public sealed partial record SendCommandToInstanceMessage(
|
||||||
[property: MemoryPackOrder(0)] Guid InstanceGuid,
|
[property: MemoryPackOrder(0)] Guid InstanceGuid,
|
||||||
[property: MemoryPackOrder(1)] string Command
|
[property: MemoryPackOrder(1)] string Command
|
||||||
) : IMessageToAgent, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>;
|
) : IMessageToAgent, ICanReply<InstanceActionResult<SendCommandToInstanceResult>>;
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
using MemoryPack;
|
using MemoryPack;
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Data.Minecraft;
|
using Phantom.Common.Data.Minecraft;
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
@@ -10,4 +9,4 @@ namespace Phantom.Common.Messages.Agent.ToAgent;
|
|||||||
public sealed partial record StopInstanceMessage(
|
public sealed partial record StopInstanceMessage(
|
||||||
[property: MemoryPackOrder(0)] Guid InstanceGuid,
|
[property: MemoryPackOrder(0)] Guid InstanceGuid,
|
||||||
[property: MemoryPackOrder(1)] MinecraftStopStrategy StopStrategy
|
[property: MemoryPackOrder(1)] MinecraftStopStrategy StopStrategy
|
||||||
) : IMessageToAgent, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>;
|
) : IMessageToAgent, ICanReply<InstanceActionResult<StopInstanceResult>>;
|
||||||
|
@@ -1,10 +0,0 @@
|
|||||||
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,6 +1,5 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using MemoryPack;
|
using MemoryPack;
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Data.Web.Users;
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
@@ -8,8 +7,8 @@ namespace Phantom.Common.Messages.Web.ToController;
|
|||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record ChangeUserRolesMessage(
|
public sealed partial record ChangeUserRolesMessage(
|
||||||
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
|
||||||
[property: MemoryPackOrder(1)] Guid SubjectUserGuid,
|
[property: MemoryPackOrder(1)] Guid SubjectUserGuid,
|
||||||
[property: MemoryPackOrder(2)] ImmutableHashSet<Guid> AddToRoleGuids,
|
[property: MemoryPackOrder(2)] ImmutableHashSet<Guid> AddToRoleGuids,
|
||||||
[property: MemoryPackOrder(3)] ImmutableHashSet<Guid> RemoveFromRoleGuids
|
[property: MemoryPackOrder(3)] ImmutableHashSet<Guid> RemoveFromRoleGuids
|
||||||
) : IMessageToController, ICanReply<Result<ChangeUserRolesResult, UserActionFailure>>;
|
) : IMessageToController, ICanReply<ChangeUserRolesResult>;
|
||||||
|
@@ -1,16 +1,14 @@
|
|||||||
using System.Collections.Immutable;
|
using MemoryPack;
|
||||||
using MemoryPack;
|
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Data.Instance;
|
using Phantom.Common.Data.Instance;
|
||||||
|
using Phantom.Common.Data.Replies;
|
||||||
using Phantom.Common.Data.Web.Instance;
|
using Phantom.Common.Data.Web.Instance;
|
||||||
using Phantom.Common.Data.Web.Users;
|
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Web.ToController;
|
namespace Phantom.Common.Messages.Web.ToController;
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record CreateOrUpdateInstanceMessage(
|
public sealed partial record CreateOrUpdateInstanceMessage(
|
||||||
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
|
||||||
[property: MemoryPackOrder(1)] Guid InstanceGuid,
|
[property: MemoryPackOrder(1)] Guid InstanceGuid,
|
||||||
[property: MemoryPackOrder(2)] InstanceConfiguration Configuration
|
[property: MemoryPackOrder(2)] InstanceConfiguration Configuration
|
||||||
) : IMessageToController, ICanReply<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>;
|
) : IMessageToController, ICanReply<InstanceActionResult<CreateOrUpdateInstanceResult>>;
|
||||||
|
@@ -1,6 +1,4 @@
|
|||||||
using System.Collections.Immutable;
|
using MemoryPack;
|
||||||
using MemoryPack;
|
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Data.Web.Users;
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
@@ -8,7 +6,7 @@ namespace Phantom.Common.Messages.Web.ToController;
|
|||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record CreateUserMessage(
|
public sealed partial record CreateUserMessage(
|
||||||
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
|
||||||
[property: MemoryPackOrder(1)] string Username,
|
[property: MemoryPackOrder(1)] string Username,
|
||||||
[property: MemoryPackOrder(2)] string Password
|
[property: MemoryPackOrder(2)] string Password
|
||||||
) : IMessageToController, ICanReply<Result<CreateUserResult, UserActionFailure>>;
|
) : IMessageToController, ICanReply<CreateUserResult>;
|
||||||
|
@@ -1,6 +1,4 @@
|
|||||||
using System.Collections.Immutable;
|
using MemoryPack;
|
||||||
using MemoryPack;
|
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Data.Web.Users;
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
@@ -8,6 +6,6 @@ namespace Phantom.Common.Messages.Web.ToController;
|
|||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record DeleteUserMessage(
|
public sealed partial record DeleteUserMessage(
|
||||||
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
|
||||||
[property: MemoryPackOrder(1)] Guid SubjectUserGuid
|
[property: MemoryPackOrder(1)] Guid SubjectUserGuid
|
||||||
) : IMessageToController, ICanReply<Result<DeleteUserResult, UserActionFailure>>;
|
) : IMessageToController, ICanReply<DeleteUserResult>;
|
||||||
|
@@ -1,14 +1,11 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using MemoryPack;
|
using MemoryPack;
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Data.Web.AuditLog;
|
using Phantom.Common.Data.Web.AuditLog;
|
||||||
using Phantom.Common.Data.Web.Users;
|
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Web.ToController;
|
namespace Phantom.Common.Messages.Web.ToController;
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record GetAuditLogMessage(
|
public sealed partial record GetAuditLogMessage(
|
||||||
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
[property: MemoryPackOrder(0)] int Count
|
||||||
[property: MemoryPackOrder(1)] int Count
|
) : IMessageToController, ICanReply<ImmutableArray<AuditLogItem>>;
|
||||||
) : IMessageToController, ICanReply<Result<ImmutableArray<AuditLogItem>, UserActionFailure>>;
|
|
||||||
|
@@ -1,13 +0,0 @@
|
|||||||
using System.Collections.Immutable;
|
|
||||||
using MemoryPack;
|
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Data.Web.Users;
|
|
||||||
using Phantom.Utils.Actor;
|
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Web.ToController;
|
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
|
||||||
public sealed partial record GetAuthenticatedUser(
|
|
||||||
[property: MemoryPackOrder(0)] Guid UserGuid,
|
|
||||||
[property: MemoryPackOrder(1)] ImmutableArray<byte> AuthToken
|
|
||||||
) : IMessageToController, ICanReply<Optional<AuthenticatedUserInfo>>;
|
|
@@ -1,14 +1,11 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using MemoryPack;
|
using MemoryPack;
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Data.Web.EventLog;
|
using Phantom.Common.Data.Web.EventLog;
|
||||||
using Phantom.Common.Data.Web.Users;
|
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Web.ToController;
|
namespace Phantom.Common.Messages.Web.ToController;
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record GetEventLogMessage(
|
public sealed partial record GetEventLogMessage(
|
||||||
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
[property: MemoryPackOrder(0)] int Count
|
||||||
[property: MemoryPackOrder(1)] int Count
|
) : IMessageToController, ICanReply<ImmutableArray<EventLogItem>>;
|
||||||
) : IMessageToController, ICanReply<Result<ImmutableArray<EventLogItem>, UserActionFailure>>;
|
|
||||||
|
@@ -1,15 +1,12 @@
|
|||||||
using System.Collections.Immutable;
|
using MemoryPack;
|
||||||
using MemoryPack;
|
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
using Phantom.Common.Data.Web.Users;
|
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Web.ToController;
|
namespace Phantom.Common.Messages.Web.ToController;
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record LaunchInstanceMessage(
|
public sealed partial record LaunchInstanceMessage(
|
||||||
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
|
||||||
[property: MemoryPackOrder(1)] Guid AgentGuid,
|
[property: MemoryPackOrder(1)] Guid AgentGuid,
|
||||||
[property: MemoryPackOrder(2)] Guid InstanceGuid
|
[property: MemoryPackOrder(2)] Guid InstanceGuid
|
||||||
) : IMessageToController, ICanReply<Result<LaunchInstanceResult, UserInstanceActionFailure>>;
|
) : IMessageToController, ICanReply<InstanceActionResult<LaunchInstanceResult>>;
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
using MemoryPack;
|
using MemoryPack;
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Data.Web.Users;
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
@@ -9,4 +8,4 @@ namespace Phantom.Common.Messages.Web.ToController;
|
|||||||
public sealed partial record LogInMessage(
|
public sealed partial record LogInMessage(
|
||||||
[property: MemoryPackOrder(0)] string Username,
|
[property: MemoryPackOrder(0)] string Username,
|
||||||
[property: MemoryPackOrder(1)] string Password
|
[property: MemoryPackOrder(1)] string Password
|
||||||
) : IMessageToController, ICanReply<Optional<LogInSuccess>>;
|
) : IMessageToController, ICanReply<LogInSuccess?>;
|
||||||
|
@@ -1,10 +0,0 @@
|
|||||||
using System.Collections.Immutable;
|
|
||||||
using MemoryPack;
|
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Web.ToController;
|
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
|
||||||
public sealed partial record LogOutMessage(
|
|
||||||
[property: MemoryPackOrder(0)] Guid UserGuid,
|
|
||||||
[property: MemoryPackOrder(1)] ImmutableArray<byte> SessionToken
|
|
||||||
) : IMessageToController;
|
|
@@ -1,16 +1,13 @@
|
|||||||
using System.Collections.Immutable;
|
using MemoryPack;
|
||||||
using MemoryPack;
|
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
using Phantom.Common.Data.Web.Users;
|
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Web.ToController;
|
namespace Phantom.Common.Messages.Web.ToController;
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record SendCommandToInstanceMessage(
|
public sealed partial record SendCommandToInstanceMessage(
|
||||||
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
|
||||||
[property: MemoryPackOrder(1)] Guid AgentGuid,
|
[property: MemoryPackOrder(1)] Guid AgentGuid,
|
||||||
[property: MemoryPackOrder(2)] Guid InstanceGuid,
|
[property: MemoryPackOrder(2)] Guid InstanceGuid,
|
||||||
[property: MemoryPackOrder(3)] string Command
|
[property: MemoryPackOrder(3)] string Command
|
||||||
) : IMessageToController, ICanReply<Result<SendCommandToInstanceResult, UserInstanceActionFailure>>;
|
) : IMessageToController, ICanReply<InstanceActionResult<SendCommandToInstanceResult>>;
|
||||||
|
@@ -1,17 +1,14 @@
|
|||||||
using System.Collections.Immutable;
|
using MemoryPack;
|
||||||
using MemoryPack;
|
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Data.Minecraft;
|
using Phantom.Common.Data.Minecraft;
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
using Phantom.Common.Data.Web.Users;
|
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Web.ToController;
|
namespace Phantom.Common.Messages.Web.ToController;
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record StopInstanceMessage(
|
public sealed partial record StopInstanceMessage(
|
||||||
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
|
||||||
[property: MemoryPackOrder(1)] Guid AgentGuid,
|
[property: MemoryPackOrder(1)] Guid AgentGuid,
|
||||||
[property: MemoryPackOrder(2)] Guid InstanceGuid,
|
[property: MemoryPackOrder(2)] Guid InstanceGuid,
|
||||||
[property: MemoryPackOrder(3)] MinecraftStopStrategy StopStrategy
|
[property: MemoryPackOrder(3)] MinecraftStopStrategy StopStrategy
|
||||||
) : IMessageToController, ICanReply<Result<StopInstanceResult, UserInstanceActionFailure>>;
|
) : IMessageToController, ICanReply<InstanceActionResult<StopInstanceResult>>;
|
||||||
|
@@ -1,8 +0,0 @@
|
|||||||
using MemoryPack;
|
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Web.ToWeb;
|
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
|
||||||
public sealed partial record RefreshUserSessionMessage(
|
|
||||||
[property: MemoryPackOrder(0)] Guid UserGuid
|
|
||||||
) : IMessageToWeb;
|
|
@@ -1,5 +1,4 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Data.Java;
|
using Phantom.Common.Data.Java;
|
||||||
using Phantom.Common.Data.Minecraft;
|
using Phantom.Common.Data.Minecraft;
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
@@ -24,31 +23,28 @@ public static class WebMessageRegistries {
|
|||||||
static WebMessageRegistries() {
|
static WebMessageRegistries() {
|
||||||
ToController.Add<RegisterWebMessage>(0);
|
ToController.Add<RegisterWebMessage>(0);
|
||||||
ToController.Add<UnregisterWebMessage>(1);
|
ToController.Add<UnregisterWebMessage>(1);
|
||||||
ToController.Add<LogInMessage, Optional<LogInSuccess>>(2);
|
ToController.Add<LogInMessage, LogInSuccess?>(2);
|
||||||
ToController.Add<LogOutMessage>(3);
|
ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(3);
|
||||||
ToController.Add<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(4);
|
ToController.Add<CreateUserMessage, CreateUserResult>(4);
|
||||||
ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(5);
|
ToController.Add<DeleteUserMessage, DeleteUserResult>(5);
|
||||||
ToController.Add<CreateUserMessage, Result<CreateUserResult, UserActionFailure>>(6);
|
ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(6);
|
||||||
ToController.Add<DeleteUserMessage, Result<DeleteUserResult, UserActionFailure>>(7);
|
ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(7);
|
||||||
ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(8);
|
ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(8);
|
||||||
ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(9);
|
ToController.Add<ChangeUserRolesMessage, ChangeUserRolesResult>(9);
|
||||||
ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(10);
|
ToController.Add<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(10);
|
||||||
ToController.Add<ChangeUserRolesMessage, Result<ChangeUserRolesResult, UserActionFailure>>(11);
|
ToController.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(11);
|
||||||
ToController.Add<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(12);
|
ToController.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(12);
|
||||||
ToController.Add<LaunchInstanceMessage, Result<LaunchInstanceResult, UserInstanceActionFailure>>(13);
|
ToController.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(13);
|
||||||
ToController.Add<StopInstanceMessage, Result<StopInstanceResult, UserInstanceActionFailure>>(14);
|
ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(14);
|
||||||
ToController.Add<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>(15);
|
ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(15);
|
||||||
ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(16);
|
ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(16);
|
||||||
ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(17);
|
ToController.Add<GetEventLogMessage, ImmutableArray<EventLogItem>>(17);
|
||||||
ToController.Add<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(18);
|
|
||||||
ToController.Add<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(19);
|
|
||||||
ToController.Add<ReplyMessage>(127);
|
ToController.Add<ReplyMessage>(127);
|
||||||
|
|
||||||
ToWeb.Add<RegisterWebResultMessage>(0);
|
ToWeb.Add<RegisterWebResultMessage>(0);
|
||||||
ToWeb.Add<RefreshAgentsMessage>(1);
|
ToWeb.Add<RefreshAgentsMessage>(1);
|
||||||
ToWeb.Add<RefreshInstancesMessage>(2);
|
ToWeb.Add<RefreshInstancesMessage>(2);
|
||||||
ToWeb.Add<InstanceOutputMessage>(3);
|
ToWeb.Add<InstanceOutputMessage>(3);
|
||||||
ToWeb.Add<RefreshUserSessionMessage>(4);
|
|
||||||
ToWeb.Add<ReplyMessage>(127);
|
ToWeb.Add<ReplyMessage>(127);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,353 +0,0 @@
|
|||||||
// <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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,56 +0,0 @@
|
|||||||
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
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "8.0.0")
|
.HasAnnotation("ProductVersion", "7.0.11")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
@@ -204,21 +204,6 @@ namespace Phantom.Controller.Database.Postgres.Migrations
|
|||||||
b.ToTable("RolePermissions", "identity");
|
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 =>
|
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserEntity", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("UserGuid")
|
b.Property<Guid>("UserGuid")
|
||||||
@@ -296,21 +281,6 @@ namespace Phantom.Controller.Database.Postgres.Migrations
|
|||||||
.IsRequired();
|
.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 =>
|
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserPermissionEntity", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Phantom.Controller.Database.Entities.PermissionEntity", null)
|
b.HasOne("Phantom.Controller.Database.Entities.PermissionEntity", null)
|
||||||
|
@@ -13,19 +13,18 @@ namespace Phantom.Controller.Database;
|
|||||||
|
|
||||||
[SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")]
|
[SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")]
|
||||||
public class ApplicationDbContext : DbContext {
|
public class ApplicationDbContext : DbContext {
|
||||||
public DbSet<UserEntity> Users { get; init; } = null!;
|
public DbSet<UserEntity> Users { get; set; } = null!;
|
||||||
public DbSet<RoleEntity> Roles { get; init; } = null!;
|
public DbSet<RoleEntity> Roles { get; set; } = null!;
|
||||||
public DbSet<PermissionEntity> Permissions { get; init; } = null!;
|
public DbSet<PermissionEntity> Permissions { get; set; } = null!;
|
||||||
|
|
||||||
public DbSet<UserRoleEntity> UserRoles { get; init; } = null!;
|
public DbSet<UserRoleEntity> UserRoles { get; set; } = null!;
|
||||||
public DbSet<UserPermissionEntity> UserPermissions { get; init; } = null!;
|
public DbSet<UserPermissionEntity> UserPermissions { get; set; } = null!;
|
||||||
public DbSet<RolePermissionEntity> RolePermissions { get; init; } = null!;
|
public DbSet<RolePermissionEntity> RolePermissions { get; set; } = null!;
|
||||||
public DbSet<UserAgentAccessEntity> UserAgentAccess { get; init; } = null!;
|
|
||||||
|
public DbSet<AgentEntity> Agents { get; set; } = null!;
|
||||||
public DbSet<AgentEntity> Agents { get; init; } = null!;
|
public DbSet<InstanceEntity> Instances { get; set; } = null!;
|
||||||
public DbSet<InstanceEntity> Instances { get; init; } = null!;
|
public DbSet<AuditLogEntity> AuditLog { get; set; } = null!;
|
||||||
public DbSet<AuditLogEntity> AuditLog { get; init; } = null!;
|
public DbSet<EventLogEntity> EventLog { get; set; } = null!;
|
||||||
public DbSet<EventLogEntity> EventLog { get; init; } = null!;
|
|
||||||
|
|
||||||
public AgentEntityUpsert AgentUpsert { get; }
|
public AgentEntityUpsert AgentUpsert { get; }
|
||||||
public InstanceEntityUpsert InstanceUpsert { get; }
|
public InstanceEntityUpsert InstanceUpsert { get; }
|
||||||
@@ -63,12 +62,6 @@ public class ApplicationDbContext : DbContext {
|
|||||||
b.HasOne<RoleEntity>().WithMany().HasForeignKey(static e => e.RoleGuid).IsRequired().OnDelete(DeleteBehavior.Cascade);
|
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);
|
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) {
|
protected override void ConfigureConventions(ModelConfigurationBuilder builder) {
|
||||||
|
@@ -9,7 +9,7 @@ namespace Phantom.Controller.Database.Entities;
|
|||||||
[SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")]
|
[SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")]
|
||||||
public sealed class AgentEntity {
|
public sealed class AgentEntity {
|
||||||
[Key]
|
[Key]
|
||||||
public Guid AgentGuid { get; init; }
|
public Guid AgentGuid { get; set; }
|
||||||
|
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public ushort ProtocolVersion { get; set; }
|
public ushort ProtocolVersion { get; set; }
|
||||||
|
@@ -13,16 +13,16 @@ public class AuditLogEntity : IDisposable {
|
|||||||
[Key]
|
[Key]
|
||||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||||
public long Id { get; init; }
|
public long Id { get; set; }
|
||||||
|
|
||||||
public Guid? UserGuid { get; init; }
|
public Guid? UserGuid { get; set; }
|
||||||
public DateTime UtcTime { get; init; } // Note: Converting to UTC is not best practice, but for historical records it's good enough.
|
public DateTime UtcTime { get; set; } // Note: Converting to UTC is not best practice, but for historical records it's good enough.
|
||||||
public AuditLogEventType EventType { get; init; }
|
public AuditLogEventType EventType { get; set; }
|
||||||
public AuditLogSubjectType SubjectType { get; init; }
|
public AuditLogSubjectType SubjectType { get; set; }
|
||||||
public string SubjectId { get; init; }
|
public string SubjectId { get; set; }
|
||||||
public JsonDocument? Data { get; init; }
|
public JsonDocument? Data { get; set; }
|
||||||
|
|
||||||
public virtual UserEntity? User { get; init; }
|
public virtual UserEntity? User { get; set; }
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||||
internal AuditLogEntity() {
|
internal AuditLogEntity() {
|
||||||
|
@@ -11,14 +11,14 @@ namespace Phantom.Controller.Database.Entities;
|
|||||||
[SuppressMessage("ReSharper", "ClassWithVirtualMembersNeverInherited.Global")]
|
[SuppressMessage("ReSharper", "ClassWithVirtualMembersNeverInherited.Global")]
|
||||||
public sealed class EventLogEntity : IDisposable {
|
public sealed class EventLogEntity : IDisposable {
|
||||||
[Key]
|
[Key]
|
||||||
public Guid EventGuid { get; init; }
|
public Guid EventGuid { get; set; }
|
||||||
|
|
||||||
public DateTime UtcTime { get; init; } // Note: Converting to UTC is not best practice, but for historical records it's good enough.
|
public DateTime UtcTime { get; set; } // Note: Converting to UTC is not best practice, but for historical records it's good enough.
|
||||||
public Guid? AgentGuid { get; init; }
|
public Guid? AgentGuid { get; set; }
|
||||||
public EventLogEventType EventType { get; init; }
|
public EventLogEventType EventType { get; set; }
|
||||||
public EventLogSubjectType SubjectType { get; init; }
|
public EventLogSubjectType SubjectType { get; set; }
|
||||||
public string SubjectId { get; init; }
|
public string SubjectId { get; set; }
|
||||||
public JsonDocument? Data { get; init; }
|
public JsonDocument? Data { get; set; }
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||||
internal EventLogEntity() {
|
internal EventLogEntity() {
|
||||||
|
@@ -11,7 +11,7 @@ namespace Phantom.Controller.Database.Entities;
|
|||||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
|
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
|
||||||
public sealed class InstanceEntity {
|
public sealed class InstanceEntity {
|
||||||
[Key]
|
[Key]
|
||||||
public Guid InstanceGuid { get; init; }
|
public Guid InstanceGuid { get; set; }
|
||||||
|
|
||||||
public Guid AgentGuid { get; set; }
|
public Guid AgentGuid { get; set; }
|
||||||
|
|
||||||
|
@@ -6,7 +6,7 @@ namespace Phantom.Controller.Database.Entities;
|
|||||||
[Table("Permissions", Schema = "identity")]
|
[Table("Permissions", Schema = "identity")]
|
||||||
public sealed class PermissionEntity {
|
public sealed class PermissionEntity {
|
||||||
[Key]
|
[Key]
|
||||||
public string Id { get; init; }
|
public string Id { get; set; }
|
||||||
|
|
||||||
public PermissionEntity(string id) {
|
public PermissionEntity(string id) {
|
||||||
Id = id;
|
Id = id;
|
||||||
|
@@ -7,9 +7,9 @@ namespace Phantom.Controller.Database.Entities;
|
|||||||
[Table("Roles", Schema = "identity")]
|
[Table("Roles", Schema = "identity")]
|
||||||
public sealed class RoleEntity {
|
public sealed class RoleEntity {
|
||||||
[Key]
|
[Key]
|
||||||
public Guid RoleGuid { get; init; }
|
public Guid RoleGuid { get; set; }
|
||||||
|
|
||||||
public string Name { get; init; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
public RoleEntity(Guid roleGuid, string name) {
|
public RoleEntity(Guid roleGuid, string name) {
|
||||||
RoleGuid = roleGuid;
|
RoleGuid = roleGuid;
|
||||||
|
@@ -4,8 +4,8 @@ namespace Phantom.Controller.Database.Entities;
|
|||||||
|
|
||||||
[Table("RolePermissions", Schema = "identity")]
|
[Table("RolePermissions", Schema = "identity")]
|
||||||
public sealed class RolePermissionEntity {
|
public sealed class RolePermissionEntity {
|
||||||
public Guid RoleGuid { get; init; }
|
public Guid RoleGuid { get; set; }
|
||||||
public string PermissionId { get; init; }
|
public string PermissionId { get; set; }
|
||||||
|
|
||||||
public RolePermissionEntity(Guid roleGuid, string permissionId) {
|
public RolePermissionEntity(Guid roleGuid, string permissionId) {
|
||||||
RoleGuid = roleGuid;
|
RoleGuid = roleGuid;
|
||||||
|
@@ -1,14 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -7,9 +7,9 @@ namespace Phantom.Controller.Database.Entities;
|
|||||||
[Table("Users", Schema = "identity")]
|
[Table("Users", Schema = "identity")]
|
||||||
public sealed class UserEntity {
|
public sealed class UserEntity {
|
||||||
[Key]
|
[Key]
|
||||||
public Guid UserGuid { get; init; }
|
public Guid UserGuid { get; set; }
|
||||||
|
|
||||||
public string Name { get; init; }
|
public string Name { get; set; }
|
||||||
public string PasswordHash { get; set; }
|
public string PasswordHash { get; set; }
|
||||||
|
|
||||||
public UserEntity(Guid userGuid, string name, string passwordHash) {
|
public UserEntity(Guid userGuid, string name, string passwordHash) {
|
||||||
|
@@ -4,8 +4,8 @@ namespace Phantom.Controller.Database.Entities;
|
|||||||
|
|
||||||
[Table("UserPermissions", Schema = "identity")]
|
[Table("UserPermissions", Schema = "identity")]
|
||||||
public sealed class UserPermissionEntity {
|
public sealed class UserPermissionEntity {
|
||||||
public Guid UserGuid { get; init; }
|
public Guid UserGuid { get; set; }
|
||||||
public string PermissionId { get; init; }
|
public string PermissionId { get; set; }
|
||||||
|
|
||||||
public UserPermissionEntity(Guid userGuid, string permissionId) {
|
public UserPermissionEntity(Guid userGuid, string permissionId) {
|
||||||
UserGuid = userGuid;
|
UserGuid = userGuid;
|
||||||
|
@@ -4,11 +4,11 @@ namespace Phantom.Controller.Database.Entities;
|
|||||||
|
|
||||||
[Table("UserRoles", Schema = "identity")]
|
[Table("UserRoles", Schema = "identity")]
|
||||||
public sealed class UserRoleEntity {
|
public sealed class UserRoleEntity {
|
||||||
public Guid UserGuid { get; init; }
|
public Guid UserGuid { get; set; }
|
||||||
public Guid RoleGuid { get; init; }
|
public Guid RoleGuid { get; set; }
|
||||||
|
|
||||||
public UserEntity User { get; init; }
|
public UserEntity User { get; set; }
|
||||||
public RoleEntity Role { get; init; }
|
public RoleEntity Role { get; set; }
|
||||||
|
|
||||||
public UserRoleEntity(Guid userGuid, Guid roleGuid) {
|
public UserRoleEntity(Guid userGuid, Guid roleGuid) {
|
||||||
UserGuid = userGuid;
|
UserGuid = userGuid;
|
||||||
|
@@ -17,12 +17,11 @@ public sealed class EventLogRepository {
|
|||||||
db.Ctx.EventLog.Add(new EventLogEntity(eventGuid, utcTime, agentGuid, eventType, subjectId, extra));
|
db.Ctx.EventLog.Add(new EventLogEntity(eventGuid, utcTime, agentGuid, eventType, subjectId, extra));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<ImmutableArray<EventLogItem>> GetMostRecentItems(ImmutableHashSet<Guid> agentGuids, int count, CancellationToken cancellationToken) {
|
public Task<ImmutableArray<EventLogItem>> GetMostRecentItems(int count, CancellationToken cancellationToken) {
|
||||||
return db.Ctx
|
return db.Ctx
|
||||||
.EventLog
|
.EventLog
|
||||||
.AsQueryable()
|
.AsQueryable()
|
||||||
.OrderByDescending(static entity => entity.UtcTime)
|
.OrderByDescending(static entity => entity.UtcTime)
|
||||||
.Where(entity => entity.AgentGuid == null || agentGuids.Contains(entity.AgentGuid.Value))
|
|
||||||
.Take(count)
|
.Take(count)
|
||||||
.AsAsyncEnumerable()
|
.AsAsyncEnumerable()
|
||||||
.Select(static entity => new EventLogItem(entity.UtcTime, entity.AgentGuid, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data?.RootElement.ToString()))
|
.Select(static entity => new EventLogItem(entity.UtcTime, entity.AgentGuid, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data?.RootElement.ToString()))
|
||||||
|
@@ -1,35 +0,0 @@
|
|||||||
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 System.Collections.Immutable;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Data.Web.Users;
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Controller.Database.Entities;
|
using Phantom.Controller.Database.Entities;
|
||||||
using Phantom.Utils.Collections;
|
using Phantom.Utils.Collections;
|
||||||
|
using Phantom.Utils.Tasks;
|
||||||
|
|
||||||
namespace Phantom.Controller.Database.Repositories;
|
namespace Phantom.Controller.Database.Repositories;
|
||||||
|
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Data.Web.Users;
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Common.Data.Web.Users.AddUserErrors;
|
using Phantom.Common.Data.Web.Users.AddUserErrors;
|
||||||
using Phantom.Common.Data.Web.Users.PasswordRequirementViolations;
|
using Phantom.Common.Data.Web.Users.PasswordRequirementViolations;
|
||||||
using Phantom.Common.Data.Web.Users.UsernameRequirementViolations;
|
using Phantom.Common.Data.Web.Users.UsernameRequirementViolations;
|
||||||
using Phantom.Controller.Database.Entities;
|
using Phantom.Controller.Database.Entities;
|
||||||
using Phantom.Utils.Collections;
|
using Phantom.Utils.Collections;
|
||||||
|
using Phantom.Utils.Tasks;
|
||||||
|
|
||||||
namespace Phantom.Controller.Database.Repositories;
|
namespace Phantom.Controller.Database.Repositories;
|
||||||
|
|
||||||
|
@@ -13,13 +13,11 @@ using Phantom.Common.Data.Web.Minecraft;
|
|||||||
using Phantom.Common.Messages.Agent;
|
using Phantom.Common.Messages.Agent;
|
||||||
using Phantom.Common.Messages.Agent.ToAgent;
|
using Phantom.Common.Messages.Agent.ToAgent;
|
||||||
using Phantom.Controller.Database;
|
using Phantom.Controller.Database;
|
||||||
using Phantom.Controller.Database.Entities;
|
|
||||||
using Phantom.Controller.Minecraft;
|
using Phantom.Controller.Minecraft;
|
||||||
using Phantom.Controller.Services.Instances;
|
using Phantom.Controller.Services.Instances;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
using Phantom.Utils.Actor.Mailbox;
|
using Phantom.Utils.Actor.Mailbox;
|
||||||
using Phantom.Utils.Actor.Tasks;
|
using Phantom.Utils.Actor.Tasks;
|
||||||
using Phantom.Utils.Collections;
|
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
@@ -94,12 +92,11 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
|||||||
Receive<NotifyIsAliveCommand>(NotifyIsAlive);
|
Receive<NotifyIsAliveCommand>(NotifyIsAlive);
|
||||||
Receive<UpdateStatsCommand>(UpdateStats);
|
Receive<UpdateStatsCommand>(UpdateStats);
|
||||||
Receive<UpdateJavaRuntimesCommand>(UpdateJavaRuntimes);
|
Receive<UpdateJavaRuntimesCommand>(UpdateJavaRuntimes);
|
||||||
ReceiveAndReplyLater<CreateOrUpdateInstanceCommand, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstance);
|
ReceiveAndReplyLater<CreateOrUpdateInstanceCommand, InstanceActionResult<CreateOrUpdateInstanceResult>>(CreateOrUpdateInstance);
|
||||||
Receive<UpdateInstanceStatusCommand>(UpdateInstanceStatus);
|
Receive<UpdateInstanceStatusCommand>(UpdateInstanceStatus);
|
||||||
Receive<UpdateInstancePlayerCountsCommand>(UpdateInstancePlayerCounts);
|
ReceiveAndReplyLater<LaunchInstanceCommand, InstanceActionResult<LaunchInstanceResult>>(LaunchInstance);
|
||||||
ReceiveAndReplyLater<LaunchInstanceCommand, Result<LaunchInstanceResult, InstanceActionFailure>>(LaunchInstance);
|
ReceiveAndReplyLater<StopInstanceCommand, InstanceActionResult<StopInstanceResult>>(StopInstance);
|
||||||
ReceiveAndReplyLater<StopInstanceCommand, Result<StopInstanceResult, InstanceActionFailure>>(StopInstance);
|
ReceiveAndReplyLater<SendCommandToInstanceCommand, InstanceActionResult<SendCommandToInstanceResult>>(SendMinecraftCommand);
|
||||||
ReceiveAndReplyLater<SendCommandToInstanceCommand, Result<SendCommandToInstanceResult, InstanceActionFailure>>(SendMinecraftCommand);
|
|
||||||
Receive<ReceiveInstanceDataCommand>(ReceiveInstanceData);
|
Receive<ReceiveInstanceDataCommand>(ReceiveInstanceData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,20 +144,20 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Result<TReply, InstanceActionFailure>> RequestInstance<TCommand, TReply>(Guid instanceGuid, TCommand command) where TCommand : InstanceActor.ICommand, ICanReply<Result<TReply, InstanceActionFailure>> {
|
private Task<InstanceActionResult<TReply>> RequestInstance<TCommand, TReply>(Guid instanceGuid, TCommand command) where TCommand : InstanceActor.ICommand, ICanReply<InstanceActionResult<TReply>> {
|
||||||
if (instanceActorByGuid.TryGetValue(instanceGuid, out var instance)) {
|
if (instanceActorByGuid.TryGetValue(instanceGuid, out var instance)) {
|
||||||
return await instance.Request(command, cancellationToken);
|
return instance.Request(command, cancellationToken);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Logger.Warning("Could not deliver command {CommandType} to instance {InstanceGuid}, instance not found.", command.GetType().Name, instanceGuid);
|
Logger.Warning("Could not deliver command {CommandType} to instance {InstanceGuid}, instance not found.", command.GetType().Name, instanceGuid);
|
||||||
return InstanceActionFailure.InstanceDoesNotExist;
|
return Task.FromResult(InstanceActionResult.General<TReply>(InstanceActionGeneralResult.InstanceDoesNotExist));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ImmutableArray<ConfigureInstanceMessage>> PrepareInitialConfigurationMessages() {
|
private async Task<ImmutableArray<ConfigureInstanceMessage>> PrepareInitialConfigurationMessages() {
|
||||||
var configurationMessages = ImmutableArray.CreateBuilder<ConfigureInstanceMessage>();
|
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);
|
var serverExecutableInfo = await minecraftVersions.GetServerExecutableInfo(instanceConfiguration.MinecraftVersion, cancellationToken);
|
||||||
configurationMessages.Add(new ConfigureInstanceMessage(instanceGuid, instanceConfiguration, new InstanceLaunchProperties(serverExecutableInfo), launchAutomatically));
|
configurationMessages.Add(new ConfigureInstanceMessage(instanceGuid, instanceConfiguration, new InstanceLaunchProperties(serverExecutableInfo), launchAutomatically));
|
||||||
}
|
}
|
||||||
@@ -184,44 +181,34 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
|||||||
|
|
||||||
public sealed record UpdateJavaRuntimesCommand(ImmutableArray<TaggedJavaRuntime> JavaRuntimes) : ICommand;
|
public sealed record UpdateJavaRuntimesCommand(ImmutableArray<TaggedJavaRuntime> JavaRuntimes) : ICommand;
|
||||||
|
|
||||||
public sealed record CreateOrUpdateInstanceCommand(Guid LoggedInUserGuid, Guid InstanceGuid, InstanceConfiguration Configuration) : ICommand, ICanReply<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>;
|
public sealed record CreateOrUpdateInstanceCommand(Guid AuditLogUserGuid, Guid InstanceGuid, InstanceConfiguration Configuration) : ICommand, ICanReply<InstanceActionResult<CreateOrUpdateInstanceResult>>;
|
||||||
|
|
||||||
public sealed record UpdateInstanceStatusCommand(Guid InstanceGuid, IInstanceStatus Status) : ICommand;
|
public sealed record UpdateInstanceStatusCommand(Guid InstanceGuid, IInstanceStatus Status) : ICommand;
|
||||||
|
|
||||||
public sealed record UpdateInstancePlayerCountsCommand(Guid InstanceGuid, InstancePlayerCounts? PlayerCounts) : ICommand;
|
|
||||||
|
|
||||||
public sealed record LaunchInstanceCommand(Guid LoggedInUserGuid, Guid InstanceGuid) : ICommand, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>;
|
public sealed record LaunchInstanceCommand(Guid InstanceGuid, Guid AuditLogUserGuid) : ICommand, ICanReply<InstanceActionResult<LaunchInstanceResult>>;
|
||||||
|
|
||||||
public sealed record StopInstanceCommand(Guid LoggedInUserGuid, Guid InstanceGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>;
|
public sealed record StopInstanceCommand(Guid InstanceGuid, Guid AuditLogUserGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<InstanceActionResult<StopInstanceResult>>;
|
||||||
|
|
||||||
public sealed record SendCommandToInstanceCommand(Guid LoggedInUserGuid, Guid InstanceGuid, string Command) : ICommand, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>;
|
public sealed record SendCommandToInstanceCommand(Guid InstanceGuid, Guid AuditLogUserGuid, string Command) : ICommand, ICanReply<InstanceActionResult<SendCommandToInstanceResult>>;
|
||||||
|
|
||||||
public sealed record ReceiveInstanceDataCommand(Instance Instance) : ICommand, IJumpAhead;
|
public sealed record ReceiveInstanceDataCommand(Instance Instance) : ICommand, IJumpAhead;
|
||||||
|
|
||||||
private async Task Initialize(InitializeCommand command) {
|
private async Task Initialize(InitializeCommand command) {
|
||||||
ImmutableArray<InstanceEntity> instanceEntities;
|
await using var ctx = dbProvider.Eager();
|
||||||
await using (var ctx = dbProvider.Eager()) {
|
await foreach (var entity in ctx.Instances.Where(instance => instance.AgentGuid == agentGuid).AsAsyncEnumerable().WithCancellation(cancellationToken)) {
|
||||||
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(
|
var instanceConfiguration = new InstanceConfiguration(
|
||||||
instanceEntity.AgentGuid,
|
entity.AgentGuid,
|
||||||
instanceEntity.InstanceName,
|
entity.InstanceName,
|
||||||
instanceEntity.ServerPort,
|
entity.ServerPort,
|
||||||
instanceEntity.RconPort,
|
entity.RconPort,
|
||||||
instanceEntity.MinecraftVersion,
|
entity.MinecraftVersion,
|
||||||
instanceEntity.MinecraftServerKind,
|
entity.MinecraftServerKind,
|
||||||
instanceEntity.MemoryAllocation,
|
entity.MemoryAllocation,
|
||||||
instanceEntity.JavaRuntimeGuid,
|
entity.JavaRuntimeGuid,
|
||||||
JvmArgumentsHelper.Split(instanceEntity.JvmArguments)
|
JvmArgumentsHelper.Split(entity.JvmArguments)
|
||||||
);
|
);
|
||||||
|
|
||||||
CreateNewInstance(Instance.Offline(instanceEntity.InstanceGuid, instanceConfiguration, instanceEntity.LaunchAutomatically));
|
CreateNewInstance(Instance.Offline(entity.InstanceGuid, instanceConfiguration, entity.LaunchAutomatically));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,15 +270,15 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
|||||||
controllerState.UpdateAgentJavaRuntimes(agentGuid, javaRuntimes);
|
controllerState.UpdateAgentJavaRuntimes(agentGuid, javaRuntimes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>> CreateOrUpdateInstance(CreateOrUpdateInstanceCommand command) {
|
private Task<InstanceActionResult<CreateOrUpdateInstanceResult>> CreateOrUpdateInstance(CreateOrUpdateInstanceCommand command) {
|
||||||
var instanceConfiguration = command.Configuration;
|
var instanceConfiguration = command.Configuration;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(instanceConfiguration.InstanceName)) {
|
if (string.IsNullOrWhiteSpace(instanceConfiguration.InstanceName)) {
|
||||||
return Task.FromResult<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstanceResult.InstanceNameMustNotBeEmpty);
|
return Task.FromResult(InstanceActionResult.Concrete(CreateOrUpdateInstanceResult.InstanceNameMustNotBeEmpty));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instanceConfiguration.MemoryAllocation <= RamAllocationUnits.Zero) {
|
if (instanceConfiguration.MemoryAllocation <= RamAllocationUnits.Zero) {
|
||||||
return Task.FromResult<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstanceResult.InstanceMemoryMustNotBeZero);
|
return Task.FromResult(InstanceActionResult.Concrete(CreateOrUpdateInstanceResult.InstanceMemoryMustNotBeZero));
|
||||||
}
|
}
|
||||||
|
|
||||||
return minecraftVersions.GetServerExecutableInfo(instanceConfiguration.MinecraftVersion, cancellationToken)
|
return minecraftVersions.GetServerExecutableInfo(instanceConfiguration.MinecraftVersion, cancellationToken)
|
||||||
@@ -299,9 +286,9 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
|||||||
.Unwrap();
|
.Unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>> CreateOrUpdateInstance1(FileDownloadInfo? serverExecutableInfo, CreateOrUpdateInstanceCommand command) {
|
private Task<InstanceActionResult<CreateOrUpdateInstanceResult>> CreateOrUpdateInstance1(FileDownloadInfo? serverExecutableInfo, CreateOrUpdateInstanceCommand command) {
|
||||||
if (serverExecutableInfo == null) {
|
if (serverExecutableInfo == null) {
|
||||||
return Task.FromResult<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstanceResult.MinecraftVersionDownloadInfoNotFound);
|
return Task.FromResult(InstanceActionResult.Concrete(CreateOrUpdateInstanceResult.MinecraftVersionDownloadInfoNotFound));
|
||||||
}
|
}
|
||||||
|
|
||||||
var instanceConfiguration = command.Configuration;
|
var instanceConfiguration = command.Configuration;
|
||||||
@@ -311,13 +298,13 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
|||||||
instanceActorRef = CreateNewInstance(Instance.Offline(command.InstanceGuid, instanceConfiguration));
|
instanceActorRef = CreateNewInstance(Instance.Offline(command.InstanceGuid, instanceConfiguration));
|
||||||
}
|
}
|
||||||
|
|
||||||
var configureInstanceCommand = new InstanceActor.ConfigureInstanceCommand(command.LoggedInUserGuid, command.InstanceGuid, instanceConfiguration, new InstanceLaunchProperties(serverExecutableInfo), isCreatingInstance);
|
var configureInstanceCommand = new InstanceActor.ConfigureInstanceCommand(command.AuditLogUserGuid, command.InstanceGuid, instanceConfiguration, new InstanceLaunchProperties(serverExecutableInfo), isCreatingInstance);
|
||||||
|
|
||||||
return instanceActorRef.Request(configureInstanceCommand, cancellationToken)
|
return instanceActorRef.Request(configureInstanceCommand, cancellationToken)
|
||||||
.ContinueOnActor(CreateOrUpdateInstance2, configureInstanceCommand);
|
.ContinueOnActor(CreateOrUpdateInstance2, configureInstanceCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Result<CreateOrUpdateInstanceResult, InstanceActionFailure> CreateOrUpdateInstance2(Result<ConfigureInstanceResult, InstanceActionFailure> result, InstanceActor.ConfigureInstanceCommand command) {
|
private InstanceActionResult<CreateOrUpdateInstanceResult> CreateOrUpdateInstance2(InstanceActionResult<ConfigureInstanceResult> result, InstanceActor.ConfigureInstanceCommand command) {
|
||||||
var instanceGuid = command.InstanceGuid;
|
var instanceGuid = command.InstanceGuid;
|
||||||
var instanceName = command.Configuration.InstanceName;
|
var instanceName = command.Configuration.InstanceName;
|
||||||
var isCreating = command.IsCreatingInstance;
|
var isCreating = command.IsCreatingInstance;
|
||||||
@@ -325,40 +312,34 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
|||||||
if (result.Is(ConfigureInstanceResult.Success)) {
|
if (result.Is(ConfigureInstanceResult.Success)) {
|
||||||
string action = isCreating ? "Added" : "Edited";
|
string action = isCreating ? "Added" : "Edited";
|
||||||
string relation = isCreating ? "to agent" : "in agent";
|
string relation = isCreating ? "to agent" : "in agent";
|
||||||
|
|
||||||
Logger.Information(action + " instance \"{InstanceName}\" (GUID {InstanceGuid}) " + relation + " \"{AgentName}\".", instanceName, instanceGuid, configuration.AgentName);
|
Logger.Information(action + " instance \"{InstanceName}\" (GUID {InstanceGuid}) " + relation + " \"{AgentName}\".", instanceName, instanceGuid, configuration.AgentName);
|
||||||
|
|
||||||
return CreateOrUpdateInstanceResult.Success;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
string action = isCreating ? "adding" : "editing";
|
string action = isCreating ? "adding" : "editing";
|
||||||
string relation = isCreating ? "to agent" : "in agent";
|
string relation = isCreating ? "to agent" : "in agent";
|
||||||
string reason = result.Into(ConfigureInstanceResultExtensions.ToSentence, InstanceActionFailureExtensions.ToSentence);
|
Logger.Information("Failed " + action + " instance \"{InstanceName}\" (GUID {InstanceGuid}) " + relation + " \"{AgentName}\". {ErrorMessage}", instanceName, instanceGuid, configuration.AgentName, result.ToSentence(ConfigureInstanceResultExtensions.ToSentence));
|
||||||
|
|
||||||
Logger.Information("Failed " + action + " instance \"{InstanceName}\" (GUID {InstanceGuid}) " + relation + " \"{AgentName}\". {ErrorMessage}", instanceName, instanceGuid, configuration.AgentName, reason);
|
|
||||||
|
|
||||||
return CreateOrUpdateInstanceResult.UnknownError;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result.Map(static result => result switch {
|
||||||
|
ConfigureInstanceResult.Success => CreateOrUpdateInstanceResult.Success,
|
||||||
|
_ => CreateOrUpdateInstanceResult.UnknownError
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateInstanceStatus(UpdateInstanceStatusCommand command) {
|
private void UpdateInstanceStatus(UpdateInstanceStatusCommand command) {
|
||||||
TellInstance(command.InstanceGuid, new InstanceActor.SetStatusCommand(command.Status));
|
TellInstance(command.InstanceGuid, new InstanceActor.SetStatusCommand(command.Status));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateInstancePlayerCounts(UpdateInstancePlayerCountsCommand command) {
|
private Task<InstanceActionResult<LaunchInstanceResult>> LaunchInstance(LaunchInstanceCommand command) {
|
||||||
TellInstance(command.InstanceGuid, new InstanceActor.SetPlayerCountsCommand(command.PlayerCounts));
|
return RequestInstance<InstanceActor.LaunchInstanceCommand, LaunchInstanceResult>(command.InstanceGuid, new InstanceActor.LaunchInstanceCommand(command.AuditLogUserGuid));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Result<LaunchInstanceResult, InstanceActionFailure>> LaunchInstance(LaunchInstanceCommand command) {
|
private Task<InstanceActionResult<StopInstanceResult>> StopInstance(StopInstanceCommand command) {
|
||||||
return RequestInstance<InstanceActor.LaunchInstanceCommand, LaunchInstanceResult>(command.InstanceGuid, new InstanceActor.LaunchInstanceCommand(command.LoggedInUserGuid));
|
return RequestInstance<InstanceActor.StopInstanceCommand, StopInstanceResult>(command.InstanceGuid, new InstanceActor.StopInstanceCommand(command.AuditLogUserGuid, command.StopStrategy));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Result<StopInstanceResult, InstanceActionFailure>> StopInstance(StopInstanceCommand command) {
|
private Task<InstanceActionResult<SendCommandToInstanceResult>> SendMinecraftCommand(SendCommandToInstanceCommand command) {
|
||||||
return RequestInstance<InstanceActor.StopInstanceCommand, StopInstanceResult>(command.InstanceGuid, new InstanceActor.StopInstanceCommand(command.LoggedInUserGuid, command.StopStrategy));
|
return RequestInstance<InstanceActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(command.InstanceGuid, new InstanceActor.SendCommandToInstanceCommand(command.AuditLogUserGuid, command.Command));
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
private void ReceiveInstanceData(ReceiveInstanceDataCommand command) {
|
||||||
|
@@ -1,16 +1,13 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Immutable;
|
|
||||||
using Akka.Actor;
|
using Akka.Actor;
|
||||||
using Phantom.Common.Data;
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Agent;
|
using Phantom.Common.Data.Agent;
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
using Phantom.Common.Data.Web.Agent;
|
using Phantom.Common.Data.Web.Agent;
|
||||||
using Phantom.Common.Data.Web.Users;
|
|
||||||
using Phantom.Common.Messages.Agent;
|
using Phantom.Common.Messages.Agent;
|
||||||
using Phantom.Common.Messages.Agent.ToAgent;
|
using Phantom.Common.Messages.Agent.ToAgent;
|
||||||
using Phantom.Controller.Database;
|
using Phantom.Controller.Database;
|
||||||
using Phantom.Controller.Minecraft;
|
using Phantom.Controller.Minecraft;
|
||||||
using Phantom.Controller.Services.Users.Sessions;
|
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
@@ -25,19 +22,17 @@ sealed class AgentManager {
|
|||||||
private readonly AuthToken authToken;
|
private readonly AuthToken authToken;
|
||||||
private readonly ControllerState controllerState;
|
private readonly ControllerState controllerState;
|
||||||
private readonly MinecraftVersions minecraftVersions;
|
private readonly MinecraftVersions minecraftVersions;
|
||||||
private readonly UserLoginManager userLoginManager;
|
|
||||||
private readonly IDbContextProvider dbProvider;
|
private readonly IDbContextProvider dbProvider;
|
||||||
private readonly CancellationToken cancellationToken;
|
private readonly CancellationToken cancellationToken;
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<Guid, ActorRef<AgentActor.ICommand>> agentsByGuid = new ();
|
private readonly ConcurrentDictionary<Guid, ActorRef<AgentActor.ICommand>> agentsByGuid = new ();
|
||||||
private readonly Func<Guid, AgentConfiguration, ActorRef<AgentActor.ICommand>> addAgentActorFactory;
|
private readonly Func<Guid, AgentConfiguration, ActorRef<AgentActor.ICommand>> addAgentActorFactory;
|
||||||
|
|
||||||
public AgentManager(IActorRefFactory actorSystem, AuthToken authToken, ControllerState controllerState, MinecraftVersions minecraftVersions, UserLoginManager userLoginManager, IDbContextProvider dbProvider, CancellationToken cancellationToken) {
|
public AgentManager(IActorRefFactory actorSystem, AuthToken authToken, ControllerState controllerState, MinecraftVersions minecraftVersions, IDbContextProvider dbProvider, CancellationToken cancellationToken) {
|
||||||
this.actorSystem = actorSystem;
|
this.actorSystem = actorSystem;
|
||||||
this.authToken = authToken;
|
this.authToken = authToken;
|
||||||
this.controllerState = controllerState;
|
this.controllerState = controllerState;
|
||||||
this.minecraftVersions = minecraftVersions;
|
this.minecraftVersions = minecraftVersions;
|
||||||
this.userLoginManager = userLoginManager;
|
|
||||||
this.dbProvider = dbProvider;
|
this.dbProvider = dbProvider;
|
||||||
this.cancellationToken = cancellationToken;
|
this.cancellationToken = cancellationToken;
|
||||||
|
|
||||||
@@ -88,18 +83,12 @@ sealed class AgentManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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>> {
|
public async Task<InstanceActionResult<TReply>> DoInstanceAction<TCommand, TReply>(Guid agentGuid, TCommand command) where TCommand : class, AgentActor.ICommand, ICanReply<InstanceActionResult<TReply>> {
|
||||||
var loggedInUser = userLoginManager.GetLoggedInUser(authToken);
|
if (agentsByGuid.TryGetValue(agentGuid, out var agent)) {
|
||||||
if (!loggedInUser.HasAccessToAgent(agentGuid) || !loggedInUser.CheckPermission(requiredPermission)) {
|
return await agent.Request(command, cancellationToken);
|
||||||
return (UserInstanceActionFailure) UserActionFailure.NotAuthorized;
|
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
if (!agentsByGuid.TryGetValue(agentGuid, out var agent)) {
|
return InstanceActionResult.General<TReply>(InstanceActionGeneralResult.AgentDoesNotExist);
|
||||||
return (UserInstanceActionFailure) InstanceActionFailure.AgentDoesNotExist;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var command = commandFactoryFromLoggedInUserGuid(loggedInUser.Guid!.Value);
|
|
||||||
var result = await agent.Request(command, cancellationToken);
|
|
||||||
return result.MapError(static error => (UserInstanceActionFailure) error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,6 @@ using Phantom.Controller.Services.Events;
|
|||||||
using Phantom.Controller.Services.Instances;
|
using Phantom.Controller.Services.Instances;
|
||||||
using Phantom.Controller.Services.Rpc;
|
using Phantom.Controller.Services.Rpc;
|
||||||
using Phantom.Controller.Services.Users;
|
using Phantom.Controller.Services.Users;
|
||||||
using Phantom.Controller.Services.Users.Sessions;
|
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
using IMessageFromAgentToController = Phantom.Common.Messages.Agent.IMessageToController;
|
using IMessageFromAgentToController = Phantom.Common.Messages.Agent.IMessageToController;
|
||||||
@@ -25,19 +24,18 @@ public sealed class ControllerServices : IDisposable {
|
|||||||
private ControllerState ControllerState { get; }
|
private ControllerState ControllerState { get; }
|
||||||
private MinecraftVersions MinecraftVersions { get; }
|
private MinecraftVersions MinecraftVersions { get; }
|
||||||
|
|
||||||
private AuthenticatedUserCache AuthenticatedUserCache { get; }
|
|
||||||
private UserManager UserManager { get; }
|
|
||||||
private RoleManager RoleManager { get; }
|
|
||||||
private UserRoleManager UserRoleManager { get; }
|
|
||||||
private UserLoginManager UserLoginManager { get; }
|
|
||||||
private PermissionManager PermissionManager { get; }
|
|
||||||
|
|
||||||
private AgentManager AgentManager { get; }
|
private AgentManager AgentManager { get; }
|
||||||
private InstanceLogManager InstanceLogManager { get; }
|
private InstanceLogManager InstanceLogManager { get; }
|
||||||
|
|
||||||
private AuditLogManager AuditLogManager { get; }
|
|
||||||
private EventLogManager EventLogManager { get; }
|
private EventLogManager EventLogManager { 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; }
|
||||||
|
|
||||||
public IRegistrationHandler<IMessageToAgent, IMessageFromAgentToController, RegisterAgentMessage> AgentRegistrationHandler { get; }
|
public IRegistrationHandler<IMessageToAgent, IMessageFromAgentToController, RegisterAgentMessage> AgentRegistrationHandler { get; }
|
||||||
public IRegistrationHandler<IMessageToWeb, IMessageFromWebToController, RegisterWebMessage> WebRegistrationHandler { get; }
|
public IRegistrationHandler<IMessageToWeb, IMessageFromWebToController, RegisterWebMessage> WebRegistrationHandler { get; }
|
||||||
|
|
||||||
@@ -53,18 +51,17 @@ public sealed class ControllerServices : IDisposable {
|
|||||||
this.ControllerState = new ControllerState();
|
this.ControllerState = new ControllerState();
|
||||||
this.MinecraftVersions = new MinecraftVersions();
|
this.MinecraftVersions = new MinecraftVersions();
|
||||||
|
|
||||||
this.AuthenticatedUserCache = new AuthenticatedUserCache();
|
this.AgentManager = new AgentManager(ActorSystem, agentAuthToken, ControllerState, MinecraftVersions, dbProvider, cancellationToken);
|
||||||
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.AgentManager = new AgentManager(ActorSystem, agentAuthToken, ControllerState, MinecraftVersions, UserLoginManager, dbProvider, cancellationToken);
|
|
||||||
this.InstanceLogManager = new InstanceLogManager();
|
this.InstanceLogManager = new InstanceLogManager();
|
||||||
|
|
||||||
|
this.UserManager = new UserManager(dbProvider);
|
||||||
|
this.RoleManager = new RoleManager(dbProvider);
|
||||||
|
this.PermissionManager = new PermissionManager(dbProvider);
|
||||||
|
|
||||||
|
this.UserRoleManager = new UserRoleManager(dbProvider);
|
||||||
|
this.UserLoginManager = new UserLoginManager(UserManager, PermissionManager);
|
||||||
this.AuditLogManager = new AuditLogManager(dbProvider);
|
this.AuditLogManager = new AuditLogManager(dbProvider);
|
||||||
this.EventLogManager = new EventLogManager(ControllerState, ActorSystem, dbProvider, shutdownCancellationToken);
|
this.EventLogManager = new EventLogManager(ActorSystem, dbProvider, shutdownCancellationToken);
|
||||||
|
|
||||||
this.AgentRegistrationHandler = new AgentRegistrationHandler(AgentManager, InstanceLogManager, EventLogManager);
|
this.AgentRegistrationHandler = new AgentRegistrationHandler(AgentManager, InstanceLogManager, EventLogManager);
|
||||||
this.WebRegistrationHandler = new WebRegistrationHandler(webAuthToken, ControllerState, InstanceLogManager, UserManager, RoleManager, UserRoleManager, UserLoginManager, AuditLogManager, AgentManager, MinecraftVersions, EventLogManager);
|
this.WebRegistrationHandler = new WebRegistrationHandler(webAuthToken, ControllerState, InstanceLogManager, UserManager, RoleManager, UserRoleManager, UserLoginManager, AuditLogManager, AgentManager, MinecraftVersions, EventLogManager);
|
||||||
|
@@ -19,8 +19,6 @@ sealed class ControllerState {
|
|||||||
public ObservableState<ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>.Receiver AgentJavaRuntimesByGuidReceiver => agentJavaRuntimesByGuid.ReceiverSide;
|
public ObservableState<ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>.Receiver AgentJavaRuntimesByGuidReceiver => agentJavaRuntimesByGuid.ReceiverSide;
|
||||||
public ObservableState<ImmutableDictionary<Guid, Instance>>.Receiver InstancesByGuidReceiver => instancesByGuid.ReceiverSide;
|
public ObservableState<ImmutableDictionary<Guid, Instance>>.Receiver InstancesByGuidReceiver => instancesByGuid.ReceiverSide;
|
||||||
|
|
||||||
public event EventHandler<Guid>? UserUpdatedOrDeleted;
|
|
||||||
|
|
||||||
public void UpdateAgent(Agent agent) {
|
public void UpdateAgent(Agent agent) {
|
||||||
agentsByGuid.PublisherSide.Publish(static (agentsByGuid, agent) => agentsByGuid.SetItem(agent.AgentGuid, agent), agent);
|
agentsByGuid.PublisherSide.Publish(static (agentsByGuid, agent) => agentsByGuid.SetItem(agent.AgentGuid, agent), agent);
|
||||||
}
|
}
|
||||||
@@ -32,8 +30,4 @@ sealed class ControllerState {
|
|||||||
public void UpdateInstance(Instance instance) {
|
public void UpdateInstance(Instance instance) {
|
||||||
instancesByGuid.PublisherSide.Publish(static (instancesByGuid, instance) => instancesByGuid.SetItem(instance.InstanceGuid, instance), instance);
|
instancesByGuid.PublisherSide.Publish(static (instancesByGuid, instance) => instancesByGuid.SetItem(instance.InstanceGuid, instance), instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateOrDeleteUser(Guid userGuid) {
|
|
||||||
UserUpdatedOrDeleted?.Invoke(null, userGuid);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,23 +1,18 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using Akka.Actor;
|
using Akka.Actor;
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Data.Web.EventLog;
|
using Phantom.Common.Data.Web.EventLog;
|
||||||
using Phantom.Common.Data.Web.Users;
|
|
||||||
using Phantom.Controller.Database;
|
using Phantom.Controller.Database;
|
||||||
using Phantom.Controller.Database.Repositories;
|
using Phantom.Controller.Database.Repositories;
|
||||||
using Phantom.Controller.Services.Users.Sessions;
|
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
namespace Phantom.Controller.Services.Events;
|
namespace Phantom.Controller.Services.Events;
|
||||||
|
|
||||||
sealed partial class EventLogManager {
|
sealed partial class EventLogManager {
|
||||||
private readonly ControllerState controllerState;
|
|
||||||
private readonly ActorRef<EventLogDatabaseStorageActor.ICommand> databaseStorageActor;
|
private readonly ActorRef<EventLogDatabaseStorageActor.ICommand> databaseStorageActor;
|
||||||
private readonly IDbContextProvider dbProvider;
|
private readonly IDbContextProvider dbProvider;
|
||||||
private readonly CancellationToken cancellationToken;
|
private readonly CancellationToken cancellationToken;
|
||||||
|
|
||||||
public EventLogManager(ControllerState controllerState, IActorRefFactory actorSystem, IDbContextProvider dbProvider, CancellationToken cancellationToken) {
|
public EventLogManager(IActorRefFactory actorSystem, IDbContextProvider dbProvider, CancellationToken cancellationToken) {
|
||||||
this.controllerState = controllerState;
|
|
||||||
this.databaseStorageActor = actorSystem.ActorOf(EventLogDatabaseStorageActor.Factory(new EventLogDatabaseStorageActor.Init(dbProvider, cancellationToken)), "EventLogDatabaseStorage");
|
this.databaseStorageActor = actorSystem.ActorOf(EventLogDatabaseStorageActor.Factory(new EventLogDatabaseStorageActor.Init(dbProvider, cancellationToken)), "EventLogDatabaseStorage");
|
||||||
this.dbProvider = dbProvider;
|
this.dbProvider = dbProvider;
|
||||||
this.cancellationToken = cancellationToken;
|
this.cancellationToken = cancellationToken;
|
||||||
@@ -27,14 +22,8 @@ sealed partial class EventLogManager {
|
|||||||
databaseStorageActor.Tell(new EventLogDatabaseStorageActor.StoreEventCommand(eventGuid, utcTime, agentGuid, eventType, subjectId, extra));
|
databaseStorageActor.Tell(new EventLogDatabaseStorageActor.StoreEventCommand(eventGuid, utcTime, agentGuid, eventType, subjectId, extra));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<ImmutableArray<EventLogItem>, UserActionFailure>> GetMostRecentItems(LoggedInUser loggedInUser, int count) {
|
public async Task<ImmutableArray<EventLogItem>> GetMostRecentItems(int count) {
|
||||||
if (!loggedInUser.CheckPermission(Permission.ViewEvents)) {
|
|
||||||
return UserActionFailure.NotAuthorized;
|
|
||||||
}
|
|
||||||
|
|
||||||
var accessibleAgentGuids = loggedInUser.FilterAccessibleAgentGuids(controllerState.AgentsByGuid.Keys.ToImmutableHashSet());
|
|
||||||
|
|
||||||
await using var db = dbProvider.Lazy();
|
await using var db = dbProvider.Lazy();
|
||||||
return await new EventLogRepository(db).GetMostRecentItems(accessibleAgentGuids, count, cancellationToken);
|
return await new EventLogRepository(db).GetMostRecentItems(count, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
using Phantom.Common.Data;
|
using Phantom.Common.Data.Instance;
|
||||||
using Phantom.Common.Data.Instance;
|
|
||||||
using Phantom.Common.Data.Minecraft;
|
using Phantom.Common.Data.Minecraft;
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
using Phantom.Common.Data.Web.Instance;
|
using Phantom.Common.Data.Web.Instance;
|
||||||
@@ -26,7 +25,6 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
|
|||||||
|
|
||||||
private InstanceConfiguration configuration;
|
private InstanceConfiguration configuration;
|
||||||
private IInstanceStatus status;
|
private IInstanceStatus status;
|
||||||
private InstancePlayerCounts? playerCounts;
|
|
||||||
private bool launchAutomatically;
|
private bool launchAutomatically;
|
||||||
|
|
||||||
private readonly ActorRef<InstanceDatabaseStorageActor.ICommand> databaseStorageActor;
|
private readonly ActorRef<InstanceDatabaseStorageActor.ICommand> databaseStorageActor;
|
||||||
@@ -36,20 +34,19 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
|
|||||||
this.agentConnection = init.AgentConnection;
|
this.agentConnection = init.AgentConnection;
|
||||||
this.cancellationToken = init.CancellationToken;
|
this.cancellationToken = init.CancellationToken;
|
||||||
|
|
||||||
(this.instanceGuid, this.configuration, this.status, this.playerCounts, this.launchAutomatically) = init.Instance;
|
(this.instanceGuid, this.configuration, this.status, this.launchAutomatically) = init.Instance;
|
||||||
|
|
||||||
this.databaseStorageActor = Context.ActorOf(InstanceDatabaseStorageActor.Factory(new InstanceDatabaseStorageActor.Init(instanceGuid, init.DbProvider, init.CancellationToken)), "DatabaseStorage");
|
this.databaseStorageActor = Context.ActorOf(InstanceDatabaseStorageActor.Factory(new InstanceDatabaseStorageActor.Init(instanceGuid, init.DbProvider, init.CancellationToken)), "DatabaseStorage");
|
||||||
|
|
||||||
Receive<SetStatusCommand>(SetStatus);
|
Receive<SetStatusCommand>(SetStatus);
|
||||||
Receive<SetPlayerCountsCommand>(SetPlayerCounts);
|
ReceiveAsyncAndReply<ConfigureInstanceCommand, InstanceActionResult<ConfigureInstanceResult>>(ConfigureInstance);
|
||||||
ReceiveAsyncAndReply<ConfigureInstanceCommand, Result<ConfigureInstanceResult, InstanceActionFailure>>(ConfigureInstance);
|
ReceiveAsyncAndReply<LaunchInstanceCommand, InstanceActionResult<LaunchInstanceResult>>(LaunchInstance);
|
||||||
ReceiveAsyncAndReply<LaunchInstanceCommand, Result<LaunchInstanceResult, InstanceActionFailure>>(LaunchInstance);
|
ReceiveAsyncAndReply<StopInstanceCommand, InstanceActionResult<StopInstanceResult>>(StopInstance);
|
||||||
ReceiveAsyncAndReply<StopInstanceCommand, Result<StopInstanceResult, InstanceActionFailure>>(StopInstance);
|
ReceiveAsyncAndReply<SendCommandToInstanceCommand, InstanceActionResult<SendCommandToInstanceResult>>(SendMinecraftCommand);
|
||||||
ReceiveAsyncAndReply<SendCommandToInstanceCommand, Result<SendCommandToInstanceResult, InstanceActionFailure>>(SendMinecraftCommand);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void NotifyInstanceUpdated() {
|
private void NotifyInstanceUpdated() {
|
||||||
agentActor.Tell(new AgentActor.ReceiveInstanceDataCommand(new Instance(instanceGuid, configuration, status, playerCounts, launchAutomatically)));
|
agentActor.Tell(new AgentActor.ReceiveInstanceDataCommand(new Instance(instanceGuid, configuration, status, launchAutomatically)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetLaunchAutomatically(bool newValue) {
|
private void SetLaunchAutomatically(bool newValue) {
|
||||||
@@ -59,41 +56,29 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Result<TReply, InstanceActionFailure>> SendInstanceActionMessage<TMessage, TReply>(TMessage message) where TMessage : IMessageToAgent, ICanReply<Result<TReply, InstanceActionFailure>> {
|
private async Task<InstanceActionResult<TReply>> SendInstanceActionMessage<TMessage, TReply>(TMessage message) where TMessage : IMessageToAgent, ICanReply<InstanceActionResult<TReply>> {
|
||||||
var reply = await agentConnection.Send<TMessage, Result<TReply, InstanceActionFailure>>(message, TimeSpan.FromSeconds(10), cancellationToken);
|
var reply = await agentConnection.Send<TMessage, InstanceActionResult<TReply>>(message, TimeSpan.FromSeconds(10), cancellationToken);
|
||||||
return reply ?? InstanceActionFailure.AgentIsNotResponding;
|
return reply.DidNotReplyIfNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface ICommand {}
|
public interface ICommand {}
|
||||||
|
|
||||||
public sealed record SetStatusCommand(IInstanceStatus Status) : ICommand;
|
public sealed record SetStatusCommand(IInstanceStatus Status) : ICommand;
|
||||||
|
|
||||||
public sealed record SetPlayerCountsCommand(InstancePlayerCounts? PlayerCounts) : ICommand;
|
|
||||||
|
|
||||||
public sealed record ConfigureInstanceCommand(Guid AuditLogUserGuid, Guid InstanceGuid, InstanceConfiguration Configuration, InstanceLaunchProperties LaunchProperties, bool IsCreatingInstance) : ICommand, ICanReply<Result<ConfigureInstanceResult, InstanceActionFailure>>;
|
public sealed record ConfigureInstanceCommand(Guid AuditLogUserGuid, Guid InstanceGuid, InstanceConfiguration Configuration, InstanceLaunchProperties LaunchProperties, bool IsCreatingInstance) : ICommand, ICanReply<InstanceActionResult<ConfigureInstanceResult>>;
|
||||||
|
|
||||||
public sealed record LaunchInstanceCommand(Guid AuditLogUserGuid) : ICommand, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>;
|
public sealed record LaunchInstanceCommand(Guid AuditLogUserGuid) : ICommand, ICanReply<InstanceActionResult<LaunchInstanceResult>>;
|
||||||
|
|
||||||
public sealed record StopInstanceCommand(Guid AuditLogUserGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>;
|
public sealed record StopInstanceCommand(Guid AuditLogUserGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<InstanceActionResult<StopInstanceResult>>;
|
||||||
|
|
||||||
public sealed record SendCommandToInstanceCommand(Guid AuditLogUserGuid, string Command) : ICommand, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>;
|
public sealed record SendCommandToInstanceCommand(Guid AuditLogUserGuid, string Command) : ICommand, ICanReply<InstanceActionResult<SendCommandToInstanceResult>>;
|
||||||
|
|
||||||
private void SetStatus(SetStatusCommand command) {
|
private void SetStatus(SetStatusCommand command) {
|
||||||
status = command.Status;
|
status = command.Status;
|
||||||
|
|
||||||
if (!status.IsRunning() && status != InstanceStatus.Offline /* Guard against temporary disconnects */) {
|
|
||||||
playerCounts = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
NotifyInstanceUpdated();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetPlayerCounts(SetPlayerCountsCommand command) {
|
|
||||||
playerCounts = command.PlayerCounts;
|
|
||||||
NotifyInstanceUpdated();
|
NotifyInstanceUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Result<ConfigureInstanceResult, InstanceActionFailure>> ConfigureInstance(ConfigureInstanceCommand command) {
|
private async Task<InstanceActionResult<ConfigureInstanceResult>> ConfigureInstance(ConfigureInstanceCommand command) {
|
||||||
var message = new ConfigureInstanceMessage(command.InstanceGuid, command.Configuration, command.LaunchProperties);
|
var message = new ConfigureInstanceMessage(command.InstanceGuid, command.Configuration, command.LaunchProperties);
|
||||||
var result = await SendInstanceActionMessage<ConfigureInstanceMessage, ConfigureInstanceResult>(message);
|
var result = await SendInstanceActionMessage<ConfigureInstanceMessage, ConfigureInstanceResult>(message);
|
||||||
|
|
||||||
@@ -113,7 +98,7 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Result<LaunchInstanceResult, InstanceActionFailure>> LaunchInstance(LaunchInstanceCommand command) {
|
private async Task<InstanceActionResult<LaunchInstanceResult>> LaunchInstance(LaunchInstanceCommand command) {
|
||||||
var message = new LaunchInstanceMessage(instanceGuid);
|
var message = new LaunchInstanceMessage(instanceGuid);
|
||||||
var result = await SendInstanceActionMessage<LaunchInstanceMessage, LaunchInstanceResult>(message);
|
var result = await SendInstanceActionMessage<LaunchInstanceMessage, LaunchInstanceResult>(message);
|
||||||
|
|
||||||
@@ -125,7 +110,7 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Result<StopInstanceResult, InstanceActionFailure>> StopInstance(StopInstanceCommand command) {
|
private async Task<InstanceActionResult<StopInstanceResult>> StopInstance(StopInstanceCommand command) {
|
||||||
var message = new StopInstanceMessage(instanceGuid, command.StopStrategy);
|
var message = new StopInstanceMessage(instanceGuid, command.StopStrategy);
|
||||||
var result = await SendInstanceActionMessage<StopInstanceMessage, StopInstanceResult>(message);
|
var result = await SendInstanceActionMessage<StopInstanceMessage, StopInstanceResult>(message);
|
||||||
|
|
||||||
@@ -137,7 +122,7 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> SendMinecraftCommand(SendCommandToInstanceCommand command) {
|
private async Task<InstanceActionResult<SendCommandToInstanceResult>> SendMinecraftCommand(SendCommandToInstanceCommand command) {
|
||||||
var message = new SendCommandToInstanceMessage(instanceGuid, command.Command);
|
var message = new SendCommandToInstanceMessage(instanceGuid, command.Command);
|
||||||
var result = await SendInstanceActionMessage<SendCommandToInstanceMessage, SendCommandToInstanceResult>(message);
|
var result = await SendInstanceActionMessage<SendCommandToInstanceMessage, SendCommandToInstanceResult>(message);
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
using Phantom.Common.Data.Replies;
|
using Akka.Actor;
|
||||||
|
using Phantom.Common.Data.Replies;
|
||||||
using Phantom.Common.Messages.Agent;
|
using Phantom.Common.Messages.Agent;
|
||||||
using Phantom.Common.Messages.Agent.BiDirectional;
|
using Phantom.Common.Messages.Agent.BiDirectional;
|
||||||
using Phantom.Common.Messages.Agent.ToAgent;
|
using Phantom.Common.Messages.Agent.ToAgent;
|
||||||
@@ -18,6 +19,8 @@ sealed class AgentMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
return Props<IMessageToController>.Create(() => new AgentMessageHandlerActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume });
|
return Props<IMessageToController>.Create(() => new AgentMessageHandlerActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IStash Stash { get; set; } = null!;
|
||||||
|
|
||||||
private readonly Guid agentGuid;
|
private readonly Guid agentGuid;
|
||||||
private readonly RpcConnectionToClient<IMessageToAgent> connection;
|
private readonly RpcConnectionToClient<IMessageToAgent> connection;
|
||||||
private readonly AgentRegistrationHandler agentRegistrationHandler;
|
private readonly AgentRegistrationHandler agentRegistrationHandler;
|
||||||
@@ -39,7 +42,6 @@ sealed class AgentMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
Receive<AdvertiseJavaRuntimesMessage>(HandleAdvertiseJavaRuntimes);
|
Receive<AdvertiseJavaRuntimesMessage>(HandleAdvertiseJavaRuntimes);
|
||||||
Receive<ReportAgentStatusMessage>(HandleReportAgentStatus);
|
Receive<ReportAgentStatusMessage>(HandleReportAgentStatus);
|
||||||
Receive<ReportInstanceStatusMessage>(HandleReportInstanceStatus);
|
Receive<ReportInstanceStatusMessage>(HandleReportInstanceStatus);
|
||||||
Receive<ReportInstancePlayerCountsMessage>(HandleReportInstancePlayerCounts);
|
|
||||||
Receive<ReportInstanceEventMessage>(HandleReportInstanceEvent);
|
Receive<ReportInstanceEventMessage>(HandleReportInstanceEvent);
|
||||||
Receive<InstanceOutputMessage>(HandleInstanceOutput);
|
Receive<InstanceOutputMessage>(HandleInstanceOutput);
|
||||||
Receive<ReplyMessage>(HandleReply);
|
Receive<ReplyMessage>(HandleReply);
|
||||||
@@ -75,10 +77,6 @@ sealed class AgentMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
agentManager.TellAgent(agentGuid, new AgentActor.UpdateInstanceStatusCommand(message.InstanceGuid, message.InstanceStatus));
|
agentManager.TellAgent(agentGuid, new AgentActor.UpdateInstanceStatusCommand(message.InstanceGuid, message.InstanceStatus));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleReportInstancePlayerCounts(ReportInstancePlayerCountsMessage message) {
|
|
||||||
agentManager.TellAgent(agentGuid, new AgentActor.UpdateInstancePlayerCountsCommand(message.InstanceGuid, message.PlayerCounts));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleReportInstanceEvent(ReportInstanceEventMessage message) {
|
private void HandleReportInstanceEvent(ReportInstanceEventMessage message) {
|
||||||
message.Event.Accept(eventLogManager.CreateInstanceEventVisitor(message.EventGuid, message.UtcTime, agentGuid, message.InstanceGuid));
|
message.Event.Accept(eventLogManager.CreateInstanceEventVisitor(message.EventGuid, message.UtcTime, agentGuid, message.InstanceGuid));
|
||||||
}
|
}
|
||||||
|
@@ -30,31 +30,22 @@ sealed class WebMessageDataUpdateSenderActor : ReceiveActor<WebMessageDataUpdate
|
|||||||
ReceiveAsync<RefreshAgentsCommand>(RefreshAgents);
|
ReceiveAsync<RefreshAgentsCommand>(RefreshAgents);
|
||||||
ReceiveAsync<RefreshInstancesCommand>(RefreshInstances);
|
ReceiveAsync<RefreshInstancesCommand>(RefreshInstances);
|
||||||
ReceiveAsync<ReceiveInstanceLogsCommand>(ReceiveInstanceLogs);
|
ReceiveAsync<ReceiveInstanceLogsCommand>(ReceiveInstanceLogs);
|
||||||
ReceiveAsync<RefreshUserSessionCommand>(RefreshUserSession);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void PreStart() {
|
protected override void PreStart() {
|
||||||
controllerState.AgentsByGuidReceiver.Register(SelfTyped, static state => new RefreshAgentsCommand(state));
|
controllerState.AgentsByGuidReceiver.Register(SelfTyped, static state => new RefreshAgentsCommand(state));
|
||||||
controllerState.InstancesByGuidReceiver.Register(SelfTyped, static state => new RefreshInstancesCommand(state));
|
controllerState.InstancesByGuidReceiver.Register(SelfTyped, static state => new RefreshInstancesCommand(state));
|
||||||
|
|
||||||
controllerState.UserUpdatedOrDeleted += OnUserUpdatedOrDeleted;
|
|
||||||
|
|
||||||
instanceLogManager.LogsReceived += OnInstanceLogsReceived;
|
instanceLogManager.LogsReceived += OnInstanceLogsReceived;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void PostStop() {
|
protected override void PostStop() {
|
||||||
instanceLogManager.LogsReceived -= OnInstanceLogsReceived;
|
instanceLogManager.LogsReceived -= OnInstanceLogsReceived;
|
||||||
|
|
||||||
controllerState.UserUpdatedOrDeleted -= OnUserUpdatedOrDeleted;
|
|
||||||
|
|
||||||
controllerState.AgentsByGuidReceiver.Unregister(SelfTyped);
|
controllerState.AgentsByGuidReceiver.Unregister(SelfTyped);
|
||||||
controllerState.InstancesByGuidReceiver.Unregister(SelfTyped);
|
controllerState.InstancesByGuidReceiver.Unregister(SelfTyped);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnUserUpdatedOrDeleted(object? sender, Guid userGuid) {
|
|
||||||
selfCached.Tell(new RefreshUserSessionCommand(userGuid));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnInstanceLogsReceived(object? sender, InstanceLogManager.Event e) {
|
private void OnInstanceLogsReceived(object? sender, InstanceLogManager.Event e) {
|
||||||
selfCached.Tell(new ReceiveInstanceLogsCommand(e.InstanceGuid, e.Lines));
|
selfCached.Tell(new ReceiveInstanceLogsCommand(e.InstanceGuid, e.Lines));
|
||||||
}
|
}
|
||||||
@@ -66,8 +57,6 @@ sealed class WebMessageDataUpdateSenderActor : ReceiveActor<WebMessageDataUpdate
|
|||||||
private sealed record RefreshInstancesCommand(ImmutableDictionary<Guid, Instance> Instances) : ICommand;
|
private sealed record RefreshInstancesCommand(ImmutableDictionary<Guid, Instance> Instances) : ICommand;
|
||||||
|
|
||||||
private sealed record ReceiveInstanceLogsCommand(Guid InstanceGuid, ImmutableArray<string> Lines) : ICommand;
|
private sealed record ReceiveInstanceLogsCommand(Guid InstanceGuid, ImmutableArray<string> Lines) : ICommand;
|
||||||
|
|
||||||
private sealed record RefreshUserSessionCommand(Guid UserGuid) : ICommand;
|
|
||||||
|
|
||||||
private Task RefreshAgents(RefreshAgentsCommand command) {
|
private Task RefreshAgents(RefreshAgentsCommand command) {
|
||||||
return connection.Send(new RefreshAgentsMessage(command.Agents.Values.ToImmutableArray()));
|
return connection.Send(new RefreshAgentsMessage(command.Agents.Values.ToImmutableArray()));
|
||||||
@@ -80,8 +69,4 @@ sealed class WebMessageDataUpdateSenderActor : ReceiveActor<WebMessageDataUpdate
|
|||||||
private Task ReceiveInstanceLogs(ReceiveInstanceLogsCommand command) {
|
private Task ReceiveInstanceLogs(ReceiveInstanceLogsCommand command) {
|
||||||
return connection.Send(new InstanceOutputMessage(command.InstanceGuid, command.Lines));
|
return connection.Send(new InstanceOutputMessage(command.InstanceGuid, command.Lines));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task RefreshUserSession(RefreshUserSessionCommand command) {
|
|
||||||
return connection.Send(new RefreshUserSessionMessage(command.UserGuid));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Data.Java;
|
using Phantom.Common.Data.Java;
|
||||||
using Phantom.Common.Data.Minecraft;
|
using Phantom.Common.Data.Minecraft;
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
@@ -15,7 +14,6 @@ using Phantom.Controller.Services.Agents;
|
|||||||
using Phantom.Controller.Services.Events;
|
using Phantom.Controller.Services.Events;
|
||||||
using Phantom.Controller.Services.Instances;
|
using Phantom.Controller.Services.Instances;
|
||||||
using Phantom.Controller.Services.Users;
|
using Phantom.Controller.Services.Users;
|
||||||
using Phantom.Controller.Services.Users.Sessions;
|
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
|
|
||||||
@@ -68,27 +66,25 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
|
|
||||||
var senderActorInit = new WebMessageDataUpdateSenderActor.Init(connection, controllerState, init.InstanceLogManager);
|
var senderActorInit = new WebMessageDataUpdateSenderActor.Init(connection, controllerState, init.InstanceLogManager);
|
||||||
Context.ActorOf(WebMessageDataUpdateSenderActor.Factory(senderActorInit), "DataUpdateSender");
|
Context.ActorOf(WebMessageDataUpdateSenderActor.Factory(senderActorInit), "DataUpdateSender");
|
||||||
|
|
||||||
ReceiveAsync<RegisterWebMessage>(HandleRegisterWeb);
|
ReceiveAsync<RegisterWebMessage>(HandleRegisterWeb);
|
||||||
Receive<UnregisterWebMessage>(HandleUnregisterWeb);
|
Receive<UnregisterWebMessage>(HandleUnregisterWeb);
|
||||||
ReceiveAndReplyLater<LogInMessage, Optional<LogInSuccess>>(HandleLogIn);
|
|
||||||
Receive<LogOutMessage>(HandleLogOut);
|
|
||||||
ReceiveAndReply<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(GetAuthenticatedUser);
|
|
||||||
ReceiveAndReplyLater<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(HandleCreateOrUpdateAdministratorUser);
|
ReceiveAndReplyLater<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(HandleCreateOrUpdateAdministratorUser);
|
||||||
ReceiveAndReplyLater<CreateUserMessage, Result<CreateUserResult, UserActionFailure>>(HandleCreateUser);
|
ReceiveAndReplyLater<CreateUserMessage, CreateUserResult>(HandleCreateUser);
|
||||||
ReceiveAndReplyLater<GetUsersMessage, ImmutableArray<UserInfo>>(HandleGetUsers);
|
ReceiveAndReplyLater<GetUsersMessage, ImmutableArray<UserInfo>>(HandleGetUsers);
|
||||||
ReceiveAndReplyLater<GetRolesMessage, ImmutableArray<RoleInfo>>(HandleGetRoles);
|
ReceiveAndReplyLater<GetRolesMessage, ImmutableArray<RoleInfo>>(HandleGetRoles);
|
||||||
ReceiveAndReplyLater<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(HandleGetUserRoles);
|
ReceiveAndReplyLater<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(HandleGetUserRoles);
|
||||||
ReceiveAndReplyLater<ChangeUserRolesMessage, Result<ChangeUserRolesResult, UserActionFailure>>(HandleChangeUserRoles);
|
ReceiveAndReplyLater<ChangeUserRolesMessage, ChangeUserRolesResult>(HandleChangeUserRoles);
|
||||||
ReceiveAndReplyLater<DeleteUserMessage, Result<DeleteUserResult, UserActionFailure>>(HandleDeleteUser);
|
ReceiveAndReplyLater<DeleteUserMessage, DeleteUserResult>(HandleDeleteUser);
|
||||||
ReceiveAndReplyLater<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(HandleCreateOrUpdateInstance);
|
ReceiveAndReplyLater<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(HandleCreateOrUpdateInstance);
|
||||||
ReceiveAndReplyLater<LaunchInstanceMessage, Result<LaunchInstanceResult, UserInstanceActionFailure>>(HandleLaunchInstance);
|
ReceiveAndReplyLater<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(HandleLaunchInstance);
|
||||||
ReceiveAndReplyLater<StopInstanceMessage, Result<StopInstanceResult, UserInstanceActionFailure>>(HandleStopInstance);
|
ReceiveAndReplyLater<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(HandleStopInstance);
|
||||||
ReceiveAndReplyLater<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>(HandleSendCommandToInstance);
|
ReceiveAndReplyLater<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(HandleSendCommandToInstance);
|
||||||
ReceiveAndReplyLater<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(HandleGetMinecraftVersions);
|
ReceiveAndReplyLater<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(HandleGetMinecraftVersions);
|
||||||
ReceiveAndReply<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(HandleGetAgentJavaRuntimes);
|
ReceiveAndReply<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(HandleGetAgentJavaRuntimes);
|
||||||
ReceiveAndReplyLater<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(HandleGetAuditLog);
|
ReceiveAndReplyLater<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(HandleGetAuditLog);
|
||||||
ReceiveAndReplyLater<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(HandleGetEventLog);
|
ReceiveAndReplyLater<GetEventLogMessage, ImmutableArray<EventLogItem>>(HandleGetEventLog);
|
||||||
|
ReceiveAndReplyLater<LogInMessage, LogInSuccess?>(HandleLogIn);
|
||||||
Receive<ReplyMessage>(HandleReply);
|
Receive<ReplyMessage>(HandleReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,24 +96,12 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
connection.Close();
|
connection.Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Optional<LogInSuccess>> HandleLogIn(LogInMessage message) {
|
|
||||||
return userLoginManager.LogIn(message.Username, message.Password);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleLogOut(LogOutMessage message) {
|
|
||||||
_ = userLoginManager.LogOut(message.UserGuid, message.SessionToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<AuthenticatedUserInfo> GetAuthenticatedUser(GetAuthenticatedUser message) {
|
|
||||||
return userLoginManager.GetAuthenticatedUser(message.UserGuid, message.AuthToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task<CreateOrUpdateAdministratorUserResult> HandleCreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message) {
|
private Task<CreateOrUpdateAdministratorUserResult> HandleCreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message) {
|
||||||
return userManager.CreateOrUpdateAdministrator(message.Username, message.Password);
|
return userManager.CreateOrUpdateAdministrator(message.Username, message.Password);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Result<CreateUserResult, UserActionFailure>> HandleCreateUser(CreateUserMessage message) {
|
private Task<CreateUserResult> HandleCreateUser(CreateUserMessage message) {
|
||||||
return userManager.Create(userLoginManager.GetLoggedInUser(message.AuthToken), message.Username, message.Password);
|
return userManager.Create(message.LoggedInUserGuid, message.Username, message.Password);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<ImmutableArray<UserInfo>> HandleGetUsers(GetUsersMessage message) {
|
private Task<ImmutableArray<UserInfo>> HandleGetUsers(GetUsersMessage message) {
|
||||||
@@ -132,48 +116,28 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
return userRoleManager.GetUserRoles(message.UserGuids);
|
return userRoleManager.GetUserRoles(message.UserGuids);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Result<ChangeUserRolesResult, UserActionFailure>> HandleChangeUserRoles(ChangeUserRolesMessage message) {
|
private Task<ChangeUserRolesResult> HandleChangeUserRoles(ChangeUserRolesMessage message) {
|
||||||
return userRoleManager.ChangeUserRoles(userLoginManager.GetLoggedInUser(message.AuthToken), message.SubjectUserGuid, message.AddToRoleGuids, message.RemoveFromRoleGuids);
|
return userRoleManager.ChangeUserRoles(message.LoggedInUserGuid, message.SubjectUserGuid, message.AddToRoleGuids, message.RemoveFromRoleGuids);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Result<DeleteUserResult, UserActionFailure>> HandleDeleteUser(DeleteUserMessage message) {
|
private Task<DeleteUserResult> HandleDeleteUser(DeleteUserMessage message) {
|
||||||
return userManager.DeleteByGuid(userLoginManager.GetLoggedInUser(message.AuthToken), message.SubjectUserGuid);
|
return userManager.DeleteByGuid(message.LoggedInUserGuid, message.SubjectUserGuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message) {
|
private Task<InstanceActionResult<CreateOrUpdateInstanceResult>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message) {
|
||||||
return agentManager.DoInstanceAction<AgentActor.CreateOrUpdateInstanceCommand, CreateOrUpdateInstanceResult>(
|
return agentManager.DoInstanceAction<AgentActor.CreateOrUpdateInstanceCommand, CreateOrUpdateInstanceResult>(message.Configuration.AgentGuid, new AgentActor.CreateOrUpdateInstanceCommand(message.LoggedInUserGuid, message.InstanceGuid, message.Configuration));
|
||||||
Permission.CreateInstances,
|
|
||||||
message.AuthToken,
|
|
||||||
message.Configuration.AgentGuid,
|
|
||||||
loggedInUserGuid => new AgentActor.CreateOrUpdateInstanceCommand(loggedInUserGuid, message.InstanceGuid, message.Configuration)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Result<LaunchInstanceResult, UserInstanceActionFailure>> HandleLaunchInstance(LaunchInstanceMessage message) {
|
private Task<InstanceActionResult<LaunchInstanceResult>> HandleLaunchInstance(LaunchInstanceMessage message) {
|
||||||
return agentManager.DoInstanceAction<AgentActor.LaunchInstanceCommand, LaunchInstanceResult>(
|
return agentManager.DoInstanceAction<AgentActor.LaunchInstanceCommand, LaunchInstanceResult>(message.AgentGuid, new AgentActor.LaunchInstanceCommand(message.InstanceGuid, message.LoggedInUserGuid));
|
||||||
Permission.ControlInstances,
|
|
||||||
message.AuthToken,
|
|
||||||
message.AgentGuid,
|
|
||||||
loggedInUserGuid => new AgentActor.LaunchInstanceCommand(loggedInUserGuid, message.InstanceGuid)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Result<StopInstanceResult, UserInstanceActionFailure>> HandleStopInstance(StopInstanceMessage message) {
|
private Task<InstanceActionResult<StopInstanceResult>> HandleStopInstance(StopInstanceMessage message) {
|
||||||
return agentManager.DoInstanceAction<AgentActor.StopInstanceCommand, StopInstanceResult>(
|
return agentManager.DoInstanceAction<AgentActor.StopInstanceCommand, StopInstanceResult>(message.AgentGuid, new AgentActor.StopInstanceCommand(message.InstanceGuid, message.LoggedInUserGuid, message.StopStrategy));
|
||||||
Permission.ControlInstances,
|
|
||||||
message.AuthToken,
|
|
||||||
message.AgentGuid,
|
|
||||||
loggedInUserGuid => new AgentActor.StopInstanceCommand(loggedInUserGuid, message.InstanceGuid, message.StopStrategy)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Result<SendCommandToInstanceResult, UserInstanceActionFailure>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) {
|
private Task<InstanceActionResult<SendCommandToInstanceResult>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) {
|
||||||
return agentManager.DoInstanceAction<AgentActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(
|
return agentManager.DoInstanceAction<AgentActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(message.AgentGuid, new AgentActor.SendCommandToInstanceCommand(message.InstanceGuid, message.LoggedInUserGuid, message.Command));
|
||||||
Permission.ControlInstances,
|
|
||||||
message.AuthToken,
|
|
||||||
message.AgentGuid,
|
|
||||||
loggedInUserGuid => new AgentActor.SendCommandToInstanceCommand(loggedInUserGuid, message.InstanceGuid, message.Command)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<ImmutableArray<MinecraftVersion>> HandleGetMinecraftVersions(GetMinecraftVersionsMessage message) {
|
private Task<ImmutableArray<MinecraftVersion>> HandleGetMinecraftVersions(GetMinecraftVersionsMessage message) {
|
||||||
@@ -184,12 +148,16 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
return controllerState.AgentJavaRuntimesByGuid;
|
return controllerState.AgentJavaRuntimesByGuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Result<ImmutableArray<AuditLogItem>, UserActionFailure>> HandleGetAuditLog(GetAuditLogMessage message) {
|
private Task<ImmutableArray<AuditLogItem>> HandleGetAuditLog(GetAuditLogMessage message) {
|
||||||
return auditLogManager.GetMostRecentItems(userLoginManager.GetLoggedInUser(message.AuthToken), message.Count);
|
return auditLogManager.GetMostRecentItems(message.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Result<ImmutableArray<EventLogItem>, UserActionFailure>> HandleGetEventLog(GetEventLogMessage message) {
|
private Task<ImmutableArray<EventLogItem>> HandleGetEventLog(GetEventLogMessage message) {
|
||||||
return eventLogManager.GetMostRecentItems(userLoginManager.GetLoggedInUser(message.AuthToken), message.Count);
|
return eventLogManager.GetMostRecentItems(message.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<LogInSuccess?> HandleLogIn(LogInMessage message) {
|
||||||
|
return userLoginManager.LogIn(message.Username, message.Password);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleReply(ReplyMessage message) {
|
private void HandleReply(ReplyMessage message) {
|
||||||
|
@@ -7,7 +7,6 @@ using Phantom.Controller.Services.Agents;
|
|||||||
using Phantom.Controller.Services.Events;
|
using Phantom.Controller.Services.Events;
|
||||||
using Phantom.Controller.Services.Instances;
|
using Phantom.Controller.Services.Instances;
|
||||||
using Phantom.Controller.Services.Users;
|
using Phantom.Controller.Services.Users;
|
||||||
using Phantom.Controller.Services.Users.Sessions;
|
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
|
@@ -1,10 +1,7 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Data.Web.AuditLog;
|
using Phantom.Common.Data.Web.AuditLog;
|
||||||
using Phantom.Common.Data.Web.Users;
|
|
||||||
using Phantom.Controller.Database;
|
using Phantom.Controller.Database;
|
||||||
using Phantom.Controller.Database.Repositories;
|
using Phantom.Controller.Database.Repositories;
|
||||||
using Phantom.Controller.Services.Users.Sessions;
|
|
||||||
|
|
||||||
namespace Phantom.Controller.Services.Users;
|
namespace Phantom.Controller.Services.Users;
|
||||||
|
|
||||||
@@ -15,11 +12,7 @@ sealed class AuditLogManager {
|
|||||||
this.dbProvider = dbProvider;
|
this.dbProvider = dbProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<ImmutableArray<AuditLogItem>, UserActionFailure>> GetMostRecentItems(LoggedInUser loggedInUser, int count) {
|
public async Task<ImmutableArray<AuditLogItem>> GetMostRecentItems(int count) {
|
||||||
if (!loggedInUser.CheckPermission(Permission.ViewAudit)) {
|
|
||||||
return UserActionFailure.NotAuthorized;
|
|
||||||
}
|
|
||||||
|
|
||||||
await using var db = dbProvider.Lazy();
|
await using var db = dbProvider.Lazy();
|
||||||
return await new AuditLogRepository(db).GetMostRecentItems(count, CancellationToken.None);
|
return await new AuditLogRepository(db).GetMostRecentItems(count, CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
@@ -36,6 +36,34 @@ sealed class PermissionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<PermissionSet> FetchPermissionsForAllUsers(Guid userId) {
|
||||||
|
await using var ctx = dbProvider.Eager();
|
||||||
|
|
||||||
|
var userPermissions = ctx.UserPermissions
|
||||||
|
.Where(up => up.UserGuid == userId)
|
||||||
|
.Select(static up => up.PermissionId);
|
||||||
|
|
||||||
|
var rolePermissions = ctx.UserRoles
|
||||||
|
.Where(ur => ur.UserGuid == userId)
|
||||||
|
.Join(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 async Task<PermissionSet> FetchPermissionsForUserId(Guid userId) {
|
||||||
|
await using var ctx = dbProvider.Eager();
|
||||||
|
|
||||||
|
var userPermissions = ctx.UserPermissions
|
||||||
|
.Where(up => up.UserGuid == userId)
|
||||||
|
.Select(static up => up.PermissionId);
|
||||||
|
|
||||||
|
var rolePermissions = ctx.UserRoles
|
||||||
|
.Where(ur => ur.UserGuid == userId)
|
||||||
|
.Join(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 static ImmutableArray<string> GetMissingPermissionsOrdered(IEnumerable<Permission> allPermissions, ImmutableHashSet<string> existingPermissionIds) {
|
public static ImmutableArray<string> GetMissingPermissionsOrdered(IEnumerable<Permission> allPermissions, ImmutableHashSet<string> existingPermissionIds) {
|
||||||
return allPermissions.Select(static permission => permission.Id).Except(existingPermissionIds).Order().ToImmutableArray();
|
return allPermissions.Select(static permission => permission.Id).Except(existingPermissionIds).Order().ToImmutableArray();
|
||||||
}
|
}
|
||||||
|
@@ -1,29 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using Phantom.Common.Data.Web.Users;
|
|
||||||
using Phantom.Controller.Database;
|
|
||||||
using Phantom.Controller.Database.Entities;
|
|
||||||
using Phantom.Controller.Database.Repositories;
|
|
||||||
|
|
||||||
namespace Phantom.Controller.Services.Users.Sessions;
|
|
||||||
|
|
||||||
sealed class AuthenticatedUserCache {
|
|
||||||
private readonly ConcurrentDictionary<Guid, AuthenticatedUserInfo> authenticatedUsersByGuid = new ();
|
|
||||||
|
|
||||||
public bool TryGet(Guid userGuid, out AuthenticatedUserInfo? userInfo) {
|
|
||||||
return authenticatedUsersByGuid.TryGetValue(userGuid, out userInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<AuthenticatedUserInfo?> Update(UserEntity user, ILazyDbContext db) {
|
|
||||||
var permissionRepository = new PermissionRepository(db);
|
|
||||||
var userPermissions = await permissionRepository.GetAllUserPermissions(user);
|
|
||||||
var userManagedAgentGuids = await permissionRepository.GetManagedAgentGuids(user);
|
|
||||||
|
|
||||||
var userGuid = user.UserGuid;
|
|
||||||
var userInfo = new AuthenticatedUserInfo(userGuid, user.Name, userPermissions, userManagedAgentGuids);
|
|
||||||
return authenticatedUsersByGuid[userGuid] = userInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Remove(Guid userGuid) {
|
|
||||||
authenticatedUsersByGuid.Remove(userGuid, out _);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,20 +0,0 @@
|
|||||||
using System.Collections.Immutable;
|
|
||||||
using Phantom.Common.Data.Web.Users;
|
|
||||||
|
|
||||||
namespace Phantom.Controller.Services.Users.Sessions;
|
|
||||||
|
|
||||||
readonly record struct LoggedInUser(AuthenticatedUserInfo? AuthenticatedUserInfo) {
|
|
||||||
public Guid? Guid => AuthenticatedUserInfo?.Guid;
|
|
||||||
|
|
||||||
public bool CheckPermission(Permission permission) {
|
|
||||||
return AuthenticatedUserInfo is {} info && info.CheckPermission(permission);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HasAccessToAgent(Guid agentGuid) {
|
|
||||||
return AuthenticatedUserInfo is {} info && info.HasAccessToAgent(agentGuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ImmutableHashSet<Guid> FilterAccessibleAgentGuids(ImmutableHashSet<Guid> agentGuids) {
|
|
||||||
return AuthenticatedUserInfo is {} info ? info.FilterAccessibleAgentGuids(agentGuids) : ImmutableHashSet<Guid>.Empty;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,140 +0,0 @@
|
|||||||
using System.Collections.Immutable;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using Phantom.Common.Data;
|
|
||||||
using Phantom.Common.Data.Web.Users;
|
|
||||||
using Phantom.Controller.Database;
|
|
||||||
using Phantom.Controller.Database.Repositories;
|
|
||||||
|
|
||||||
namespace Phantom.Controller.Services.Users.Sessions;
|
|
||||||
|
|
||||||
sealed class UserLoginManager {
|
|
||||||
private const int SessionIdBytes = 20;
|
|
||||||
|
|
||||||
private readonly AuthenticatedUserCache authenticatedUserCache;
|
|
||||||
private readonly UserManager userManager;
|
|
||||||
private readonly IDbContextProvider dbProvider;
|
|
||||||
|
|
||||||
private readonly UserSessionBucket[] sessionBuckets = new UserSessionBucket[256];
|
|
||||||
|
|
||||||
public UserLoginManager(AuthenticatedUserCache authenticatedUserCache, UserManager userManager, IDbContextProvider dbProvider) {
|
|
||||||
this.authenticatedUserCache = authenticatedUserCache;
|
|
||||||
this.userManager = userManager;
|
|
||||||
this.dbProvider = dbProvider;
|
|
||||||
|
|
||||||
for (int i = 0; i < sessionBuckets.GetLength(0); i++) {
|
|
||||||
sessionBuckets[i] = new UserSessionBucket();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private UserSessionBucket GetSessionBucket(ImmutableArray<byte> token) {
|
|
||||||
return sessionBuckets[token[0]];
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Optional<LogInSuccess>> LogIn(string username, string password) {
|
|
||||||
Guid userGuid;
|
|
||||||
AuthenticatedUserInfo? authenticatedUserInfo;
|
|
||||||
|
|
||||||
await using (var db = dbProvider.Lazy()) {
|
|
||||||
var userRepository = new UserRepository(db);
|
|
||||||
|
|
||||||
var user = await userRepository.GetByName(username);
|
|
||||||
if (user == null || !UserPasswords.Verify(password, user.PasswordHash)) {
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
authenticatedUserInfo = await authenticatedUserCache.Update(user, db);
|
|
||||||
if (authenticatedUserInfo == null) {
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
userGuid = user.UserGuid;
|
|
||||||
|
|
||||||
var auditLogWriter = new AuditLogRepository(db).Writer(userGuid);
|
|
||||||
auditLogWriter.UserLoggedIn(user);
|
|
||||||
|
|
||||||
await db.Ctx.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
var authToken = ImmutableArray.Create(RandomNumberGenerator.GetBytes(SessionIdBytes));
|
|
||||||
GetSessionBucket(authToken).Add(userGuid, authToken);
|
|
||||||
|
|
||||||
return new LogInSuccess(authenticatedUserInfo, authToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LogOut(Guid userGuid, ImmutableArray<byte> authToken) {
|
|
||||||
if (!GetSessionBucket(authToken).Remove(userGuid, authToken)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await using var db = dbProvider.Lazy();
|
|
||||||
|
|
||||||
var auditLogWriter = new AuditLogRepository(db).Writer(userGuid);
|
|
||||||
auditLogWriter.UserLoggedOut(userGuid);
|
|
||||||
|
|
||||||
await db.Ctx.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public LoggedInUser GetLoggedInUser(ImmutableArray<byte> authToken) {
|
|
||||||
var userGuid = GetSessionBucket(authToken).FindUserGuid(authToken);
|
|
||||||
return userGuid != null && authenticatedUserCache.TryGet(userGuid.Value, out var userInfo) ? new LoggedInUser(userInfo) : default;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AuthenticatedUserInfo? GetAuthenticatedUser(Guid userGuid, ImmutableArray<byte> authToken) {
|
|
||||||
return authenticatedUserCache.TryGet(userGuid, out var userInfo) && GetSessionBucket(authToken).Contains(userGuid, authToken) ? userInfo : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class UserSessionBucket {
|
|
||||||
private ImmutableList<UserSession> sessions = ImmutableList<UserSession>.Empty;
|
|
||||||
|
|
||||||
public void Add(Guid userGuid, ImmutableArray<byte> authToken) {
|
|
||||||
lock (this) {
|
|
||||||
var session = new UserSession(userGuid, authToken);
|
|
||||||
if (!sessions.Contains(session)) {
|
|
||||||
sessions = sessions.Add(session);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Contains(Guid userGuid, ImmutableArray<byte> authToken) {
|
|
||||||
lock (this) {
|
|
||||||
return sessions.Contains(new UserSession(userGuid, authToken));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Guid? FindUserGuid(ImmutableArray<byte> authToken) {
|
|
||||||
lock (this) {
|
|
||||||
return sessions.Find(session => session.AuthTokenEquals(authToken))?.UserGuid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Remove(Guid userGuid, ImmutableArray<byte> authToken) {
|
|
||||||
lock (this) {
|
|
||||||
int index = sessions.IndexOf(new UserSession(userGuid, authToken));
|
|
||||||
if (index == -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
sessions = sessions.RemoveAt(index);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed record UserSession(Guid UserGuid, ImmutableArray<byte> AuthToken) {
|
|
||||||
public bool AuthTokenEquals(ImmutableArray<byte> other) {
|
|
||||||
return CryptographicOperations.FixedTimeEquals(AuthToken.AsSpan(), other.AsSpan());
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(UserSession? other) {
|
|
||||||
if (ReferenceEquals(null, other)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return UserGuid.Equals(other.UserGuid) && AuthTokenEquals(other.AuthToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode() {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -0,0 +1,34 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using Phantom.Common.Data.Web.Users;
|
||||||
|
|
||||||
|
namespace Phantom.Controller.Services.Users;
|
||||||
|
|
||||||
|
sealed class UserLoginManager {
|
||||||
|
private const int SessionIdBytes = 20;
|
||||||
|
private readonly ConcurrentDictionary<string, List<ImmutableArray<byte>>> sessionTokensByUsername = new ();
|
||||||
|
|
||||||
|
private readonly UserManager userManager;
|
||||||
|
private readonly PermissionManager permissionManager;
|
||||||
|
|
||||||
|
public UserLoginManager(UserManager userManager, PermissionManager permissionManager) {
|
||||||
|
this.userManager = userManager;
|
||||||
|
this.permissionManager = permissionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LogInSuccess?> LogIn(string username, string password) {
|
||||||
|
var user = await userManager.GetAuthenticated(username, password);
|
||||||
|
if (user == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var token = ImmutableArray.Create(RandomNumberGenerator.GetBytes(SessionIdBytes));
|
||||||
|
var sessionTokens = sessionTokensByUsername.GetOrAdd(username, static _ => new List<ImmutableArray<byte>>());
|
||||||
|
lock (sessionTokens) {
|
||||||
|
sessionTokens.Add(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LogInSuccess(user.UserGuid, await permissionManager.FetchPermissionsForUserId(user.UserGuid), token);
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user