1
0
mirror of https://github.com/chylex/Minecraft-Phantom-Panel.git synced 2025-09-16 18:24:48 +02:00

4 Commits

Author SHA1 Message Date
820852d096 WIP 2024-03-28 15:32:54 +01:00
94c0af9f64 WIP 2024-03-28 14:13:09 +01:00
374bcd21f4 WIP 2024-03-25 18:51:27 +01:00
68459461c4 WIP 2024-03-25 06:12:45 +01:00
314 changed files with 2997 additions and 4270 deletions

View File

@@ -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"
] ]

View File

@@ -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() {

View File

@@ -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");
} }

View File

@@ -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);
} }
} }

View File

@@ -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) {
newListenerCount = ++listenerCount;
DownloadProgress += listener.DownloadProgressEventHandler; DownloadProgress += listener.DownloadProgressEventHandler;
listenerCancellationRegistrations.Add(listener.CancellationToken.Register(Unregister, listener)); 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;
lock (this) {
MinecraftServerExecutableDownloadListener listener = (MinecraftServerExecutableDownloadListener) listenerObject!; MinecraftServerExecutableDownloadListener listener = (MinecraftServerExecutableDownloadListener) listenerObject!;
DownloadProgress -= listener.DownloadProgressEventHandler; DownloadProgress -= listener.DownloadProgressEventHandler;
newListenerCount = --listenerCount; if (--listeners <= 0) {
if (newListenerCount <= 0) { Logger.Debug("Unregistered last download listener, cancelling download.");
cancellationTokenSource.Cancel(); cancellationTokenSource.Cancel();
} }
}
if (newListenerCount <= 0) {
Logger.Debug("Unregistered last download listener, cancelling download.");
}
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.");
lock (this) {
Completed?.Invoke(this, EventArgs.Empty); Completed?.Invoke(this, EventArgs.Empty);
Completed = null; Completed = null;
DownloadProgress = 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();
} }
} }

View File

@@ -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) {}
}
} }

View File

@@ -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();

View File

@@ -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.",

View File

@@ -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,16 +16,20 @@ 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();
} }
@@ -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 {
while (!CancellationToken.IsCancellationRequested) {
serverOutputWhileWaitingForOnlinePlayers.Reset();
var onlinePlayerCount = await serverStatusProtocol.GetOnlinePlayerCount(serverPort, CancellationToken);
if (onlinePlayerCount == null) {
Logger.Warning("Could not detect whether any players are online, starting a new backup.");
break;
} }
try { if (onlinePlayerCount > 0) {
await task;
Logger.Information("Players are online, starting a new backup."); Logger.Information("Players are online, starting a new backup.");
} catch (OperationCanceledException) { break;
throw; }
} catch (Exception) {
Logger.Warning("Could not detect whether any players are online, starting a new backup."); 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.");
} }
} }

View File

@@ -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,27 +7,9 @@ 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;
private readonly CancellationToken cancellationToken; private readonly CancellationToken cancellationToken;
@@ -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();
} }

View File

@@ -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 ILogger logger;
private readonly InstanceContext context; private readonly InstanceContext context;
private readonly CancellationTokenSource actorCancellationTokenSource = new ();
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);
} }
} }

View File

@@ -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));
} }

View File

@@ -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);

View File

@@ -1,208 +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),
_ => 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);
}
}

View File

@@ -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);

View 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.");
}
}

View File

@@ -4,18 +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 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 ();
@@ -24,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;
@@ -38,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;
} }
@@ -54,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 ticket; return new Ticket(instanceGuid, memoryAllocation, serverPort, rconPort);
}
}
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;
} }
@@ -85,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);
} }

View File

@@ -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...");
var newState = await TryLaunchInstance(context, configuration, launcher, ticket, reportStatus, cancellationToken);
if (newState is not InstanceRunningState) {
context.Services.InstanceTicketManager.Release(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; Result<InstanceProcess, InstanceLaunchFailReason> result;
if (ticketManager.IsValid(ticket)) {
try { try {
result = await LaunchInstance(context, launcher, reportStatus, cancellationToken); result = await LaunchInstanceImpl(context, launcher, reportStatus, cancellationToken);
} catch (OperationCanceledException) { } catch (OperationCanceledException) {
reportStatus(InstanceStatus.NotRunning); reportStatus(InstanceStatus.NotRunning);
return null; return new InstanceNotRunningState();
} catch (Exception e) { } catch (Exception e) {
context.Logger.Error(e, "Caught exception while launching instance."); context.Logger.Error(e, "Caught exception while launching instance.");
result = InstanceLaunchFailReason.UnknownError; result = InstanceLaunchFailReason.UnknownError;
} }
}
else {
context.Logger.Error("Attempted to launch instance with an invalid ticket!");
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;

View File

@@ -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();

View File

@@ -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();
}
}

View File

@@ -0,0 +1,5 @@
namespace Phantom.Agent.Services.Instances.States;
interface IInstanceState {
void Initialize();
}

View File

@@ -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);
}
}

View File

@@ -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;
} }

View File

@@ -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) {

View File

@@ -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());

View File

@@ -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() {

View File

@@ -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);
} }
} }

View File

@@ -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);
}
}

View File

@@ -5,6 +5,7 @@ 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
); );

View File

@@ -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);

View File

@@ -1,5 +0,0 @@
namespace Phantom.Common.Data.Web.Users;
public enum UserActionFailure {
NotAuthorized
}

View File

@@ -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;

View File

@@ -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.");

View File

@@ -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 {

View File

@@ -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;
} }
} }

View File

@@ -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
);

View File

@@ -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);
}
}

View File

@@ -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."
};
}
}

View File

@@ -0,0 +1,9 @@
namespace Phantom.Common.Data.Replies;
public enum InstanceActionGeneralResult : byte {
None,
AgentDoesNotExist,
AgentShuttingDown,
AgentIsNotResponding,
InstanceDoesNotExist
}

View 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);
}
}

View File

@@ -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;
}

View File

@@ -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);
} }

View File

@@ -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>>;

View File

@@ -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>>;

View File

@@ -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>>;

View File

@@ -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>>;

View File

@@ -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;

View File

@@ -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>;

View File

@@ -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>>;

View File

@@ -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>;

View File

@@ -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>;

View File

@@ -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>>;

View File

@@ -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>>;

View File

@@ -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>>;

View File

@@ -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>>;

View File

@@ -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?>;

View File

@@ -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;

View File

@@ -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>>;

View File

@@ -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>>;

View File

@@ -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;

View File

@@ -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);
} }

View File

@@ -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
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -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)

View File

@@ -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; init; } = null!; public DbSet<AgentEntity> Agents { get; set; } = null!;
public DbSet<InstanceEntity> Instances { get; init; } = null!; public DbSet<InstanceEntity> Instances { get; set; } = null!;
public DbSet<AuditLogEntity> AuditLog { get; init; } = null!; public DbSet<AuditLogEntity> AuditLog { get; set; } = null!;
public DbSet<EventLogEntity> EventLog { get; init; } = null!; public DbSet<EventLogEntity> EventLog { get; set; } = 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) {

View File

@@ -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; }

View File

@@ -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() {

View File

@@ -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() {

View File

@@ -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; }

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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()))

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 InstanceGuid, Guid AuditLogUserGuid) : ICommand, ICanReply<InstanceActionResult<LaunchInstanceResult>>;
public sealed record LaunchInstanceCommand(Guid LoggedInUserGuid, Guid InstanceGuid) : ICommand, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>; public sealed record StopInstanceCommand(Guid InstanceGuid, Guid AuditLogUserGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<InstanceActionResult<StopInstanceResult>>;
public sealed record StopInstanceCommand(Guid LoggedInUserGuid, Guid InstanceGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>; public sealed record SendCommandToInstanceCommand(Guid InstanceGuid, Guid AuditLogUserGuid, string Command) : ICommand, ICanReply<InstanceActionResult<SendCommandToInstanceResult>>;
public sealed record SendCommandToInstanceCommand(Guid LoggedInUserGuid, Guid InstanceGuid, string Command) : ICommand, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>;
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) {

View File

@@ -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 {
return InstanceActionResult.General<TReply>(InstanceActionGeneralResult.AgentDoesNotExist);
} }
if (!agentsByGuid.TryGetValue(agentGuid, out var agent)) {
return (UserInstanceActionFailure) InstanceActionFailure.AgentDoesNotExist;
}
var command = commandFactoryFromLoggedInUserGuid(loggedInUser.Guid!.Value);
var result = await agent.Request(command, cancellationToken);
return result.MapError(static error => (UserInstanceActionFailure) error);
} }
} }

View File

@@ -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);

View File

@@ -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);
}
} }

View File

@@ -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);
} }
} }

View File

@@ -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<InstanceActionResult<ConfigureInstanceResult>>;
public sealed record ConfigureInstanceCommand(Guid AuditLogUserGuid, Guid InstanceGuid, InstanceConfiguration Configuration, InstanceLaunchProperties LaunchProperties, bool IsCreatingInstance) : ICommand, ICanReply<Result<ConfigureInstanceResult, InstanceActionFailure>>; public sealed record LaunchInstanceCommand(Guid AuditLogUserGuid) : ICommand, ICanReply<InstanceActionResult<LaunchInstanceResult>>;
public sealed record LaunchInstanceCommand(Guid AuditLogUserGuid) : ICommand, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>; public sealed record StopInstanceCommand(Guid AuditLogUserGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<InstanceActionResult<StopInstanceResult>>;
public sealed record StopInstanceCommand(Guid AuditLogUserGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>; public sealed record SendCommandToInstanceCommand(Guid AuditLogUserGuid, string Command) : ICommand, ICanReply<InstanceActionResult<SendCommandToInstanceResult>>;
public sealed record SendCommandToInstanceCommand(Guid AuditLogUserGuid, string Command) : ICommand, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>;
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(); NotifyInstanceUpdated();
} }
private void SetPlayerCounts(SetPlayerCountsCommand command) { private async Task<InstanceActionResult<ConfigureInstanceResult>> ConfigureInstance(ConfigureInstanceCommand command) {
playerCounts = command.PlayerCounts;
NotifyInstanceUpdated();
}
private async Task<Result<ConfigureInstanceResult, InstanceActionFailure>> 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);

View File

@@ -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));
} }

View File

@@ -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));
} }
@@ -67,8 +58,6 @@ sealed class WebMessageDataUpdateSenderActor : ReceiveActor<WebMessageDataUpdate
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));
}
} }

View File

@@ -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;
@@ -71,24 +69,22 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
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) {

View File

@@ -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;

View File

@@ -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);
} }

View File

@@ -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();
} }

View File

@@ -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 _);
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}

View File

@@ -1,11 +1,8 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using Phantom.Common.Data;
using Phantom.Common.Data.Web.Users; using Phantom.Common.Data.Web.Users;
using Phantom.Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults;
using Phantom.Controller.Database; using Phantom.Controller.Database;
using Phantom.Controller.Database.Entities; using Phantom.Controller.Database.Entities;
using Phantom.Controller.Database.Repositories; using Phantom.Controller.Database.Repositories;
using Phantom.Controller.Services.Users.Sessions;
using Phantom.Utils.Logging; using Phantom.Utils.Logging;
using Serilog; using Serilog;
@@ -14,13 +11,9 @@ namespace Phantom.Controller.Services.Users;
sealed class UserManager { sealed class UserManager {
private static readonly ILogger Logger = PhantomLogger.Create<UserManager>(); private static readonly ILogger Logger = PhantomLogger.Create<UserManager>();
private readonly AuthenticatedUserCache authenticatedUserCache;
private readonly ControllerState controllerState;
private readonly IDbContextProvider dbProvider; private readonly IDbContextProvider dbProvider;
public UserManager(AuthenticatedUserCache authenticatedUserCache, ControllerState controllerState, IDbContextProvider dbProvider) { public UserManager(IDbContextProvider dbProvider) {
this.authenticatedUserCache = authenticatedUserCache;
this.controllerState = controllerState;
this.dbProvider = dbProvider; this.dbProvider = dbProvider;
} }
@@ -57,12 +50,13 @@ sealed class UserManager {
wasCreated = true; wasCreated = true;
} }
else { else {
return new CreationFailed(result.Error); return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.CreationFailed(result.Error);
} }
} }
else { else {
if (userRepository.SetUserPassword(user, password).TryGetError(out var error)) { var result = userRepository.SetUserPassword(user, password);
return new UpdatingFailed(error); if (!result) {
return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.UpdatingFailed(result.Error);
} }
auditLogWriter.AdministratorUserModified(user); auditLogWriter.AdministratorUserModified(user);
@@ -71,7 +65,7 @@ sealed class UserManager {
var role = await new RoleRepository(db).GetByGuid(Role.Administrator.Guid); var role = await new RoleRepository(db).GetByGuid(Role.Administrator.Guid);
if (role == null) { if (role == null) {
return new AddingToRoleFailed(); return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.AddingToRoleFailed();
} }
await new UserRoleRepository(db).Add(user, role); await new UserRoleRepository(db).Add(user, role);
@@ -85,21 +79,17 @@ sealed class UserManager {
Logger.Information("Updated administrator user \"{Username}\" (GUID {Guid}).", username, user.UserGuid); Logger.Information("Updated administrator user \"{Username}\" (GUID {Guid}).", username, user.UserGuid);
} }
return new Success(user.ToUserInfo()); return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.Success(user.ToUserInfo());
} catch (Exception e) { } catch (Exception e) {
Logger.Error(e, "Could not create or update administrator user \"{Username}\".", username); Logger.Error(e, "Could not create or update administrator user \"{Username}\".", username);
return new UnknownError(); return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.UnknownError();
} }
} }
public async Task<Result<CreateUserResult, UserActionFailure>> Create(LoggedInUser loggedInUser, string username, string password) { public async Task<CreateUserResult> Create(Guid loggedInUserGuid, string username, string password) {
if (!loggedInUser.CheckPermission(Permission.EditUsers)) {
return UserActionFailure.NotAuthorized;
}
await using var db = dbProvider.Lazy(); await using var db = dbProvider.Lazy();
var userRepository = new UserRepository(db); var userRepository = new UserRepository(db);
var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUser.Guid); var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUserGuid);
try { try {
var result = await userRepository.CreateUser(username, password); var result = await userRepository.CreateUser(username, password);
@@ -120,11 +110,7 @@ sealed class UserManager {
} }
} }
public async Task<Result<DeleteUserResult, UserActionFailure>> DeleteByGuid(LoggedInUser loggedInUser, Guid userGuid) { public async Task<DeleteUserResult> DeleteByGuid(Guid loggedInUserGuid, Guid userGuid) {
if (!loggedInUser.CheckPermission(Permission.EditUsers)) {
return UserActionFailure.NotAuthorized;
}
await using var db = dbProvider.Lazy(); await using var db = dbProvider.Lazy();
var userRepository = new UserRepository(db); var userRepository = new UserRepository(db);
@@ -133,18 +119,12 @@ sealed class UserManager {
return DeleteUserResult.NotFound; return DeleteUserResult.NotFound;
} }
authenticatedUserCache.Remove(userGuid); var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUserGuid);
var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUser.Guid);
try { try {
userRepository.DeleteUser(user); userRepository.DeleteUser(user);
auditLogWriter.UserDeleted(user); auditLogWriter.UserDeleted(user);
await db.Ctx.SaveChangesAsync(); await db.Ctx.SaveChangesAsync();
// In case the user logged in during deletion.
authenticatedUserCache.Remove(userGuid);
controllerState.UpdateOrDeleteUser(userGuid);
Logger.Information("Deleted user \"{Username}\" (GUID {Guid}).", user.Name, user.UserGuid); Logger.Information("Deleted user \"{Username}\" (GUID {Guid}).", user.Name, user.UserGuid);
return DeleteUserResult.Deleted; return DeleteUserResult.Deleted;
} catch (Exception e) { } catch (Exception e) {

View File

@@ -1,9 +1,7 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using Phantom.Common.Data;
using Phantom.Common.Data.Web.Users; 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.Logging; using Phantom.Utils.Logging;
using Serilog; using Serilog;
@@ -12,13 +10,9 @@ namespace Phantom.Controller.Services.Users;
sealed class UserRoleManager { sealed class UserRoleManager {
private static readonly ILogger Logger = PhantomLogger.Create<UserRoleManager>(); private static readonly ILogger Logger = PhantomLogger.Create<UserRoleManager>();
private readonly AuthenticatedUserCache authenticatedUserCache;
private readonly ControllerState controllerState;
private readonly IDbContextProvider dbProvider; private readonly IDbContextProvider dbProvider;
public UserRoleManager(AuthenticatedUserCache authenticatedUserCache, ControllerState controllerState, IDbContextProvider dbProvider) { public UserRoleManager(IDbContextProvider dbProvider) {
this.authenticatedUserCache = authenticatedUserCache;
this.controllerState = controllerState;
this.dbProvider = dbProvider; this.dbProvider = dbProvider;
} }
@@ -27,11 +21,7 @@ sealed class UserRoleManager {
return await new UserRoleRepository(db).GetRoleGuidsByUserGuid(userGuids); return await new UserRoleRepository(db).GetRoleGuidsByUserGuid(userGuids);
} }
public async Task<Result<ChangeUserRolesResult, UserActionFailure>> ChangeUserRoles(LoggedInUser loggedInUser, Guid subjectUserGuid, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids) { public async Task<ChangeUserRolesResult> ChangeUserRoles(Guid loggedInUserGuid, Guid subjectUserGuid, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids) {
if (!loggedInUser.CheckPermission(Permission.EditUsers)) {
return UserActionFailure.NotAuthorized;
}
await using var db = dbProvider.Lazy(); await using var db = dbProvider.Lazy();
var userRepository = new UserRepository(db); var userRepository = new UserRepository(db);
@@ -42,7 +32,7 @@ sealed class UserRoleManager {
var roleRepository = new RoleRepository(db); var roleRepository = new RoleRepository(db);
var userRoleRepository = new UserRoleRepository(db); var userRoleRepository = new UserRoleRepository(db);
var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUser.Guid); var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUserGuid);
var rolesByGuid = await roleRepository.GetByGuids(addToRoleGuids.Union(removeFromRoleGuids)); var rolesByGuid = await roleRepository.GetByGuids(addToRoleGuids.Union(removeFromRoleGuids));
@@ -72,9 +62,6 @@ sealed class UserRoleManager {
auditLogWriter.UserRolesChanged(user, addedToRoleNames, removedFromRoleNames); auditLogWriter.UserRolesChanged(user, addedToRoleNames, removedFromRoleNames);
await db.Ctx.SaveChangesAsync(); await db.Ctx.SaveChangesAsync();
await authenticatedUserCache.Update(user, db);
controllerState.UpdateOrDeleteUser(user.UserGuid);
Logger.Information("Changed roles for user \"{Username}\" (GUID {Guid}).", user.Name, user.UserGuid); Logger.Information("Changed roles for user \"{Username}\" (GUID {Guid}).", user.Name, user.UserGuid);
return new ChangeUserRolesResult(addedToRoleGuids.ToImmutable(), removedFromRoleGuids.ToImmutable()); return new ChangeUserRolesResult(addedToRoleGuids.ToImmutable(), removedFromRoleGuids.ToImmutable());
} catch (Exception e) { } catch (Exception e) {

View File

@@ -10,12 +10,11 @@ using Phantom.Utils.Logging;
using Phantom.Utils.Rpc; using Phantom.Utils.Rpc;
using Phantom.Utils.Rpc.Runtime; using Phantom.Utils.Rpc.Runtime;
using Phantom.Utils.Runtime; using Phantom.Utils.Runtime;
using Phantom.Utils.Tasks;
var shutdownCancellationTokenSource = new CancellationTokenSource(); var shutdownCancellationTokenSource = new CancellationTokenSource();
var shutdownCancellationToken = shutdownCancellationTokenSource.Token; var shutdownCancellationToken = shutdownCancellationTokenSource.Token;
ProgramCulture.UseInvariantCulture();
PosixSignals.RegisterCancellation(shutdownCancellationTokenSource, static () => { PosixSignals.RegisterCancellation(shutdownCancellationTokenSource, static () => {
PhantomLogger.Root.InformationHeading("Stopping Phantom Panel controller..."); PhantomLogger.Root.InformationHeading("Stopping Phantom Panel controller...");
}); });
@@ -63,12 +62,14 @@ try {
return new RpcConfiguration(serviceName, host, port, connectionKey.Certificate); return new RpcConfiguration(serviceName, host, port, connectionKey.Certificate);
} }
var rpcTaskManager = new TaskManager(PhantomLogger.Create<TaskManager>("Rpc"));
try { try {
await Task.WhenAll( await Task.WhenAll(
RpcServerRuntime.Launch(ConfigureRpc("Agent", agentRpcServerHost, agentRpcServerPort, agentKeyData), AgentMessageRegistries.Definitions, controllerServices.AgentRegistrationHandler, controllerServices.ActorSystem, shutdownCancellationToken), RpcServerRuntime.Launch(ConfigureRpc("Agent", agentRpcServerHost, agentRpcServerPort, agentKeyData), AgentMessageRegistries.Definitions, controllerServices.AgentRegistrationHandler, controllerServices.ActorSystem, shutdownCancellationToken),
RpcServerRuntime.Launch(ConfigureRpc("Web", webRpcServerHost, webRpcServerPort, webKeyData), WebMessageRegistries.Definitions, controllerServices.WebRegistrationHandler, controllerServices.ActorSystem, shutdownCancellationToken) RpcServerRuntime.Launch(ConfigureRpc("Web", webRpcServerHost, webRpcServerPort, webKeyData), WebMessageRegistries.Definitions, controllerServices.WebRegistrationHandler, controllerServices.ActorSystem, shutdownCancellationToken)
); );
} finally { } finally {
await rpcTaskManager.Stop();
NetMQConfig.Cleanup(); NetMQConfig.Cleanup();
} }

View File

@@ -1,8 +1,8 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<LangVersion>13</LangVersion> <LangVersion>11</LangVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>

Some files were not shown because too many files have changed in this diff Show More