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

2 Commits

158 changed files with 1237 additions and 2529 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,5 +1,4 @@
using System.Collections.ObjectModel; using System.Text;
using System.Text;
using Phantom.Agent.Minecraft.Instance; using Phantom.Agent.Minecraft.Instance;
using Phantom.Agent.Minecraft.Java; using Phantom.Agent.Minecraft.Java;
using Phantom.Agent.Minecraft.Server; using Phantom.Agent.Minecraft.Server;
@@ -12,7 +11,6 @@ public abstract class BaseLauncher : IServerLauncher {
private readonly InstanceProperties instanceProperties; private readonly InstanceProperties instanceProperties;
protected string MinecraftVersion => instanceProperties.ServerVersion; protected string MinecraftVersion => instanceProperties.ServerVersion;
protected string InstanceFolder => instanceProperties.InstanceFolder;
private protected BaseLauncher(InstanceProperties instanceProperties) { private protected BaseLauncher(InstanceProperties instanceProperties) {
this.instanceProperties = instanceProperties; this.instanceProperties = instanceProperties;
@@ -53,14 +51,16 @@ public abstract class BaseLauncher : IServerLauncher {
var processConfigurator = new ProcessConfigurator { var processConfigurator = new ProcessConfigurator {
FileName = javaRuntimeExecutable.ExecutablePath, FileName = javaRuntimeExecutable.ExecutablePath,
WorkingDirectory = InstanceFolder, WorkingDirectory = instanceProperties.InstanceFolder,
RedirectInput = true, RedirectInput = true,
UseShellExecute = false UseShellExecute = false
}; };
var processArguments = processConfigurator.ArgumentList; var processArguments = processConfigurator.ArgumentList;
PrepareJvmArguments(serverJar).Build(processArguments); PrepareJvmArguments(serverJar).Build(processArguments);
PrepareJavaProcessArguments(processArguments, serverJar.FilePath); processArguments.Add("-jar");
processArguments.Add(serverJar.FilePath);
processArguments.Add("nogui");
var process = processConfigurator.CreateProcess(); var process = processConfigurator.CreateProcess();
var instanceProcess = new InstanceProcess(instanceProperties, process); var instanceProcess = new InstanceProcess(instanceProperties, process);
@@ -99,12 +99,6 @@ public abstract class BaseLauncher : IServerLauncher {
private protected virtual void CustomizeJvmArguments(JvmArgumentBuilder arguments) {} private protected virtual void CustomizeJvmArguments(JvmArgumentBuilder arguments) {}
protected virtual void PrepareJavaProcessArguments(Collection<string> processArguments, string serverJarFilePath) {
processArguments.Add("-jar");
processArguments.Add(serverJarFilePath);
processArguments.Add("nogui");
}
private protected virtual Task<ServerJarInfo> PrepareServerJar(ILogger logger, string serverJarPath, CancellationToken cancellationToken) { private protected virtual Task<ServerJarInfo> PrepareServerJar(ILogger logger, string serverJarPath, CancellationToken cancellationToken) {
return Task.FromResult(new ServerJarInfo(serverJarPath)); return Task.FromResult(new ServerJarInfo(serverJarPath));
} }

View File

@@ -1,29 +0,0 @@
using System.Collections.ObjectModel;
using Phantom.Agent.Minecraft.Instance;
using Phantom.Agent.Minecraft.Java;
using Serilog;
namespace Phantom.Agent.Minecraft.Launcher.Types;
public sealed class ForgeLauncher : BaseLauncher {
public ForgeLauncher(InstanceProperties instanceProperties) : base(instanceProperties) {}
private protected override void CustomizeJvmArguments(JvmArgumentBuilder arguments) {
arguments.AddProperty("terminal.ansi", "true"); // TODO
}
protected override void PrepareJavaProcessArguments(Collection<string> processArguments, string serverJarFilePath) {
if (OperatingSystem.IsWindows()) {
processArguments.Add("@libraries/net/minecraftforge/forge/1.20.1-47.2.0/win_args.txt");
}
else {
processArguments.Add("@libraries/net/minecraftforge/forge/1.20.1-47.2.0/unix_args.txt");
}
processArguments.Add("nogui");
}
private protected override Task<ServerJarInfo> PrepareServerJar(ILogger logger, string serverJarPath, CancellationToken cancellationToken) {
return Task.FromResult(new ServerJarInfo(Path.Combine(InstanceFolder, "run.sh")));
}
}

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) { DownloadProgress += listener.DownloadProgressEventHandler;
newListenerCount = ++listenerCount; listener.CancellationToken.Register(Unregister, listener);
DownloadProgress += listener.DownloadProgressEventHandler;
listenerCancellationRegistrations.Add(listener.CancellationToken.Register(Unregister, listener));
}
Logger.Debug("Registered download listener, current listener count: {Listeners}", newListenerCount);
} }
private void Unregister(object? listenerObject) { private void Unregister(object? listenerObject) {
int newListenerCount; MinecraftServerExecutableDownloadListener listener = (MinecraftServerExecutableDownloadListener) listenerObject!;
DownloadProgress -= listener.DownloadProgressEventHandler;
lock (this) { if (--listeners <= 0) {
MinecraftServerExecutableDownloadListener listener = (MinecraftServerExecutableDownloadListener) listenerObject!;
DownloadProgress -= listener.DownloadProgressEventHandler;
newListenerCount = --listenerCount;
if (newListenerCount <= 0) {
cancellationTokenSource.Cancel();
}
}
if (newListenerCount <= 0) {
Logger.Debug("Unregistered last download listener, cancelling download."); Logger.Debug("Unregistered last download listener, cancelling download.");
cancellationTokenSource.Cancel();
} }
else { else {
Logger.Debug("Unregistered download listener, current listener count: {Listeners}", newListenerCount); Logger.Debug("Unregistered download listener, current listener count: {Listeners}", listeners);
} }
} }
@@ -66,19 +51,9 @@ sealed class MinecraftServerExecutableDownloader {
private void OnCompleted(Task task) { private void OnCompleted(Task task) {
Logger.Debug("Download task completed."); Logger.Debug("Download task completed.");
Completed?.Invoke(this, EventArgs.Empty);
lock (this) { Completed = null;
Completed?.Invoke(this, EventArgs.Empty); DownloadProgress = null;
Completed = null;
DownloadProgress = null;
foreach (var registration in listenerCancellationRegistrations) {
registration.Dispose();
}
listenerCancellationRegistrations.Clear();
cancellationTokenSource.Dispose();
}
} }
private sealed class DownloadProgressCallback { private sealed class DownloadProgressCallback {
@@ -93,14 +68,15 @@ sealed class MinecraftServerExecutableDownloader {
} }
} }
private static async Task<string?> DownloadAndGetPath(FileDownloadInfo fileDownloadInfo, string minecraftVersion, string filePath, DownloadProgressCallback progressCallback, CancellationToken cancellationToken) { private async Task<string?> DownloadAndGetPath(FileDownloadInfo fileDownloadInfo, string minecraftVersion, string filePath) {
string tmpFilePath = filePath + ".tmp"; string tmpFilePath = filePath + ".tmp";
var cancellationToken = cancellationTokenSource.Token;
try { try {
Logger.Information("Downloading server version {Version} from: {Url} ({Size})", minecraftVersion, fileDownloadInfo.DownloadUrl, fileDownloadInfo.Size.ToHumanReadable(decimalPlaces: 1)); Logger.Information("Downloading server version {Version} from: {Url} ({Size})", minecraftVersion, fileDownloadInfo.DownloadUrl, fileDownloadInfo.Size.ToHumanReadable(decimalPlaces: 1));
try { try {
using var http = new HttpClient(); using var http = new HttpClient();
await FetchServerExecutableFile(http, progressCallback, fileDownloadInfo, tmpFilePath, cancellationToken); await FetchServerExecutableFile(http, new DownloadProgressCallback(this), fileDownloadInfo, tmpFilePath, cancellationToken);
} catch (Exception) { } catch (Exception) {
TryDeleteExecutableAfterFailure(tmpFilePath); TryDeleteExecutableAfterFailure(tmpFilePath);
throw; throw;
@@ -118,6 +94,8 @@ sealed class MinecraftServerExecutableDownloader {
} catch (Exception e) { } catch (Exception e) {
Logger.Error(e, "An unexpected error occurred."); Logger.Error(e, "An unexpected error occurred.");
return null; return null;
} finally {
cancellationTokenSource.Dispose();
} }
} }

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,6 +18,7 @@ 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; }
@@ -28,12 +30,13 @@ public sealed class AgentServices {
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.InstanceTicketManager = new InstanceTicketManager(agentInfo, controllerConnection);
var instanceManagerInit = new InstanceManagerActor.Init(controllerConnection, agentFolders, AgentState, JavaRuntimeRepository, InstanceTicketManager, BackupManager); var instanceManagerInit = new InstanceManagerActor.Init(controllerConnection, agentFolders, AgentState, JavaRuntimeRepository, InstanceTicketManager, TaskManager, BackupManager);
this.InstanceManager = ActorSystem.ActorOf(InstanceManagerActor.Factory(instanceManagerInit), "InstanceManager"); this.InstanceManager = ActorSystem.ActorOf(InstanceManagerActor.Factory(instanceManagerInit), "InstanceManager");
} }
@@ -47,6 +50,7 @@ public sealed class AgentServices {
Logger.Information("Stopping services..."); Logger.Information("Stopping services...");
await InstanceManager.Stop(new InstanceManagerActor.ShutdownCommand()); await InstanceManager.Stop(new InstanceManagerActor.ShutdownCommand());
await TaskManager.Stop();
BackupManager.Dispose(); BackupManager.Dispose();

View File

@@ -25,7 +25,7 @@ sealed class BackupArchiver {
} }
private bool IsFolderSkipped(ImmutableList<string> relativePath) { private bool IsFolderSkipped(ImmutableList<string> relativePath) {
return relativePath is ["cache" or "crash-reports" or "debug" or "libraries" or "logs" or "mods" or "servermods" or "versions"]; return relativePath is ["cache" or "crash-reports" or "debug" or "libraries" or "logs" or "mods" or "versions"];
} }
[SuppressMessage("ReSharper", "ConvertIfStatementToReturnStatement")] [SuppressMessage("ReSharper", "ConvertIfStatementToReturnStatement")]

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));
return await context.Actor.Request(new InstanceActor.BackupInstanceCommand(backupManager), context.ActorCancellationToken);
} catch (OperationCanceledException) {
return new BackupCreationResult(BackupCreationResultKind.InstanceNotRunning);
} finally { } finally {
backupSemaphore.Release(); backupSemaphore.Release();
} }
} }
private async Task WaitForOnlinePlayers() { private async Task WaitForOnlinePlayers() {
var task = playerCountTracker.WaitForOnlinePlayers(CancellationToken); bool needsToLogOfflinePlayersMessage = true;
if (!task.IsCompleted) {
Logger.Information("Waiting for someone to join before starting a new backup.");
}
process.AddOutputListener(ServerOutputListener, maxLinesToReadFromHistory: 0);
try { try {
await task; while (!CancellationToken.IsCancellationRequested) {
Logger.Information("Players are online, starting a new backup."); serverOutputWhileWaitingForOnlinePlayers.Reset();
} catch (OperationCanceledException) {
throw; var onlinePlayerCount = await serverStatusProtocol.GetOnlinePlayerCount(serverPort, CancellationToken);
} catch (Exception) { if (onlinePlayerCount == null) {
Logger.Warning("Could not detect whether any players are online, starting a new backup."); Logger.Warning("Could not detect whether any players are online, starting a new backup.");
break;
}
if (onlinePlayerCount > 0) {
Logger.Information("Players are online, starting a new backup.");
break;
}
if (needsToLogOfflinePlayersMessage) {
needsToLogOfflinePlayersMessage = false;
Logger.Information("No players are online, waiting for someone to join before starting a new backup.");
}
await Task.Delay(TimeSpan.FromSeconds(10), CancellationToken);
Logger.Debug("Waiting for server output before checking for online players again...");
await serverOutputWhileWaitingForOnlinePlayers.WaitHandle.WaitOneAsync(CancellationToken);
}
} finally {
process.RemoveOutputListener(ServerOutputListener);
}
}
private void ServerOutputListener(object? sender, string line) {
if (!serverOutputWhileWaitingForOnlinePlayers.IsSet) {
serverOutputWhileWaitingForOnlinePlayers.Set();
Logger.Debug("Detected server output, signalling to check for online players again.");
} }
} }

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

@@ -27,8 +27,6 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
private readonly InstanceTicketManager instanceTicketManager; private readonly InstanceTicketManager instanceTicketManager;
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 InstanceRunningState? runningState = null;
@@ -40,7 +38,7 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
this.shutdownCancellationToken = init.ShutdownCancellationToken; this.shutdownCancellationToken = init.ShutdownCancellationToken;
var logger = PhantomLogger.Create<InstanceActor>(init.ShortName); var logger = PhantomLogger.Create<InstanceActor>(init.ShortName);
this.context = new InstanceContext(instanceGuid, init.ShortName, logger, instanceServices, SelfTyped, actorCancellationTokenSource.Token); this.context = new InstanceContext(instanceGuid, init.ShortName, logger, instanceServices, SelfTyped);
Receive<ReportInstanceStatusCommand>(ReportInstanceStatus); Receive<ReportInstanceStatusCommand>(ReportInstanceStatus);
ReceiveAsync<LaunchInstanceCommand>(LaunchInstance); ReceiveAsync<LaunchInstanceCommand>(LaunchInstance);
@@ -135,12 +133,7 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
return new BackupCreationResult(BackupCreationResultKind.InstanceNotRunning); return new BackupCreationResult(BackupCreationResultKind.InstanceNotRunning);
} }
else { else {
SetAndReportStatus(InstanceStatus.BackingUp); return await command.BackupManager.CreateBackup(context.ShortName, runningState.Process, shutdownCancellationToken);
try {
return await command.BackupManager.CreateBackup(context.ShortName, runningState.Process, shutdownCancellationToken);
} finally {
SetAndReportStatus(InstanceStatus.Running);
}
} }
} }
@@ -155,7 +148,6 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
private async Task Shutdown(ShutdownCommand command) { private async Task Shutdown(ShutdownCommand command) {
await StopInstance(new StopInstanceCommand(MinecraftStopStrategy.Instant)); await StopInstance(new StopInstanceCommand(MinecraftStopStrategy.Instant));
await actorCancellationTokenSource.CancelAsync();
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

@@ -6,13 +6,13 @@ using Phantom.Agent.Minecraft.Properties;
using Phantom.Agent.Minecraft.Server; using Phantom.Agent.Minecraft.Server;
using Phantom.Agent.Rpc; using Phantom.Agent.Rpc;
using Phantom.Agent.Services.Backups; using Phantom.Agent.Services.Backups;
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.Utils.Actor; using Phantom.Utils.Actor;
using Phantom.Utils.IO; using Phantom.Utils.IO;
using Phantom.Utils.Logging; using Phantom.Utils.Logging;
using Phantom.Utils.Tasks;
using Serilog; using Serilog;
namespace Phantom.Agent.Services.Instances; namespace Phantom.Agent.Services.Instances;
@@ -20,7 +20,7 @@ namespace Phantom.Agent.Services.Instances;
sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand> { sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand> {
private static readonly ILogger Logger = PhantomLogger.Create<InstanceManagerActor>(); 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 readonly record struct Init(ControllerConnection ControllerConnection, AgentFolders AgentFolders, AgentState AgentState, JavaRuntimeRepository JavaRuntimeRepository, InstanceTicketManager InstanceTicketManager, TaskManager TaskManager, BackupManager BackupManager);
public static Props<ICommand> Factory(Init init) { public static Props<ICommand> Factory(Init init) {
return Props<ICommand>.Create(() => new InstanceManagerActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume }); return Props<ICommand>.Create(() => new InstanceManagerActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume });
@@ -47,12 +47,12 @@ sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand>
var minecraftServerExecutables = new MinecraftServerExecutables(init.AgentFolders.ServerExecutableFolderPath); var minecraftServerExecutables = new MinecraftServerExecutables(init.AgentFolders.ServerExecutableFolderPath);
var launchServices = new LaunchServices(minecraftServerExecutables, init.JavaRuntimeRepository); var launchServices = new LaunchServices(minecraftServerExecutables, init.JavaRuntimeRepository);
this.instanceServices = new InstanceServices(init.ControllerConnection, init.BackupManager, launchServices); this.instanceServices = new InstanceServices(init.ControllerConnection, init.TaskManager, init.BackupManager, launchServices);
ReceiveAndReply<ConfigureInstanceCommand, Result<ConfigureInstanceResult, InstanceActionFailure>>(ConfigureInstance); ReceiveAndReply<ConfigureInstanceCommand, InstanceActionResult<ConfigureInstanceResult>>(ConfigureInstance);
ReceiveAndReply<LaunchInstanceCommand, Result<LaunchInstanceResult, InstanceActionFailure>>(LaunchInstance); ReceiveAndReply<LaunchInstanceCommand, InstanceActionResult<LaunchInstanceResult>>(LaunchInstance);
ReceiveAndReply<StopInstanceCommand, Result<StopInstanceResult, InstanceActionFailure>>(StopInstance); ReceiveAndReply<StopInstanceCommand, InstanceActionResult<StopInstanceResult>>(StopInstance);
ReceiveAsyncAndReply<SendCommandToInstanceCommand, Result<SendCommandToInstanceResult, InstanceActionFailure>>(SendCommandToInstance); ReceiveAsyncAndReply<SendCommandToInstanceCommand, InstanceActionResult<SendCommandToInstanceResult>>(SendCommandToInstance);
ReceiveAsync<ShutdownCommand>(Shutdown); ReceiveAsync<ShutdownCommand>(Shutdown);
} }
@@ -65,17 +65,17 @@ sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand>
public interface ICommand {} 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 ConfigureInstanceCommand(Guid InstanceGuid, InstanceConfiguration Configuration, InstanceLaunchProperties LaunchProperties, bool LaunchNow, bool AlwaysReportStatus) : ICommand, ICanReply<InstanceActionResult<ConfigureInstanceResult>>;
public sealed record LaunchInstanceCommand(Guid InstanceGuid) : ICommand, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>; public sealed record LaunchInstanceCommand(Guid InstanceGuid) : ICommand, ICanReply<InstanceActionResult<LaunchInstanceResult>>;
public sealed record StopInstanceCommand(Guid InstanceGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>; public sealed record StopInstanceCommand(Guid InstanceGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<InstanceActionResult<StopInstanceResult>>;
public sealed record SendCommandToInstanceCommand(Guid InstanceGuid, string Command) : ICommand, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>; public sealed record SendCommandToInstanceCommand(Guid InstanceGuid, string Command) : ICommand, ICanReply<InstanceActionResult<SendCommandToInstanceResult>>;
public sealed record ShutdownCommand : ICommand; public sealed record ShutdownCommand : ICommand;
private Result<ConfigureInstanceResult, InstanceActionFailure> ConfigureInstance(ConfigureInstanceCommand command) { private InstanceActionResult<ConfigureInstanceResult> ConfigureInstance(ConfigureInstanceCommand command) {
var instanceGuid = command.InstanceGuid; var instanceGuid = command.InstanceGuid;
var configuration = command.Configuration; var configuration = command.Configuration;
@@ -102,7 +102,6 @@ sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand>
IServerLauncher launcher = configuration.MinecraftServerKind switch { IServerLauncher launcher = configuration.MinecraftServerKind switch {
MinecraftServerKind.Vanilla => new VanillaLauncher(properties), MinecraftServerKind.Vanilla => new VanillaLauncher(properties),
MinecraftServerKind.Fabric => new FabricLauncher(properties), MinecraftServerKind.Fabric => new FabricLauncher(properties),
MinecraftServerKind.Forge => new ForgeLauncher(properties),
_ => InvalidLauncher.Instance _ => InvalidLauncher.Instance
}; };
@@ -131,64 +130,65 @@ sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand>
LaunchInstance(new LaunchInstanceCommand(instanceGuid)); LaunchInstance(new LaunchInstanceCommand(instanceGuid));
} }
return ConfigureInstanceResult.Success; return InstanceActionResult.Concrete(ConfigureInstanceResult.Success);
} }
private Result<LaunchInstanceResult, InstanceActionFailure> LaunchInstance(LaunchInstanceCommand command) { private InstanceActionResult<LaunchInstanceResult> LaunchInstance(LaunchInstanceCommand command) {
var instanceGuid = command.InstanceGuid; var instanceGuid = command.InstanceGuid;
if (!instances.TryGetValue(instanceGuid, out var instanceInfo)) { if (!instances.TryGetValue(instanceGuid, out var instanceInfo)) {
return InstanceActionFailure.InstanceDoesNotExist; return InstanceActionResult.General<LaunchInstanceResult>(InstanceActionGeneralResult.InstanceDoesNotExist);
} }
var ticket = instanceTicketManager.Reserve(instanceInfo.Configuration); var ticket = instanceTicketManager.Reserve(instanceInfo.Configuration);
if (!ticket) { if (ticket is Result<InstanceTicketManager.Ticket, LaunchInstanceResult>.Fail fail) {
return ticket.Error; return InstanceActionResult.Concrete(fail.Error);
} }
if (agentState.InstancesByGuid.TryGetValue(instanceGuid, out var instance)) { if (agentState.InstancesByGuid.TryGetValue(instanceGuid, out var instance)) {
var status = instance.Status; var status = instance.Status;
if (status.IsRunning()) { if (status.IsRunning()) {
return LaunchInstanceResult.InstanceAlreadyRunning; return InstanceActionResult.Concrete(LaunchInstanceResult.InstanceAlreadyRunning);
} }
else if (status.IsLaunching()) { else if (status.IsLaunching()) {
return LaunchInstanceResult.InstanceAlreadyLaunching; return InstanceActionResult.Concrete(LaunchInstanceResult.InstanceAlreadyLaunching);
} }
} }
// TODO report status?
instanceInfo.Actor.Tell(new InstanceActor.LaunchInstanceCommand(instanceInfo.Configuration, instanceInfo.Launcher, ticket.Value, IsRestarting: false)); instanceInfo.Actor.Tell(new InstanceActor.LaunchInstanceCommand(instanceInfo.Configuration, instanceInfo.Launcher, ticket.Value, IsRestarting: false));
return LaunchInstanceResult.LaunchInitiated; return InstanceActionResult.Concrete(LaunchInstanceResult.LaunchInitiated);
} }
private Result<StopInstanceResult, InstanceActionFailure> StopInstance(StopInstanceCommand command) { private InstanceActionResult<StopInstanceResult> StopInstance(StopInstanceCommand command) {
var instanceGuid = command.InstanceGuid; var instanceGuid = command.InstanceGuid;
if (!instances.TryGetValue(instanceGuid, out var instanceInfo)) { if (!instances.TryGetValue(instanceGuid, out var instanceInfo)) {
return InstanceActionFailure.InstanceDoesNotExist; return InstanceActionResult.General<StopInstanceResult>(InstanceActionGeneralResult.InstanceDoesNotExist);
} }
if (agentState.InstancesByGuid.TryGetValue(instanceGuid, out var instance)) { if (agentState.InstancesByGuid.TryGetValue(instanceGuid, out var instance)) {
var status = instance.Status; var status = instance.Status;
if (status.IsStopping()) { if (status.IsStopping()) {
return StopInstanceResult.InstanceAlreadyStopping; return InstanceActionResult.Concrete(StopInstanceResult.InstanceAlreadyStopping);
} }
else if (!status.CanStop()) { else if (!status.IsRunning()) {
return StopInstanceResult.InstanceAlreadyStopped; return InstanceActionResult.Concrete(StopInstanceResult.InstanceAlreadyStopped);
} }
} }
instanceInfo.Actor.Tell(new InstanceActor.StopInstanceCommand(command.StopStrategy)); instanceInfo.Actor.Tell(new InstanceActor.StopInstanceCommand(command.StopStrategy));
return StopInstanceResult.StopInitiated; return InstanceActionResult.Concrete(StopInstanceResult.StopInitiated);
} }
private async Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> SendCommandToInstance(SendCommandToInstanceCommand command) { private async Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommandToInstance(SendCommandToInstanceCommand command) {
var instanceGuid = command.InstanceGuid; var instanceGuid = command.InstanceGuid;
if (!instances.TryGetValue(instanceGuid, out var instanceInfo)) { if (!instances.TryGetValue(instanceGuid, out var instanceInfo)) {
return InstanceActionFailure.InstanceDoesNotExist; return InstanceActionResult.General<SendCommandToInstanceResult>(InstanceActionGeneralResult.InstanceDoesNotExist);
} }
try { try {
return await instanceInfo.Actor.Request(new InstanceActor.SendCommandToInstanceCommand(command.Command), shutdownCancellationToken); return InstanceActionResult.Concrete(await instanceInfo.Actor.Request(new InstanceActor.SendCommandToInstanceCommand(command.Command), shutdownCancellationToken));
} catch (OperationCanceledException) { } catch (OperationCanceledException) {
return InstanceActionFailure.AgentShuttingDown; return InstanceActionResult.General<SendCommandToInstanceResult>(InstanceActionGeneralResult.AgentShuttingDown);
} }
} }

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, BackupManager BackupManager, LaunchServices LaunchServices);

View File

@@ -1,8 +1,8 @@
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.Common.Data.Instance; using Phantom.Common.Data.Instance;
using Phantom.Utils.Tasks;
namespace Phantom.Agent.Services.Instances.State; namespace Phantom.Agent.Services.Instances.State;

View File

@@ -8,7 +8,7 @@ using Phantom.Utils.Tasks;
namespace Phantom.Agent.Services.Instances.State; namespace Phantom.Agent.Services.Instances.State;
sealed class InstanceLogSender : CancellableBackgroundTask { sealed class InstanceLogSender : CancellableBackgroundTask {
private static readonly BoundedChannelOptions BufferOptions = new (capacity: 100) { private static readonly BoundedChannelOptions BufferOptions = new (capacity: 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,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

@@ -19,7 +19,6 @@ sealed class InstanceRunningState : IDisposable {
private readonly CancellationToken cancellationToken; private readonly CancellationToken cancellationToken;
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;
@@ -32,10 +31,9 @@ sealed class InstanceRunningState : IDisposable {
this.Process = process; this.Process = process;
this.cancellationToken = cancellationToken; 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;
} }
@@ -95,11 +93,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,8 +102,8 @@ sealed class InstanceRunningState : IDisposable {
isDisposed = true; isDisposed = true;
} }
OnStopInitiated();
logSender.Stop(); logSender.Stop();
backupScheduler.Stop();
Process.Dispose(); Process.Dispose();

View File

@@ -25,8 +25,6 @@ static class InstanceStopProcedure {
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);
@@ -87,7 +85,7 @@ static class InstanceStopProcedure {
private static async Task WaitForSessionToEnd(InstanceContext context, InstanceProcess process) { private static async Task WaitForSessionToEnd(InstanceContext context, InstanceProcess process) {
try { try {
await process.WaitForExit(TimeSpan.FromSeconds(55)); await process.WaitForExit(TimeSpan.FromSeconds(55));
} 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,5 +1,4 @@
using Phantom.Agent.Services.Instances; using Phantom.Agent.Services.Instances;
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.Common.Messages.Agent; using Phantom.Common.Messages.Agent;
@@ -33,10 +32,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);
} }
@@ -75,23 +74,23 @@ 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.InstanceManager.Request(new InstanceManagerActor.ConfigureInstanceCommand(message.InstanceGuid, message.Configuration, message.LaunchProperties, message.LaunchNow, alwaysReportStatus));
} }
private async Task<Result<ConfigureInstanceResult, InstanceActionFailure>> HandleConfigureInstance(ConfigureInstanceMessage message) { private async Task<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.InstanceManager.Request(new InstanceManagerActor.LaunchInstanceCommand(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.InstanceManager.Request(new InstanceManagerActor.StopInstanceCommand(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.InstanceManager.Request(new InstanceManagerActor.SendCommandToInstanceCommand(message.InstanceGuid, message.Command));
} }

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

@@ -4,7 +4,8 @@ using MemoryPack;
namespace Phantom.Common.Data.Web.Users; namespace Phantom.Common.Data.Web.Users;
[MemoryPackable(GenerateType.VersionTolerant)] [MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record LogInSuccess( public sealed partial record LogInSuccess (
[property: MemoryPackOrder(0)] AuthenticatedUserInfo UserInfo, [property: MemoryPackOrder(0)] Guid UserGuid,
[property: MemoryPackOrder(1)] ImmutableArray<byte> AuthToken [property: MemoryPackOrder(1)] PermissionSet Permissions,
[property: MemoryPackOrder(2)] ImmutableArray<byte> Token
); );

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

@@ -2,6 +2,5 @@
public enum MinecraftServerKind : ushort { public enum MinecraftServerKind : ushort {
Vanilla = 1, Vanilla = 1,
Fabric = 2, Fabric = 2
Forge = 3
} }

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

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,10 +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.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;
@@ -13,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;
} }
@@ -60,8 +54,9 @@ sealed class UserManager {
} }
} }
else { else {
if (userRepository.SetUserPassword(user, password).TryGetError(out var error)) { var result = userRepository.SetUserPassword(user, password);
return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.UpdatingFailed(error); if (!result) {
return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.UpdatingFailed(result.Error);
} }
auditLogWriter.AdministratorUserModified(user); auditLogWriter.AdministratorUserModified(user);
@@ -91,14 +86,10 @@ sealed class UserManager {
} }
} }
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);
@@ -119,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);
@@ -132,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

@@ -27,7 +27,7 @@ WORKDIR /data
COPY --from=eclipse-temurin:8-jre /opt/java/openjdk /opt/java/8 COPY --from=eclipse-temurin:8-jre /opt/java/openjdk /opt/java/8
COPY --from=eclipse-temurin:16-jdk /opt/java/openjdk /opt/java/16 COPY --from=eclipse-temurin:16-jdk /opt/java/openjdk /opt/java/16
COPY --from=eclipse-temurin:17-jre /opt/java/openjdk /opt/java/17 COPY --from=eclipse-temurin:17-jre /opt/java/openjdk /opt/java/17
COPY --from=eclipse-temurin:21-jre /opt/java/openjdk /opt/java/21 COPY --from=eclipse-temurin:20-jre /opt/java/openjdk /opt/java/20
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive

View File

@@ -1,12 +1,10 @@
using System.Diagnostics.CodeAnalysis; using Akka.Actor;
using Akka.Actor;
using Akka.Configuration; using Akka.Configuration;
using Akka.Dispatch; using Akka.Dispatch;
using Akka.Dispatch.MessageQueues; using Akka.Dispatch.MessageQueues;
namespace Phantom.Utils.Actor.Mailbox; namespace Phantom.Utils.Actor.Mailbox;
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
public sealed class UnboundedJumpAheadMailbox : MailboxType, IProducesMessageQueue<UnboundedJumpAheadMessageQueue> { public sealed class UnboundedJumpAheadMailbox : MailboxType, IProducesMessageQueue<UnboundedJumpAheadMessageQueue> {
public const string Name = "unbounded-jump-ahead-mailbox"; public const string Name = "unbounded-jump-ahead-mailbox";

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