mirror of
				https://github.com/chylex/Minecraft-Phantom-Panel.git
				synced 2025-10-31 20:17:16 +01:00 
			
		
		
		
	Compare commits
	
		
			29 Commits
		
	
	
		
			94148add2d
			...
			wip-forge
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 35ca896849 | |||
| 30b3ba60cd | |||
| 8149d31d51 | |||
| 9873f8779f | |||
| 31e101b21e | |||
| 398bb14742 | |||
| 4e89d7b12f | |||
| 3b313bf0a8 | |||
| 21c90cb7c2 | |||
| 7b68b5d50d | |||
| 2d70d2b7d1 | |||
| 8c623171f3 | |||
| 62f8c685f0 | |||
| 4a110db078 | |||
| f683a1f700 | |||
| 3ffb37529c | |||
| 4c3b81c54a | |||
| 8e2b019aa1 | |||
| 9a2c13c1e0 | |||
| 991b32032c | |||
| 875fd9a766 | |||
| f7f08ec55c | |||
| 1b12fd9c3b | |||
| 424dccb14e | |||
| d03f532996 | |||
| c99f5bc6bf | |||
| d591318340 | |||
| c7b57fac97 | |||
| 137a2a53c3 | 
| @@ -3,7 +3,7 @@ | ||||
|   "isRoot": true, | ||||
|   "tools": { | ||||
|     "dotnet-ef": { | ||||
|       "version": "7.0.0-rc.1.22426.7", | ||||
|       "version": "8.0.3", | ||||
|       "commands": [ | ||||
|         "dotnet-ef" | ||||
|       ] | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using System.Text; | ||||
| using System.Collections.ObjectModel; | ||||
| using System.Text; | ||||
| using Phantom.Agent.Minecraft.Instance; | ||||
| using Phantom.Agent.Minecraft.Java; | ||||
| using Phantom.Agent.Minecraft.Server; | ||||
| @@ -11,6 +12,7 @@ public abstract class BaseLauncher : IServerLauncher { | ||||
| 	private readonly InstanceProperties instanceProperties; | ||||
|  | ||||
| 	protected string MinecraftVersion => instanceProperties.ServerVersion; | ||||
| 	protected string InstanceFolder => instanceProperties.InstanceFolder; | ||||
|  | ||||
| 	private protected BaseLauncher(InstanceProperties instanceProperties) { | ||||
| 		this.instanceProperties = instanceProperties; | ||||
| @@ -51,16 +53,14 @@ public abstract class BaseLauncher : IServerLauncher { | ||||
| 		 | ||||
| 		var processConfigurator = new ProcessConfigurator { | ||||
| 			FileName = javaRuntimeExecutable.ExecutablePath, | ||||
| 			WorkingDirectory = instanceProperties.InstanceFolder, | ||||
| 			WorkingDirectory = InstanceFolder, | ||||
| 			RedirectInput = true, | ||||
| 			UseShellExecute = false | ||||
| 		}; | ||||
| 		 | ||||
| 		var processArguments = processConfigurator.ArgumentList; | ||||
| 		PrepareJvmArguments(serverJar).Build(processArguments); | ||||
| 		processArguments.Add("-jar"); | ||||
| 		processArguments.Add(serverJar.FilePath); | ||||
| 		processArguments.Add("nogui"); | ||||
| 		PrepareJavaProcessArguments(processArguments, serverJar.FilePath); | ||||
|  | ||||
| 		var process = processConfigurator.CreateProcess(); | ||||
| 		var instanceProcess = new InstanceProcess(instanceProperties, process); | ||||
| @@ -99,6 +99,12 @@ public abstract class BaseLauncher : IServerLauncher { | ||||
|  | ||||
| 	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) { | ||||
| 		return Task.FromResult(new ServerJarInfo(serverJarPath)); | ||||
| 	} | ||||
|   | ||||
| @@ -0,0 +1,29 @@ | ||||
| using System.Collections.ObjectModel; | ||||
| using Phantom.Agent.Minecraft.Instance; | ||||
| using Phantom.Agent.Minecraft.Java; | ||||
| using Serilog; | ||||
|  | ||||
| namespace Phantom.Agent.Minecraft.Launcher.Types;  | ||||
|  | ||||
| public sealed class ForgeLauncher : BaseLauncher { | ||||
| 	public ForgeLauncher(InstanceProperties instanceProperties) : base(instanceProperties) {} | ||||
| 	 | ||||
| 	private protected override void CustomizeJvmArguments(JvmArgumentBuilder arguments) { | ||||
| 		arguments.AddProperty("terminal.ansi", "true"); // TODO | ||||
| 	} | ||||
|  | ||||
| 	protected override void PrepareJavaProcessArguments(Collection<string> processArguments, string serverJarFilePath) { | ||||
| 		if (OperatingSystem.IsWindows()) { | ||||
| 			processArguments.Add("@libraries/net/minecraftforge/forge/1.20.1-47.2.0/win_args.txt"); | ||||
| 		} | ||||
| 		else { | ||||
| 			processArguments.Add("@libraries/net/minecraftforge/forge/1.20.1-47.2.0/unix_args.txt"); | ||||
| 		} | ||||
| 		 | ||||
| 		processArguments.Add("nogui"); | ||||
| 	} | ||||
|  | ||||
| 	private protected override Task<ServerJarInfo> PrepareServerJar(ILogger logger, string serverJarPath, CancellationToken cancellationToken) { | ||||
| 		return Task.FromResult(new ServerJarInfo(Path.Combine(InstanceFolder, "run.sh"))); | ||||
| 	} | ||||
| } | ||||
| @@ -18,4 +18,5 @@ static class MinecraftServerProperties { | ||||
| 	public static readonly MinecraftServerProperty<ushort> ServerPort = new UnsignedShort("server-port"); | ||||
| 	public static readonly MinecraftServerProperty<ushort> RconPort = new UnsignedShort("rcon.port"); | ||||
| 	public static readonly MinecraftServerProperty<bool> EnableRcon = new Boolean("enable-rcon"); | ||||
| 	public static readonly MinecraftServerProperty<bool> SyncChunkWrites = new Boolean("sync-chunk-writes"); | ||||
| } | ||||
|   | ||||
| @@ -5,11 +5,13 @@ namespace Phantom.Agent.Minecraft.Properties; | ||||
| public sealed record ServerProperties( | ||||
| 	ushort ServerPort, | ||||
| 	ushort RconPort, | ||||
| 	bool EnableRcon = true | ||||
| 	bool EnableRcon = true, | ||||
| 	bool SyncChunkWrites = false | ||||
| ) { | ||||
| 	internal void SetTo(JavaPropertiesFileEditor properties) { | ||||
| 		MinecraftServerProperties.ServerPort.Set(properties, ServerPort); | ||||
| 		MinecraftServerProperties.RconPort.Set(properties, RconPort); | ||||
| 		MinecraftServerProperties.EnableRcon.Set(properties, EnableRcon); | ||||
| 		MinecraftServerProperties.SyncChunkWrites.Set(properties, SyncChunkWrites); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -16,32 +16,47 @@ sealed class MinecraftServerExecutableDownloader { | ||||
| 	public event EventHandler? Completed; | ||||
| 	 | ||||
| 	private readonly CancellationTokenSource cancellationTokenSource = new (); | ||||
| 	private int listeners = 0; | ||||
| 	 | ||||
| 	private readonly List<CancellationTokenRegistration> listenerCancellationRegistrations = new (); | ||||
| 	private int listenerCount = 0; | ||||
|  | ||||
| 	public MinecraftServerExecutableDownloader(FileDownloadInfo fileDownloadInfo, string minecraftVersion, string filePath, MinecraftServerExecutableDownloadListener listener) { | ||||
| 		Register(listener); | ||||
| 		Task = DownloadAndGetPath(fileDownloadInfo, minecraftVersion, filePath); | ||||
| 		Task = DownloadAndGetPath(fileDownloadInfo, minecraftVersion, filePath, new DownloadProgressCallback(this), cancellationTokenSource.Token); | ||||
| 		Task.ContinueWith(OnCompleted, TaskScheduler.Default); | ||||
| 	} | ||||
|  | ||||
| 	public void Register(MinecraftServerExecutableDownloadListener listener) { | ||||
| 		++listeners; | ||||
| 		Logger.Debug("Registered download listener, current listener count: {Listeners}", listeners); | ||||
| 		int newListenerCount; | ||||
| 		 | ||||
| 		DownloadProgress += listener.DownloadProgressEventHandler; | ||||
| 		listener.CancellationToken.Register(Unregister, listener); | ||||
| 		lock (this) { | ||||
| 			newListenerCount = ++listenerCount; | ||||
| 			 | ||||
| 			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) { | ||||
| 		MinecraftServerExecutableDownloadListener listener = (MinecraftServerExecutableDownloadListener) listenerObject!; | ||||
| 		DownloadProgress -= listener.DownloadProgressEventHandler; | ||||
| 		int newListenerCount; | ||||
| 		 | ||||
| 		lock (this) { | ||||
| 			MinecraftServerExecutableDownloadListener listener = (MinecraftServerExecutableDownloadListener) listenerObject!; | ||||
| 			DownloadProgress -= listener.DownloadProgressEventHandler; | ||||
|  | ||||
| 		if (--listeners <= 0) { | ||||
| 			newListenerCount = --listenerCount; | ||||
| 			if (newListenerCount <= 0) { | ||||
| 				cancellationTokenSource.Cancel(); | ||||
| 			} | ||||
| 		} | ||||
| 		 | ||||
| 		if (newListenerCount <= 0) { | ||||
| 			Logger.Debug("Unregistered last download listener, cancelling download."); | ||||
| 			cancellationTokenSource.Cancel(); | ||||
| 		} | ||||
| 		else { | ||||
| 			Logger.Debug("Unregistered download listener, current listener count: {Listeners}", listeners); | ||||
| 			Logger.Debug("Unregistered download listener, current listener count: {Listeners}", newListenerCount); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -51,9 +66,19 @@ sealed class MinecraftServerExecutableDownloader { | ||||
|  | ||||
| 	private void OnCompleted(Task task) { | ||||
| 		Logger.Debug("Download task completed."); | ||||
| 		Completed?.Invoke(this, EventArgs.Empty); | ||||
| 		Completed = null; | ||||
| 		DownloadProgress = null; | ||||
|  | ||||
| 		lock (this) { | ||||
| 			Completed?.Invoke(this, EventArgs.Empty); | ||||
| 			Completed = null; | ||||
| 			DownloadProgress = null; | ||||
|  | ||||
| 			foreach (var registration in listenerCancellationRegistrations) { | ||||
| 				registration.Dispose(); | ||||
| 			} | ||||
| 			 | ||||
| 			listenerCancellationRegistrations.Clear(); | ||||
| 			cancellationTokenSource.Dispose(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private sealed class DownloadProgressCallback { | ||||
| @@ -68,15 +93,14 @@ sealed class MinecraftServerExecutableDownloader { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private async Task<string?> DownloadAndGetPath(FileDownloadInfo fileDownloadInfo, string minecraftVersion, string filePath) { | ||||
| 	private static async Task<string?> DownloadAndGetPath(FileDownloadInfo fileDownloadInfo, string minecraftVersion, string filePath, DownloadProgressCallback progressCallback, CancellationToken cancellationToken) { | ||||
| 		string tmpFilePath = filePath + ".tmp"; | ||||
|  | ||||
| 		var cancellationToken = cancellationTokenSource.Token; | ||||
| 		try { | ||||
| 			Logger.Information("Downloading server version {Version} from: {Url} ({Size})", minecraftVersion, fileDownloadInfo.DownloadUrl, fileDownloadInfo.Size.ToHumanReadable(decimalPlaces: 1)); | ||||
| 			try { | ||||
| 				using var http = new HttpClient(); | ||||
| 				await FetchServerExecutableFile(http, new DownloadProgressCallback(this), fileDownloadInfo, tmpFilePath, cancellationToken); | ||||
| 				await FetchServerExecutableFile(http, progressCallback, fileDownloadInfo, tmpFilePath, cancellationToken); | ||||
| 			} catch (Exception) { | ||||
| 				TryDeleteExecutableAfterFailure(tmpFilePath); | ||||
| 				throw; | ||||
| @@ -94,8 +118,6 @@ sealed class MinecraftServerExecutableDownloader { | ||||
| 		} catch (Exception e) { | ||||
| 			Logger.Error(e, "An unexpected error occurred."); | ||||
| 			return null; | ||||
| 		} finally { | ||||
| 			cancellationTokenSource.Dispose(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -3,28 +3,12 @@ using System.Buffers.Binary; | ||||
| using System.Net; | ||||
| using System.Net.Sockets; | ||||
| using System.Text; | ||||
| using Phantom.Utils.Logging; | ||||
| using Serilog; | ||||
| using Phantom.Common.Data.Instance; | ||||
|  | ||||
| namespace Phantom.Agent.Minecraft.Server; | ||||
|  | ||||
| public sealed class ServerStatusProtocol { | ||||
| 	private readonly ILogger logger; | ||||
|  | ||||
| 	public ServerStatusProtocol(string loggerName) { | ||||
| 		this.logger = PhantomLogger.Create<ServerStatusProtocol>(loggerName); | ||||
| 	} | ||||
|  | ||||
| 	public async Task<int?> GetOnlinePlayerCount(int serverPort, CancellationToken cancellationToken) { | ||||
| 		try { | ||||
| 			return await GetOnlinePlayerCountOrThrow(serverPort, cancellationToken); | ||||
| 		} catch (Exception e) { | ||||
| 			logger.Error(e, "Caught exception while checking if players are online."); | ||||
| 			return null; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private async Task<int?> GetOnlinePlayerCountOrThrow(int serverPort, CancellationToken cancellationToken) { | ||||
| public static class ServerStatusProtocol { | ||||
| 	public static async Task<InstancePlayerCounts> GetPlayerCounts(ushort serverPort, CancellationToken cancellationToken) { | ||||
| 		using var tcpClient = new TcpClient(); | ||||
| 		await tcpClient.ConnectAsync(IPAddress.Loopback, serverPort, cancellationToken); | ||||
| 		var tcpStream = tcpClient.GetStream(); | ||||
| @@ -33,24 +17,22 @@ public sealed class ServerStatusProtocol { | ||||
| 		tcpStream.WriteByte(0xFE); | ||||
| 		await tcpStream.FlushAsync(cancellationToken); | ||||
|  | ||||
| 		short? messageLength = await ReadStreamHeader(tcpStream, cancellationToken); | ||||
| 		return messageLength == null ? null : await ReadOnlinePlayerCount(tcpStream, messageLength.Value * 2, cancellationToken); | ||||
| 		short messageLength = await ReadStreamHeader(tcpStream, cancellationToken); | ||||
| 		return await ReadPlayerCounts(tcpStream, messageLength * 2, cancellationToken); | ||||
| 	} | ||||
|  | ||||
| 	private async Task<short?> ReadStreamHeader(NetworkStream tcpStream, CancellationToken cancellationToken) { | ||||
| 	private static async Task<short> ReadStreamHeader(NetworkStream tcpStream, CancellationToken cancellationToken) { | ||||
| 		var headerBuffer = ArrayPool<byte>.Shared.Rent(3); | ||||
| 		try { | ||||
| 			await tcpStream.ReadExactlyAsync(headerBuffer, 0, 3, cancellationToken); | ||||
|  | ||||
| 			if (headerBuffer[0] != 0xFF) { | ||||
| 				logger.Error("Unexpected first byte in response from server: {FirstByte}.", headerBuffer[0]); | ||||
| 				return null; | ||||
| 				throw new ProtocolException("Unexpected first byte in response from server: " + headerBuffer[0]); | ||||
| 			} | ||||
|  | ||||
| 			short messageLength = BinaryPrimitives.ReadInt16BigEndian(headerBuffer.AsSpan(1)); | ||||
| 			if (messageLength <= 0) { | ||||
| 				logger.Error("Unexpected message length in response from server: {MessageLength}.", messageLength); | ||||
| 				return null; | ||||
| 				throw new ProtocolException("Unexpected message length in response from server: " + messageLength); | ||||
| 			} | ||||
| 			 | ||||
| 			return messageLength; | ||||
| @@ -59,35 +41,54 @@ public sealed class ServerStatusProtocol { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private async Task<int?> ReadOnlinePlayerCount(NetworkStream tcpStream, int messageLength, CancellationToken cancellationToken) { | ||||
| 	private static async Task<InstancePlayerCounts> ReadPlayerCounts(NetworkStream tcpStream, int messageLength, CancellationToken cancellationToken) { | ||||
| 		var messageBuffer = ArrayPool<byte>.Shared.Rent(messageLength); | ||||
| 		try { | ||||
| 			await tcpStream.ReadExactlyAsync(messageBuffer, 0, messageLength, cancellationToken); | ||||
|  | ||||
| 			// Valid response separator encoded in UTF-16BE is 0x00 0xA7 (§). | ||||
| 			const byte SeparatorSecondByte = 0xA7; | ||||
| 			 | ||||
| 			static bool IsValidSeparator(ReadOnlySpan<byte> buffer, int index) { | ||||
| 				return index > 0 && buffer[index - 1] == 0x00; | ||||
| 			} | ||||
| 			 | ||||
| 			int separator2 = Array.LastIndexOf(messageBuffer, SeparatorSecondByte); | ||||
| 			int separator1 = separator2 == -1 ? -1 : Array.LastIndexOf(messageBuffer, SeparatorSecondByte, separator2 - 1); | ||||
| 			if (!IsValidSeparator(messageBuffer, separator1) || !IsValidSeparator(messageBuffer, separator2)) { | ||||
| 				logger.Error("Could not find message separators in response from server."); | ||||
| 				return null; | ||||
| 			} | ||||
|  | ||||
| 			string onlinePlayerCountStr = Encoding.BigEndianUnicode.GetString(messageBuffer.AsSpan((separator1 + 1)..(separator2 - 1))); | ||||
| 			if (!int.TryParse(onlinePlayerCountStr, out int onlinePlayerCount)) { | ||||
| 				logger.Error("Could not parse online player count in response from server: {OnlinePlayerCount}.", onlinePlayerCountStr); | ||||
| 				return null; | ||||
| 			} | ||||
| 			 | ||||
| 			logger.Debug("Detected {OnlinePlayerCount} online player(s).", onlinePlayerCount); | ||||
| 			return onlinePlayerCount; | ||||
| 			return ReadPlayerCountsFromResponse(messageBuffer.AsSpan(0, messageLength)); | ||||
| 		} finally { | ||||
| 			ArrayPool<byte>.Shared.Return(messageBuffer); | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	/// <summary> | ||||
| 	/// Legacy query protocol uses the paragraph symbol (§) as separator encoded in UTF-16BE. | ||||
| 	/// </summary> | ||||
| 	private static readonly byte[] Separator = { 0x00, 0xA7 }; | ||||
| 	 | ||||
| 	private static InstancePlayerCounts ReadPlayerCountsFromResponse(ReadOnlySpan<byte> messageBuffer) { | ||||
| 		int lastSeparator = messageBuffer.LastIndexOf(Separator); | ||||
| 		int middleSeparator = messageBuffer[..lastSeparator].LastIndexOf(Separator); | ||||
| 		 | ||||
| 		if (lastSeparator == -1 || middleSeparator == -1) { | ||||
| 			throw new ProtocolException("Could not find message separators in response from server."); | ||||
| 		} | ||||
|  | ||||
| 		var onlinePlayerCountBuffer = messageBuffer[(middleSeparator + Separator.Length)..lastSeparator]; | ||||
| 		var maximumPlayerCountBuffer = messageBuffer[(lastSeparator + Separator.Length)..]; | ||||
| 		 | ||||
| 		// Player counts are integers, whose maximum string length is 10 characters. | ||||
| 		Span<char> integerStringBuffer = stackalloc char[10]; | ||||
| 		 | ||||
| 		return new InstancePlayerCounts( | ||||
| 			DecodeAndParsePlayerCount(onlinePlayerCountBuffer, integerStringBuffer, "online"), | ||||
| 			DecodeAndParsePlayerCount(maximumPlayerCountBuffer, integerStringBuffer, "maximum") | ||||
| 		); | ||||
| 	} | ||||
| 	 | ||||
| 	private static int DecodeAndParsePlayerCount(ReadOnlySpan<byte> inputBuffer, Span<char> tempCharBuffer, string countType) { | ||||
| 		if (!Encoding.BigEndianUnicode.TryGetChars(inputBuffer, tempCharBuffer, out int charCount)) { | ||||
| 			throw new ProtocolException("Could not decode " + countType + " player count in response from server."); | ||||
| 		} | ||||
| 		 | ||||
| 		if (!int.TryParse(tempCharBuffer, out int playerCount)) { | ||||
| 			throw new ProtocolException("Could not parse " + countType + " player count in response from server: " + tempCharBuffer[..charCount].ToString()); | ||||
| 		} | ||||
| 		 | ||||
| 		return playerCount; | ||||
| 	} | ||||
| 	 | ||||
| 	public sealed class ProtocolException : Exception { | ||||
| 		internal ProtocolException(string message) : base(message) {} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -6,7 +6,6 @@ using Phantom.Agent.Services.Instances; | ||||
| using Phantom.Common.Data.Agent; | ||||
| using Phantom.Utils.Actor; | ||||
| using Phantom.Utils.Logging; | ||||
| using Phantom.Utils.Tasks; | ||||
| using Serilog; | ||||
|  | ||||
| namespace Phantom.Agent.Services; | ||||
| @@ -18,7 +17,6 @@ public sealed class AgentServices { | ||||
|  | ||||
| 	private AgentFolders AgentFolders { get; } | ||||
| 	private AgentState AgentState { get; } | ||||
| 	private TaskManager TaskManager { get; } | ||||
| 	private BackupManager BackupManager { get; } | ||||
|  | ||||
| 	internal JavaRuntimeRepository JavaRuntimeRepository { get; } | ||||
| @@ -30,13 +28,12 @@ public sealed class AgentServices { | ||||
| 		 | ||||
| 		this.AgentFolders = agentFolders; | ||||
| 		this.AgentState = new AgentState(); | ||||
| 		this.TaskManager = new TaskManager(PhantomLogger.Create<TaskManager, AgentServices>()); | ||||
| 		this.BackupManager = new BackupManager(agentFolders, serviceConfiguration.MaxConcurrentCompressionTasks); | ||||
| 		 | ||||
| 		this.JavaRuntimeRepository = new JavaRuntimeRepository(); | ||||
| 		this.InstanceTicketManager = new InstanceTicketManager(agentInfo, controllerConnection); | ||||
| 		 | ||||
| 		var instanceManagerInit = new InstanceManagerActor.Init(controllerConnection, agentFolders, AgentState, JavaRuntimeRepository, InstanceTicketManager, TaskManager, BackupManager); | ||||
| 		var instanceManagerInit = new InstanceManagerActor.Init(controllerConnection, agentFolders, AgentState, JavaRuntimeRepository, InstanceTicketManager, BackupManager); | ||||
| 		this.InstanceManager = ActorSystem.ActorOf(InstanceManagerActor.Factory(instanceManagerInit), "InstanceManager"); | ||||
| 	} | ||||
|  | ||||
| @@ -50,7 +47,6 @@ public sealed class AgentServices { | ||||
| 		Logger.Information("Stopping services..."); | ||||
| 		 | ||||
| 		await InstanceManager.Stop(new InstanceManagerActor.ShutdownCommand()); | ||||
| 		await TaskManager.Stop(); | ||||
| 		 | ||||
| 		BackupManager.Dispose(); | ||||
| 		 | ||||
|   | ||||
| @@ -25,7 +25,7 @@ sealed class BackupArchiver { | ||||
| 	} | ||||
|  | ||||
| 	private bool IsFolderSkipped(ImmutableList<string> relativePath) { | ||||
| 		return relativePath is ["cache" or "crash-reports" or "debug" or "libraries" or "logs" or "mods" or "versions"]; | ||||
| 		return relativePath is ["cache" or "crash-reports" or "debug" or "libraries" or "logs" or "mods" or "servermods" or "versions"]; | ||||
| 	} | ||||
| 	 | ||||
| 	[SuppressMessage("ReSharper", "ConvertIfStatementToReturnStatement")] | ||||
|   | ||||
| @@ -67,6 +67,10 @@ sealed class BackupManager : IDisposable { | ||||
| 				resultBuilder.Kind = BackupCreationResultKind.BackupCancelled; | ||||
| 				logger.Warning("Backup creation was cancelled."); | ||||
| 				return null; | ||||
| 			} catch (TimeoutException) { | ||||
| 				resultBuilder.Kind = BackupCreationResultKind.BackupTimedOut; | ||||
| 				logger.Warning("Backup creation timed out."); | ||||
| 				return null; | ||||
| 			} catch (Exception e) { | ||||
| 				resultBuilder.Kind = BackupCreationResultKind.UnknownError; | ||||
| 				logger.Error(e, "Caught exception while creating an instance backup."); | ||||
| @@ -76,6 +80,9 @@ sealed class BackupManager : IDisposable { | ||||
| 					await dispatcher.EnableAutomaticSaving(); | ||||
| 				} catch (OperationCanceledException) { | ||||
| 					// Ignore. | ||||
| 				} catch (TimeoutException) { | ||||
| 					resultBuilder.Warnings |= BackupCreationWarnings.CouldNotRestoreAutomaticSaving; | ||||
| 					logger.Warning("Timed out waiting for automatic saving to be re-enabled."); | ||||
| 				} catch (Exception e) { | ||||
| 					resultBuilder.Warnings |= BackupCreationWarnings.CouldNotRestoreAutomaticSaving; | ||||
| 					logger.Error(e, "Caught exception while enabling automatic saving after creating an instance backup."); | ||||
| @@ -120,6 +127,7 @@ sealed class BackupManager : IDisposable { | ||||
| 				BackupCreationResultKind.Success                            => "Backup created successfully.", | ||||
| 				BackupCreationResultKind.InstanceNotRunning                 => "Instance is not running.", | ||||
| 				BackupCreationResultKind.BackupCancelled                    => "Backup cancelled.", | ||||
| 				BackupCreationResultKind.BackupTimedOut                     => "Backup timed out.", | ||||
| 				BackupCreationResultKind.BackupAlreadyRunning               => "A backup is already being created.", | ||||
| 				BackupCreationResultKind.BackupFileAlreadyExists            => "Backup with the same name already exists.", | ||||
| 				BackupCreationResultKind.CouldNotCreateBackupFolder         => "Could not create backup folder.", | ||||
|   | ||||
| @@ -1,10 +1,8 @@ | ||||
| using Phantom.Agent.Minecraft.Instance; | ||||
| using Phantom.Agent.Minecraft.Server; | ||||
| using Phantom.Agent.Services.Instances; | ||||
| using Phantom.Agent.Services.Instances; | ||||
| using Phantom.Agent.Services.Instances.State; | ||||
| using Phantom.Common.Data.Backups; | ||||
| using Phantom.Utils.Logging; | ||||
| using Phantom.Utils.Tasks; | ||||
| using Phantom.Utils.Threading; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Backups; | ||||
|  | ||||
| @@ -16,27 +14,23 @@ sealed class BackupScheduler : CancellableBackgroundTask { | ||||
|  | ||||
| 	private readonly BackupManager backupManager; | ||||
| 	private readonly InstanceContext context; | ||||
| 	private readonly InstanceProcess process; | ||||
| 	private readonly SemaphoreSlim backupSemaphore = new (1, 1); | ||||
| 	private readonly int serverPort; | ||||
| 	private readonly ServerStatusProtocol serverStatusProtocol; | ||||
| 	private readonly ManualResetEventSlim serverOutputWhileWaitingForOnlinePlayers = new (); | ||||
| 	private readonly InstancePlayerCountTracker playerCountTracker; | ||||
| 	 | ||||
| 	public event EventHandler<BackupCreationResult>? BackupCompleted; | ||||
|  | ||||
| 	public BackupScheduler(InstanceContext context, InstanceProcess process, int serverPort) : base(PhantomLogger.Create<BackupScheduler>(context.ShortName), context.Services.TaskManager, "Backup scheduler for " + context.ShortName) { | ||||
| 	public BackupScheduler(InstanceContext context, InstancePlayerCountTracker playerCountTracker) : base(PhantomLogger.Create<BackupScheduler>(context.ShortName)) { | ||||
| 		this.backupManager = context.Services.BackupManager; | ||||
| 		this.context = context; | ||||
| 		this.process = process; | ||||
| 		this.serverPort = serverPort; | ||||
| 		this.serverStatusProtocol = new ServerStatusProtocol(context.ShortName); | ||||
| 		this.playerCountTracker = playerCountTracker; | ||||
| 		Start(); | ||||
| 	} | ||||
|  | ||||
| 	protected override async Task RunTask() { | ||||
| 		await Task.Delay(InitialDelay, CancellationToken); | ||||
| 		Logger.Information("Starting a new backup after server launched."); | ||||
| 			 | ||||
| 		 | ||||
| 		while (!CancellationToken.IsCancellationRequested) { | ||||
| 			var result = await CreateBackup(); | ||||
| 			BackupCompleted?.Invoke(this, result); | ||||
| @@ -69,43 +63,18 @@ sealed class BackupScheduler : CancellableBackgroundTask { | ||||
| 	} | ||||
|  | ||||
| 	private async Task WaitForOnlinePlayers() { | ||||
| 		bool needsToLogOfflinePlayersMessage = true; | ||||
| 		 | ||||
| 		process.AddOutputListener(ServerOutputListener, maxLinesToReadFromHistory: 0); | ||||
| 		try { | ||||
| 			while (!CancellationToken.IsCancellationRequested) { | ||||
| 				serverOutputWhileWaitingForOnlinePlayers.Reset(); | ||||
| 				 | ||||
| 				var onlinePlayerCount = await serverStatusProtocol.GetOnlinePlayerCount(serverPort, CancellationToken); | ||||
| 				if (onlinePlayerCount == null) { | ||||
| 					Logger.Warning("Could not detect whether any players are online, starting a new backup."); | ||||
| 					break; | ||||
| 				} | ||||
|  | ||||
| 				if (onlinePlayerCount > 0) { | ||||
| 					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); | ||||
| 		var task = playerCountTracker.WaitForOnlinePlayers(CancellationToken); | ||||
| 		if (!task.IsCompleted) { | ||||
| 			Logger.Information("Waiting for someone to join before starting a new backup."); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private void ServerOutputListener(object? sender, string line) { | ||||
| 		if (!serverOutputWhileWaitingForOnlinePlayers.IsSet) { | ||||
| 			serverOutputWhileWaitingForOnlinePlayers.Set(); | ||||
| 			Logger.Debug("Detected server output, signalling to check for online players again."); | ||||
| 		 | ||||
| 		try { | ||||
| 			await task; | ||||
| 			Logger.Information("Players are online, starting a new backup."); | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			throw; | ||||
| 		} catch (Exception) { | ||||
| 			Logger.Warning("Could not detect whether any players are online, starting a new backup."); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using System.Text.RegularExpressions; | ||||
| using System.Collections.Immutable; | ||||
| using System.Text.RegularExpressions; | ||||
| using Phantom.Agent.Minecraft.Command; | ||||
| using Phantom.Agent.Minecraft.Instance; | ||||
| using Phantom.Utils.Tasks; | ||||
| @@ -7,8 +8,26 @@ using Serilog; | ||||
| namespace Phantom.Agent.Services.Backups; | ||||
|  | ||||
| sealed partial class BackupServerCommandDispatcher : IDisposable { | ||||
| 	[GeneratedRegex(@"^\[(?:.*?)\] \[Server thread/INFO\]: (.*?)$", RegexOptions.NonBacktracking)] | ||||
| 	[GeneratedRegex(@"^(?:(?:\[.*?\] \[Server thread/INFO\].*?:)|(?:[\d-]+? [\d:]+? \[INFO\])) (.*?)$", RegexOptions.NonBacktracking)] | ||||
| 	private static partial Regex ServerThreadInfoRegex(); | ||||
| 	 | ||||
| 	private static readonly ImmutableHashSet<string> AutomaticSavingDisabledMessages = ImmutableHashSet.Create( | ||||
| 		"Automatic saving is now disabled", | ||||
| 		"Turned off world auto-saving", | ||||
| 		"CONSOLE: Disabling level saving.." | ||||
| 	); | ||||
| 	 | ||||
| 	private static readonly ImmutableHashSet<string> SavedTheGameMessages = ImmutableHashSet.Create( | ||||
| 		"Saved the game", | ||||
| 		"Saved the world", | ||||
| 		"CONSOLE: Save complete." | ||||
| 	); | ||||
| 	 | ||||
| 	private static readonly ImmutableHashSet<string> AutomaticSavingEnabledMessages = ImmutableHashSet.Create( | ||||
| 		"Automatic saving is now enabled", | ||||
| 		"Turned on world auto-saving", | ||||
| 		"CONSOLE: Enabling level saving.." | ||||
| 	); | ||||
|  | ||||
| 	private readonly ILogger logger; | ||||
| 	private readonly InstanceProcess process; | ||||
| @@ -32,18 +51,17 @@ sealed partial class BackupServerCommandDispatcher : IDisposable { | ||||
|  | ||||
| 	public async Task DisableAutomaticSaving() { | ||||
| 		await process.SendCommand(MinecraftCommand.SaveOff, cancellationToken); | ||||
| 		await automaticSavingDisabled.Task.WaitAsync(cancellationToken); | ||||
| 		await automaticSavingDisabled.Task.WaitAsync(TimeSpan.FromSeconds(30), cancellationToken); | ||||
| 	} | ||||
|  | ||||
| 	public async Task SaveAllChunks() { | ||||
| 		// TODO Try if not flushing and waiting a few seconds before flushing reduces lag. | ||||
| 		await process.SendCommand(MinecraftCommand.SaveAll(flush: true), cancellationToken); | ||||
| 		await savedTheGame.Task.WaitAsync(cancellationToken); | ||||
| 		await savedTheGame.Task.WaitAsync(TimeSpan.FromMinutes(1), cancellationToken); | ||||
| 	} | ||||
|  | ||||
| 	public async Task EnableAutomaticSaving() { | ||||
| 		await process.SendCommand(MinecraftCommand.SaveOn, cancellationToken); | ||||
| 		await automaticSavingEnabled.Task.WaitAsync(cancellationToken); | ||||
| 		await automaticSavingEnabled.Task.WaitAsync(TimeSpan.FromMinutes(1), cancellationToken); | ||||
| 	} | ||||
|  | ||||
| 	private void OnOutput(object? sender, string? line) { | ||||
| @@ -59,19 +77,19 @@ sealed partial class BackupServerCommandDispatcher : IDisposable { | ||||
| 		string info = match.Groups[1].Value; | ||||
|  | ||||
| 		if (!automaticSavingDisabled.Task.IsCompleted) { | ||||
| 			if (info == "Automatic saving is now disabled") { | ||||
| 			if (AutomaticSavingDisabledMessages.Contains(info)) { | ||||
| 				logger.Debug("Detected that automatic saving is disabled."); | ||||
| 				automaticSavingDisabled.SetResult(); | ||||
| 			} | ||||
| 		} | ||||
| 		else if (!savedTheGame.Task.IsCompleted) { | ||||
| 			if (info == "Saved the game") { | ||||
| 			if (SavedTheGameMessages.Contains(info)) { | ||||
| 				logger.Debug("Detected that the game is saved."); | ||||
| 				savedTheGame.SetResult(); | ||||
| 			} | ||||
| 		} | ||||
| 		else if (!automaticSavingEnabled.Task.IsCompleted) { | ||||
| 			if (info == "Automatic saving is now enabled") { | ||||
| 			if (AutomaticSavingEnabledMessages.Contains(info)) { | ||||
| 				logger.Debug("Detected that automatic saving is enabled."); | ||||
| 				automaticSavingEnabled.SetResult(); | ||||
| 			} | ||||
|   | ||||
| @@ -135,7 +135,12 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> { | ||||
| 			return new BackupCreationResult(BackupCreationResultKind.InstanceNotRunning); | ||||
| 		} | ||||
| 		else { | ||||
| 			return await command.BackupManager.CreateBackup(context.ShortName, runningState.Process, shutdownCancellationToken); | ||||
| 			SetAndReportStatus(InstanceStatus.BackingUp); | ||||
| 			try { | ||||
| 				return await command.BackupManager.CreateBackup(context.ShortName, runningState.Process, shutdownCancellationToken); | ||||
| 			} finally { | ||||
| 				SetAndReportStatus(InstanceStatus.Running); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -6,13 +6,13 @@ using Phantom.Agent.Minecraft.Properties; | ||||
| using Phantom.Agent.Minecraft.Server; | ||||
| using Phantom.Agent.Rpc; | ||||
| using Phantom.Agent.Services.Backups; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Common.Data.Minecraft; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Utils.Actor; | ||||
| using Phantom.Utils.IO; | ||||
| using Phantom.Utils.Logging; | ||||
| using Phantom.Utils.Tasks; | ||||
| using Serilog; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances; | ||||
| @@ -20,7 +20,7 @@ namespace Phantom.Agent.Services.Instances; | ||||
| sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand> { | ||||
| 	private static readonly ILogger Logger = PhantomLogger.Create<InstanceManagerActor>(); | ||||
|  | ||||
| 	public readonly record struct Init(ControllerConnection ControllerConnection, AgentFolders AgentFolders, AgentState AgentState, JavaRuntimeRepository JavaRuntimeRepository, InstanceTicketManager InstanceTicketManager, TaskManager TaskManager, BackupManager BackupManager); | ||||
| 	public readonly record struct Init(ControllerConnection ControllerConnection, AgentFolders AgentFolders, AgentState AgentState, JavaRuntimeRepository JavaRuntimeRepository, InstanceTicketManager InstanceTicketManager, BackupManager BackupManager); | ||||
|  | ||||
| 	public static Props<ICommand> Factory(Init init) { | ||||
| 		return Props<ICommand>.Create(() => new InstanceManagerActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume }); | ||||
| @@ -47,12 +47,12 @@ sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand> | ||||
| 		var minecraftServerExecutables = new MinecraftServerExecutables(init.AgentFolders.ServerExecutableFolderPath); | ||||
| 		var launchServices = new LaunchServices(minecraftServerExecutables, init.JavaRuntimeRepository); | ||||
|  | ||||
| 		this.instanceServices = new InstanceServices(init.ControllerConnection, init.TaskManager, init.BackupManager, launchServices); | ||||
| 		this.instanceServices = new InstanceServices(init.ControllerConnection, init.BackupManager, launchServices); | ||||
| 		 | ||||
| 		ReceiveAndReply<ConfigureInstanceCommand, InstanceActionResult<ConfigureInstanceResult>>(ConfigureInstance); | ||||
| 		ReceiveAndReply<LaunchInstanceCommand, InstanceActionResult<LaunchInstanceResult>>(LaunchInstance); | ||||
| 		ReceiveAndReply<StopInstanceCommand, InstanceActionResult<StopInstanceResult>>(StopInstance); | ||||
| 		ReceiveAsyncAndReply<SendCommandToInstanceCommand, InstanceActionResult<SendCommandToInstanceResult>>(SendCommandToInstance); | ||||
| 		ReceiveAndReply<ConfigureInstanceCommand, Result<ConfigureInstanceResult, InstanceActionFailure>>(ConfigureInstance); | ||||
| 		ReceiveAndReply<LaunchInstanceCommand, Result<LaunchInstanceResult, InstanceActionFailure>>(LaunchInstance); | ||||
| 		ReceiveAndReply<StopInstanceCommand, Result<StopInstanceResult, InstanceActionFailure>>(StopInstance); | ||||
| 		ReceiveAsyncAndReply<SendCommandToInstanceCommand, Result<SendCommandToInstanceResult, InstanceActionFailure>>(SendCommandToInstance); | ||||
| 		ReceiveAsync<ShutdownCommand>(Shutdown); | ||||
| 	} | ||||
|  | ||||
| @@ -65,17 +65,17 @@ sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand> | ||||
| 	 | ||||
| 	public interface ICommand {} | ||||
| 	 | ||||
| 	public sealed record ConfigureInstanceCommand(Guid InstanceGuid, InstanceConfiguration Configuration, InstanceLaunchProperties LaunchProperties, bool LaunchNow, bool AlwaysReportStatus) : ICommand, ICanReply<InstanceActionResult<ConfigureInstanceResult>>; | ||||
| 	public sealed record ConfigureInstanceCommand(Guid InstanceGuid, InstanceConfiguration Configuration, InstanceLaunchProperties LaunchProperties, bool LaunchNow, bool AlwaysReportStatus) : ICommand, ICanReply<Result<ConfigureInstanceResult, InstanceActionFailure>>; | ||||
| 	 | ||||
| 	public sealed record LaunchInstanceCommand(Guid InstanceGuid) : ICommand, ICanReply<InstanceActionResult<LaunchInstanceResult>>; | ||||
| 	public sealed record LaunchInstanceCommand(Guid InstanceGuid) : ICommand, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>; | ||||
| 	 | ||||
| 	public sealed record StopInstanceCommand(Guid InstanceGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<InstanceActionResult<StopInstanceResult>>; | ||||
| 	public sealed record StopInstanceCommand(Guid InstanceGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>; | ||||
| 	 | ||||
| 	public sealed record SendCommandToInstanceCommand(Guid InstanceGuid, string Command) : ICommand, ICanReply<InstanceActionResult<SendCommandToInstanceResult>>; | ||||
| 	public sealed record SendCommandToInstanceCommand(Guid InstanceGuid, string Command) : ICommand, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>; | ||||
| 	 | ||||
| 	public sealed record ShutdownCommand : ICommand; | ||||
|  | ||||
| 	private InstanceActionResult<ConfigureInstanceResult> ConfigureInstance(ConfigureInstanceCommand command) { | ||||
| 	private Result<ConfigureInstanceResult, InstanceActionFailure> ConfigureInstance(ConfigureInstanceCommand command) { | ||||
| 		var instanceGuid = command.InstanceGuid; | ||||
| 		var configuration = command.Configuration; | ||||
|  | ||||
| @@ -102,6 +102,7 @@ sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand> | ||||
| 		IServerLauncher launcher = configuration.MinecraftServerKind switch { | ||||
| 			MinecraftServerKind.Vanilla => new VanillaLauncher(properties), | ||||
| 			MinecraftServerKind.Fabric  => new FabricLauncher(properties), | ||||
| 			MinecraftServerKind.Forge   => new ForgeLauncher(properties), | ||||
| 			_                           => InvalidLauncher.Instance | ||||
| 		}; | ||||
|  | ||||
| @@ -130,64 +131,64 @@ sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand> | ||||
| 			LaunchInstance(new LaunchInstanceCommand(instanceGuid)); | ||||
| 		} | ||||
|  | ||||
| 		return InstanceActionResult.Concrete(ConfigureInstanceResult.Success); | ||||
| 		return ConfigureInstanceResult.Success; | ||||
| 	} | ||||
|  | ||||
| 	private InstanceActionResult<LaunchInstanceResult> LaunchInstance(LaunchInstanceCommand command) { | ||||
| 	private Result<LaunchInstanceResult, InstanceActionFailure> LaunchInstance(LaunchInstanceCommand command) { | ||||
| 		var instanceGuid = command.InstanceGuid; | ||||
| 		if (!instances.TryGetValue(instanceGuid, out var instanceInfo)) { | ||||
| 			return InstanceActionResult.General<LaunchInstanceResult>(InstanceActionGeneralResult.InstanceDoesNotExist); | ||||
| 			return InstanceActionFailure.InstanceDoesNotExist; | ||||
| 		} | ||||
| 		 | ||||
| 		var ticket = instanceTicketManager.Reserve(instanceInfo.Configuration); | ||||
| 		if (ticket is Result<InstanceTicketManager.Ticket, LaunchInstanceResult>.Fail fail) { | ||||
| 			return InstanceActionResult.Concrete(fail.Error); | ||||
| 		if (!ticket) { | ||||
| 			return ticket.Error; | ||||
| 		} | ||||
|  | ||||
| 		if (agentState.InstancesByGuid.TryGetValue(instanceGuid, out var instance)) { | ||||
| 			var status = instance.Status; | ||||
| 			if (status.IsRunning()) { | ||||
| 				return InstanceActionResult.Concrete(LaunchInstanceResult.InstanceAlreadyRunning); | ||||
| 				return LaunchInstanceResult.InstanceAlreadyRunning; | ||||
| 			} | ||||
| 			else if (status.IsLaunching()) { | ||||
| 				return InstanceActionResult.Concrete(LaunchInstanceResult.InstanceAlreadyLaunching); | ||||
| 				return LaunchInstanceResult.InstanceAlreadyLaunching; | ||||
| 			} | ||||
| 		} | ||||
| 		 | ||||
| 		instanceInfo.Actor.Tell(new InstanceActor.LaunchInstanceCommand(instanceInfo.Configuration, instanceInfo.Launcher, ticket.Value, IsRestarting: false)); | ||||
| 		return InstanceActionResult.Concrete(LaunchInstanceResult.LaunchInitiated); | ||||
| 		return LaunchInstanceResult.LaunchInitiated; | ||||
| 	} | ||||
|  | ||||
| 	private InstanceActionResult<StopInstanceResult> StopInstance(StopInstanceCommand command) { | ||||
| 	private Result<StopInstanceResult, InstanceActionFailure> StopInstance(StopInstanceCommand command) { | ||||
| 		var instanceGuid = command.InstanceGuid; | ||||
| 		if (!instances.TryGetValue(instanceGuid, out var instanceInfo)) { | ||||
| 			return InstanceActionResult.General<StopInstanceResult>(InstanceActionGeneralResult.InstanceDoesNotExist); | ||||
| 			return InstanceActionFailure.InstanceDoesNotExist; | ||||
| 		} | ||||
|          | ||||
| 		if (agentState.InstancesByGuid.TryGetValue(instanceGuid, out var instance)) { | ||||
| 			var status = instance.Status; | ||||
| 			if (status.IsStopping()) { | ||||
| 				return InstanceActionResult.Concrete(StopInstanceResult.InstanceAlreadyStopping); | ||||
| 				return StopInstanceResult.InstanceAlreadyStopping; | ||||
| 			} | ||||
| 			else if (!status.CanStop()) { | ||||
| 				return InstanceActionResult.Concrete(StopInstanceResult.InstanceAlreadyStopped); | ||||
| 				return StopInstanceResult.InstanceAlreadyStopped; | ||||
| 			} | ||||
| 		} | ||||
| 			 | ||||
| 		instanceInfo.Actor.Tell(new InstanceActor.StopInstanceCommand(command.StopStrategy)); | ||||
| 		return InstanceActionResult.Concrete(StopInstanceResult.StopInitiated); | ||||
| 		return StopInstanceResult.StopInitiated; | ||||
| 	} | ||||
|  | ||||
| 	private async Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommandToInstance(SendCommandToInstanceCommand command) { | ||||
| 	private async Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> SendCommandToInstance(SendCommandToInstanceCommand command) { | ||||
| 		var instanceGuid = command.InstanceGuid; | ||||
| 		if (!instances.TryGetValue(instanceGuid, out var instanceInfo)) { | ||||
| 			return InstanceActionResult.General<SendCommandToInstanceResult>(InstanceActionGeneralResult.InstanceDoesNotExist); | ||||
| 			return InstanceActionFailure.InstanceDoesNotExist; | ||||
| 		} | ||||
|  | ||||
| 		try { | ||||
| 			return InstanceActionResult.Concrete(await instanceInfo.Actor.Request(new InstanceActor.SendCommandToInstanceCommand(command.Command), shutdownCancellationToken)); | ||||
| 			return await instanceInfo.Actor.Request(new InstanceActor.SendCommandToInstanceCommand(command.Command), shutdownCancellationToken); | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			return InstanceActionResult.General<SendCommandToInstanceResult>(InstanceActionGeneralResult.AgentShuttingDown); | ||||
| 			return InstanceActionFailure.AgentShuttingDown; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| using Phantom.Agent.Minecraft.Launcher; | ||||
| using Phantom.Agent.Rpc; | ||||
| using Phantom.Agent.Services.Backups; | ||||
| using Phantom.Utils.Tasks; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances; | ||||
|  | ||||
| sealed record InstanceServices(ControllerConnection ControllerConnection, TaskManager TaskManager, BackupManager BackupManager, LaunchServices LaunchServices); | ||||
| sealed record InstanceServices(ControllerConnection ControllerConnection, BackupManager BackupManager, LaunchServices LaunchServices); | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| using Phantom.Agent.Minecraft.Instance; | ||||
| using Phantom.Agent.Minecraft.Launcher; | ||||
| using Phantom.Agent.Minecraft.Server; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Utils.Tasks; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances.State; | ||||
|  | ||||
|   | ||||
| @@ -22,7 +22,7 @@ sealed class InstanceLogSender : CancellableBackgroundTask { | ||||
| 	 | ||||
| 	private int droppedLinesSinceLastSend; | ||||
|  | ||||
| 	public InstanceLogSender(ControllerConnection controllerConnection, TaskManager taskManager, Guid instanceGuid, string loggerName) : base(PhantomLogger.Create<InstanceLogSender>(loggerName), taskManager, "Instance log sender for " + loggerName) { | ||||
| 	public InstanceLogSender(ControllerConnection controllerConnection, Guid instanceGuid, string loggerName) : base(PhantomLogger.Create<InstanceLogSender>(loggerName)) { | ||||
| 		this.controllerConnection = controllerConnection; | ||||
| 		this.instanceGuid = instanceGuid; | ||||
| 		this.outputChannel = Channel.CreateBounded<string>(BufferOptions, OnLineDropped); | ||||
|   | ||||
| @@ -0,0 +1,140 @@ | ||||
| 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(); | ||||
| 	} | ||||
| } | ||||
| @@ -19,6 +19,7 @@ sealed class InstanceRunningState : IDisposable { | ||||
| 	private readonly CancellationToken cancellationToken; | ||||
|  | ||||
| 	private readonly InstanceLogSender logSender; | ||||
| 	private readonly InstancePlayerCountTracker playerCountTracker; | ||||
| 	private readonly BackupScheduler backupScheduler; | ||||
|  | ||||
| 	private bool isDisposed; | ||||
| @@ -31,9 +32,10 @@ sealed class InstanceRunningState : IDisposable { | ||||
| 		this.Process = process; | ||||
| 		this.cancellationToken = cancellationToken; | ||||
|  | ||||
| 		this.logSender = new InstanceLogSender(context.Services.ControllerConnection, context.Services.TaskManager, context.InstanceGuid, context.ShortName); | ||||
| 		this.logSender = new InstanceLogSender(context.Services.ControllerConnection, context.InstanceGuid, context.ShortName); | ||||
| 		this.playerCountTracker = new InstancePlayerCountTracker(context, process, configuration.ServerPort); | ||||
|  | ||||
| 		this.backupScheduler = new BackupScheduler(context, process, configuration.ServerPort); | ||||
| 		this.backupScheduler = new BackupScheduler(context, playerCountTracker); | ||||
| 		this.backupScheduler.BackupCompleted += OnScheduledBackupCompleted; | ||||
| 	} | ||||
|  | ||||
| @@ -93,6 +95,11 @@ sealed class InstanceRunningState : IDisposable { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public void OnStopInitiated() { | ||||
| 		backupScheduler.Stop(); | ||||
| 		playerCountTracker.Stop(); | ||||
| 	} | ||||
| 	 | ||||
| 	private bool TryDispose() { | ||||
| 		lock (this) { | ||||
| 			if (isDisposed) { | ||||
| @@ -102,8 +109,8 @@ sealed class InstanceRunningState : IDisposable { | ||||
| 			isDisposed = true; | ||||
| 		} | ||||
|  | ||||
| 		OnStopInitiated(); | ||||
| 		logSender.Stop(); | ||||
| 		backupScheduler.Stop(); | ||||
| 		 | ||||
| 		Process.Dispose(); | ||||
| 		 | ||||
|   | ||||
| @@ -25,6 +25,8 @@ static class InstanceStopProcedure { | ||||
|  | ||||
| 		try { | ||||
| 			// Too late to cancel the stop procedure now. | ||||
| 			runningState.OnStopInitiated(); | ||||
| 			 | ||||
| 			if (!process.HasEnded) { | ||||
| 				context.Logger.Information("Session stopping now."); | ||||
| 				await DoStop(context, process); | ||||
| @@ -85,7 +87,7 @@ static class InstanceStopProcedure { | ||||
| 	private static async Task WaitForSessionToEnd(InstanceContext context, InstanceProcess process) { | ||||
| 		try { | ||||
| 			await process.WaitForExit(TimeSpan.FromSeconds(55)); | ||||
| 		} catch (OperationCanceledException) { | ||||
| 		} catch (TimeoutException) { | ||||
| 			try { | ||||
| 				context.Logger.Warning("Waiting timed out, killing session..."); | ||||
| 				process.Kill(); | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using Phantom.Agent.Services.Instances; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Messages.Agent; | ||||
| @@ -32,10 +33,10 @@ public sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToAgent | ||||
| 		 | ||||
| 		ReceiveAsync<RegisterAgentSuccessMessage>(HandleRegisterAgentSuccess); | ||||
| 		Receive<RegisterAgentFailureMessage>(HandleRegisterAgentFailure); | ||||
| 		ReceiveAndReplyLater<ConfigureInstanceMessage, InstanceActionResult<ConfigureInstanceResult>>(HandleConfigureInstance); | ||||
| 		ReceiveAndReplyLater<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(HandleLaunchInstance); | ||||
| 		ReceiveAndReplyLater<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(HandleStopInstance); | ||||
| 		ReceiveAndReplyLater<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(HandleSendCommandToInstance); | ||||
| 		ReceiveAndReplyLater<ConfigureInstanceMessage, Result<ConfigureInstanceResult, InstanceActionFailure>>(HandleConfigureInstance); | ||||
| 		ReceiveAndReplyLater<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(HandleLaunchInstance); | ||||
| 		ReceiveAndReplyLater<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(HandleStopInstance); | ||||
| 		ReceiveAndReplyLater<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(HandleSendCommandToInstance); | ||||
| 		Receive<ReplyMessage>(HandleReply); | ||||
| 	} | ||||
|  | ||||
| @@ -74,23 +75,23 @@ public sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToAgent | ||||
| 		Environment.Exit(1); | ||||
| 	} | ||||
| 	 | ||||
| 	private Task<InstanceActionResult<ConfigureInstanceResult>> HandleConfigureInstance(ConfigureInstanceMessage message, bool alwaysReportStatus) { | ||||
| 	private Task<Result<ConfigureInstanceResult, InstanceActionFailure>> HandleConfigureInstance(ConfigureInstanceMessage message, bool alwaysReportStatus) { | ||||
| 		return agent.InstanceManager.Request(new InstanceManagerActor.ConfigureInstanceCommand(message.InstanceGuid, message.Configuration, message.LaunchProperties, message.LaunchNow, alwaysReportStatus)); | ||||
| 	} | ||||
| 	 | ||||
| 	private async Task<InstanceActionResult<ConfigureInstanceResult>> HandleConfigureInstance(ConfigureInstanceMessage message) { | ||||
| 	private async Task<Result<ConfigureInstanceResult, InstanceActionFailure>> HandleConfigureInstance(ConfigureInstanceMessage message) { | ||||
| 		return await HandleConfigureInstance(message, alwaysReportStatus: false); | ||||
| 	} | ||||
|  | ||||
| 	private async Task<InstanceActionResult<LaunchInstanceResult>> HandleLaunchInstance(LaunchInstanceMessage message) { | ||||
| 	private async Task<Result<LaunchInstanceResult, InstanceActionFailure>> HandleLaunchInstance(LaunchInstanceMessage message) { | ||||
| 		return await agent.InstanceManager.Request(new InstanceManagerActor.LaunchInstanceCommand(message.InstanceGuid)); | ||||
| 	} | ||||
|  | ||||
| 	private async Task<InstanceActionResult<StopInstanceResult>> HandleStopInstance(StopInstanceMessage message) { | ||||
| 	private async Task<Result<StopInstanceResult, InstanceActionFailure>> HandleStopInstance(StopInstanceMessage message) { | ||||
| 		return await agent.InstanceManager.Request(new InstanceManagerActor.StopInstanceCommand(message.InstanceGuid, message.StopStrategy)); | ||||
| 	} | ||||
|  | ||||
| 	private async Task<InstanceActionResult<SendCommandToInstanceResult>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) { | ||||
| 	private async Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) { | ||||
| 		return await agent.InstanceManager.Request(new InstanceManagerActor.SendCommandToInstanceCommand(message.InstanceGuid, message.Command)); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -7,7 +7,7 @@ public enum EventLogEventType { | ||||
| 	InstanceStopped, | ||||
| 	InstanceBackupSucceeded, | ||||
| 	InstanceBackupSucceededWithWarnings, | ||||
| 	InstanceBackupFailed, | ||||
| 	InstanceBackupFailed | ||||
| } | ||||
|  | ||||
| public static class EventLogEventTypeExtensions { | ||||
| @@ -18,7 +18,7 @@ public static class EventLogEventTypeExtensions { | ||||
| 		{ EventLogEventType.InstanceStopped, EventLogSubjectType.Instance }, | ||||
| 		{ EventLogEventType.InstanceBackupSucceeded, EventLogSubjectType.Instance }, | ||||
| 		{ EventLogEventType.InstanceBackupSucceededWithWarnings, EventLogSubjectType.Instance }, | ||||
| 		{ EventLogEventType.InstanceBackupFailed, EventLogSubjectType.Instance }, | ||||
| 		{ EventLogEventType.InstanceBackupFailed, EventLogSubjectType.Instance } | ||||
| 	}; | ||||
|  | ||||
| 	static EventLogEventTypeExtensions() { | ||||
|   | ||||
| @@ -8,9 +8,10 @@ public sealed partial record Instance( | ||||
| 	[property: MemoryPackOrder(0)] Guid InstanceGuid, | ||||
| 	[property: MemoryPackOrder(1)] InstanceConfiguration Configuration, | ||||
| 	[property: MemoryPackOrder(2)] IInstanceStatus Status, | ||||
| 	[property: MemoryPackOrder(3)] bool LaunchAutomatically | ||||
| 	[property: MemoryPackOrder(3)] InstancePlayerCounts? PlayerCounts, | ||||
| 	[property: MemoryPackOrder(4)] bool LaunchAutomatically | ||||
| ) { | ||||
| 	public static Instance Offline(Guid instanceGuid, InstanceConfiguration configuration, bool launchAutomatically = false) { | ||||
| 		return new Instance(instanceGuid, configuration, InstanceStatus.Offline, launchAutomatically); | ||||
| 		return new Instance(instanceGuid, configuration, InstanceStatus.Offline, PlayerCounts: null, launchAutomatically); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,24 @@ | ||||
| using System.Collections.Immutable; | ||||
| using MemoryPack; | ||||
|  | ||||
| namespace Phantom.Common.Data.Web.Users; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record AuthenticatedUserInfo( | ||||
| 	[property: MemoryPackOrder(0)] Guid Guid, | ||||
| 	[property: MemoryPackOrder(1)] string Name, | ||||
| 	[property: MemoryPackOrder(2)] PermissionSet Permissions, | ||||
| 	[property: MemoryPackOrder(3)] ImmutableHashSet<Guid> ManagedAgentGuids | ||||
| ) { | ||||
| 	public bool CheckPermission(Permission permission) { | ||||
| 		return Permissions.Check(permission); | ||||
| 	} | ||||
|  | ||||
| 	public bool HasAccessToAgent(Guid agentGuid) { | ||||
| 		return ManagedAgentGuids.Contains(agentGuid) || Permissions.Check(Permission.ManageAllAgents); | ||||
| 	} | ||||
|  | ||||
| 	public ImmutableHashSet<Guid> FilterAccessibleAgentGuids(ImmutableHashSet<Guid> agentGuids) { | ||||
| 		return Permissions.Check(Permission.ManageAllAgents) ? agentGuids : agentGuids.Intersect(ManagedAgentGuids); | ||||
| 	} | ||||
| } | ||||
| @@ -4,8 +4,7 @@ using MemoryPack; | ||||
| namespace Phantom.Common.Data.Web.Users; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record LogInSuccess ( | ||||
| 	[property: MemoryPackOrder(0)] Guid UserGuid, | ||||
| 	[property: MemoryPackOrder(1)] PermissionSet Permissions, | ||||
| 	[property: MemoryPackOrder(2)] ImmutableArray<byte> Token | ||||
| public sealed partial record LogInSuccess( | ||||
| 	[property: MemoryPackOrder(0)] AuthenticatedUserInfo UserInfo, | ||||
| 	[property: MemoryPackOrder(1)] ImmutableArray<byte> AuthToken | ||||
| ); | ||||
|   | ||||
| @@ -14,6 +14,9 @@ public sealed record Permission(string Id, Permission? Parent) { | ||||
| 		return Register(id, this); | ||||
| 	} | ||||
|  | ||||
| 	public const string ManageAllAgentsPolicy = "Agents.ManageAll"; | ||||
| 	public static readonly Permission ManageAllAgents = Register(ManageAllAgentsPolicy); | ||||
| 	 | ||||
| 	public const string ViewInstancesPolicy = "Instances.View"; | ||||
| 	public static readonly Permission ViewInstances = Register(ViewInstancesPolicy); | ||||
| 	 | ||||
|   | ||||
| @@ -0,0 +1,5 @@ | ||||
| namespace Phantom.Common.Data.Web.Users; | ||||
|  | ||||
| public enum UserActionFailure { | ||||
| 	NotAuthorized | ||||
| } | ||||
| @@ -0,0 +1,25 @@ | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data.Replies; | ||||
|  | ||||
| namespace Phantom.Common.Data.Web.Users; | ||||
|  | ||||
| [MemoryPackable] | ||||
| [MemoryPackUnion(0, typeof(OfUserActionFailure))] | ||||
| [MemoryPackUnion(1, typeof(OfInstanceActionFailure))] | ||||
| public abstract partial record UserInstanceActionFailure { | ||||
| 	internal UserInstanceActionFailure() {} | ||||
| 	 | ||||
| 	public static implicit operator UserInstanceActionFailure(UserActionFailure failure) { | ||||
| 		return new OfUserActionFailure(failure); | ||||
| 	} | ||||
| 	 | ||||
| 	public static implicit operator UserInstanceActionFailure(InstanceActionFailure failure) { | ||||
| 		return new OfInstanceActionFailure(failure); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record OfUserActionFailure([property: MemoryPackOrder(0)] UserActionFailure Failure) : UserInstanceActionFailure; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record OfInstanceActionFailure([property: MemoryPackOrder(0)] InstanceActionFailure Failure) : UserInstanceActionFailure; | ||||
| @@ -14,9 +14,7 @@ public sealed partial class AuthToken { | ||||
| 	private readonly byte[] bytes; | ||||
|  | ||||
| 	internal AuthToken(byte[]? bytes) { | ||||
| 		if (bytes == null) { | ||||
| 			throw new ArgumentNullException(nameof(bytes)); | ||||
| 		} | ||||
| 		ArgumentNullException.ThrowIfNull(bytes); | ||||
|  | ||||
| 		if (bytes.Length != Length) { | ||||
| 			throw new ArgumentOutOfRangeException(nameof(bytes), "Invalid token length: " + bytes.Length + ". Token length must be exactly " + Length + " bytes."); | ||||
|   | ||||
| @@ -1,15 +1,16 @@ | ||||
| namespace Phantom.Common.Data.Backups; | ||||
|  | ||||
| public enum BackupCreationResultKind : byte { | ||||
| 	UnknownError, | ||||
| 	Success, | ||||
| 	InstanceNotRunning, | ||||
| 	BackupCancelled, | ||||
| 	BackupAlreadyRunning, | ||||
| 	BackupFileAlreadyExists, | ||||
| 	CouldNotCreateBackupFolder, | ||||
| 	CouldNotCopyWorldToTemporaryFolder, | ||||
| 	CouldNotCreateWorldArchive | ||||
| 	UnknownError = 0, | ||||
| 	Success = 1, | ||||
| 	InstanceNotRunning = 2, | ||||
| 	BackupTimedOut = 3, | ||||
| 	BackupCancelled = 4, | ||||
| 	BackupAlreadyRunning = 5, | ||||
| 	BackupFileAlreadyExists = 6, | ||||
| 	CouldNotCreateBackupFolder = 7, | ||||
| 	CouldNotCopyWorldToTemporaryFolder = 8, | ||||
| 	CouldNotCreateWorldArchive = 9 | ||||
| } | ||||
|  | ||||
| public static class BackupCreationResultSummaryExtensions { | ||||
|   | ||||
| @@ -9,9 +9,10 @@ namespace Phantom.Common.Data.Instance; | ||||
| [MemoryPackUnion(3, typeof(InstanceIsDownloading))] | ||||
| [MemoryPackUnion(4, typeof(InstanceIsLaunching))] | ||||
| [MemoryPackUnion(5, typeof(InstanceIsRunning))] | ||||
| [MemoryPackUnion(6, typeof(InstanceIsRestarting))] | ||||
| [MemoryPackUnion(7, typeof(InstanceIsStopping))] | ||||
| [MemoryPackUnion(8, typeof(InstanceIsFailed))] | ||||
| [MemoryPackUnion(6, typeof(InstanceIsBackingUp))] | ||||
| [MemoryPackUnion(7, typeof(InstanceIsRestarting))] | ||||
| [MemoryPackUnion(8, typeof(InstanceIsStopping))] | ||||
| [MemoryPackUnion(9, typeof(InstanceIsFailed))] | ||||
| public partial interface IInstanceStatus {} | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| @@ -32,6 +33,9 @@ public sealed partial record InstanceIsLaunching : IInstanceStatus; | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record InstanceIsRunning : IInstanceStatus; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record InstanceIsBackingUp : IInstanceStatus; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record InstanceIsRestarting : IInstanceStatus; | ||||
|  | ||||
| @@ -46,6 +50,7 @@ public static class InstanceStatus { | ||||
| 	public static readonly IInstanceStatus NotRunning = new InstanceIsNotRunning(); | ||||
| 	public static readonly IInstanceStatus Launching = new InstanceIsLaunching(); | ||||
| 	public static readonly IInstanceStatus Running = new InstanceIsRunning(); | ||||
| 	public static readonly IInstanceStatus BackingUp = new InstanceIsBackingUp(); | ||||
| 	public static readonly IInstanceStatus Restarting = new InstanceIsRestarting(); | ||||
| 	public static readonly IInstanceStatus Stopping = new InstanceIsStopping(); | ||||
| 	 | ||||
| @@ -58,7 +63,7 @@ public static class InstanceStatus { | ||||
| 	} | ||||
|  | ||||
| 	public static bool IsRunning(this IInstanceStatus status) { | ||||
| 		return status is InstanceIsRunning; | ||||
| 		return status is InstanceIsRunning or InstanceIsBackingUp; | ||||
| 	} | ||||
| 	 | ||||
| 	public static bool IsStopping(this IInstanceStatus status) { | ||||
| @@ -70,10 +75,10 @@ public static class InstanceStatus { | ||||
| 	} | ||||
|  | ||||
| 	public static bool CanStop(this IInstanceStatus status) { | ||||
| 		return status is InstanceIsDownloading or InstanceIsLaunching or InstanceIsRunning; | ||||
| 		return status.IsRunning() || status.IsLaunching(); | ||||
| 	} | ||||
|  | ||||
| 	public static bool CanSendCommand(this IInstanceStatus status) { | ||||
| 		return status is InstanceIsRunning; | ||||
| 		return status.IsRunning(); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,9 @@ | ||||
| using MemoryPack; | ||||
|  | ||||
| namespace Phantom.Common.Data.Instance; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public readonly partial record struct InstancePlayerCounts( | ||||
| 	[property: MemoryPackOrder(0)] int Online, | ||||
| 	[property: MemoryPackOrder(1)] int Maximum | ||||
| ); | ||||
| @@ -2,5 +2,6 @@ | ||||
|  | ||||
| public enum MinecraftServerKind : ushort { | ||||
| 	Vanilla = 1, | ||||
| 	Fabric = 2 | ||||
| 	Fabric = 2, | ||||
| 	Forge = 3 | ||||
| } | ||||
|   | ||||
							
								
								
									
										10
									
								
								Common/Phantom.Common.Data/Optional.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								Common/Phantom.Common.Data/Optional.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| 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); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										20
									
								
								Common/Phantom.Common.Data/Replies/InstanceActionFailure.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								Common/Phantom.Common.Data/Replies/InstanceActionFailure.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| namespace Phantom.Common.Data.Replies; | ||||
|  | ||||
| public enum InstanceActionFailure : byte { | ||||
| 	AgentDoesNotExist, | ||||
| 	AgentShuttingDown, | ||||
| 	AgentIsNotResponding, | ||||
| 	InstanceDoesNotExist | ||||
| } | ||||
|  | ||||
| public static class InstanceActionFailureExtensions { | ||||
| 	public static string ToSentence(this InstanceActionFailure failure) { | ||||
| 		return failure switch { | ||||
| 			InstanceActionFailure.AgentDoesNotExist    => "Agent does not exist.", | ||||
| 			InstanceActionFailure.AgentShuttingDown    => "Agent is shutting down.", | ||||
| 			InstanceActionFailure.AgentIsNotResponding => "Agent is not responding.", | ||||
| 			InstanceActionFailure.InstanceDoesNotExist => "Instance does not exist.", | ||||
| 			_                                          => "Unknown error." | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
| @@ -1,9 +0,0 @@ | ||||
| namespace Phantom.Common.Data.Replies; | ||||
|  | ||||
| public enum InstanceActionGeneralResult : byte { | ||||
| 	None, | ||||
| 	AgentDoesNotExist, | ||||
| 	AgentShuttingDown, | ||||
| 	AgentIsNotResponding, | ||||
| 	InstanceDoesNotExist | ||||
| } | ||||
| @@ -1,42 +0,0 @@ | ||||
| using MemoryPack; | ||||
|  | ||||
| namespace Phantom.Common.Data.Replies; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record InstanceActionResult<T>( | ||||
| 	[property: MemoryPackOrder(0)] InstanceActionGeneralResult GeneralResult, | ||||
| 	[property: MemoryPackOrder(1)] T? ConcreteResult | ||||
| ) { | ||||
| 	public bool Is(T? concreteResult) { | ||||
| 		return GeneralResult == InstanceActionGeneralResult.None && EqualityComparer<T>.Default.Equals(ConcreteResult, concreteResult); | ||||
| 	} | ||||
|  | ||||
| 	public InstanceActionResult<T2> Map<T2>(Func<T, T2> mapper) { | ||||
| 		return new InstanceActionResult<T2>(GeneralResult, ConcreteResult is not null ? mapper(ConcreteResult) : default); | ||||
| 	} | ||||
|  | ||||
| 	public string ToSentence(Func<T, string> concreteResultToSentence) { | ||||
| 		return GeneralResult switch { | ||||
| 			InstanceActionGeneralResult.None                 => concreteResultToSentence(ConcreteResult!), | ||||
| 			InstanceActionGeneralResult.AgentDoesNotExist    => "Agent does not exist.", | ||||
| 			InstanceActionGeneralResult.AgentShuttingDown    => "Agent is shutting down.", | ||||
| 			InstanceActionGeneralResult.AgentIsNotResponding => "Agent is not responding.", | ||||
| 			InstanceActionGeneralResult.InstanceDoesNotExist => "Instance does not exist.", | ||||
| 			_                                                => "Unknown result." | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| public static class InstanceActionResult { | ||||
| 	public static InstanceActionResult<T> General<T>(InstanceActionGeneralResult generalResult) { | ||||
| 		return new InstanceActionResult<T>(generalResult, default); | ||||
| 	} | ||||
|  | ||||
| 	public static InstanceActionResult<T> Concrete<T>(T? concreteResult) { | ||||
| 		return new InstanceActionResult<T>(InstanceActionGeneralResult.None, concreteResult); | ||||
| 	} | ||||
|  | ||||
| 	public static InstanceActionResult<T> DidNotReplyIfNull<T>(this InstanceActionResult<T>? result) { | ||||
| 		return result ?? General<T>(InstanceActionGeneralResult.AgentIsNotResponding); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										108
									
								
								Common/Phantom.Common.Data/Result.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								Common/Phantom.Common.Data/Result.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using MemoryPack; | ||||
| using Phantom.Utils.Result; | ||||
|  | ||||
| namespace Phantom.Common.Data; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial class Result<TValue, TError> { | ||||
| 	[MemoryPackOrder(0)] | ||||
| 	[MemoryPackInclude] | ||||
| 	private readonly bool hasValue; | ||||
| 	 | ||||
| 	[MemoryPackOrder(1)] | ||||
| 	[MemoryPackInclude] | ||||
| 	private readonly TValue? value; | ||||
| 	 | ||||
| 	[MemoryPackOrder(2)] | ||||
| 	[MemoryPackInclude] | ||||
| 	private readonly TError? error; | ||||
|  | ||||
| 	[MemoryPackIgnore] | ||||
| 	public TValue Value => hasValue ? value! : throw new InvalidOperationException("Attempted to get value from an error result."); | ||||
| 	 | ||||
| 	[MemoryPackIgnore] | ||||
| 	public TError Error => !hasValue ? error! : throw new InvalidOperationException("Attempted to get error from a success result."); | ||||
| 	 | ||||
| 	private Result(bool hasValue, TValue? value, TError? error) { | ||||
| 		this.hasValue = hasValue; | ||||
| 		this.value = value; | ||||
| 		this.error = error; | ||||
| 	} | ||||
|  | ||||
| 	public bool Is(TValue expectedValue) { | ||||
| 		return hasValue && EqualityComparer<TValue>.Default.Equals(value, expectedValue); | ||||
| 	} | ||||
|  | ||||
| 	public TOutput Into<TOutput>(Func<TValue, TOutput> valueConverter, Func<TError, TOutput> errorConverter) { | ||||
| 		return hasValue ? valueConverter(value!) : errorConverter(error!); | ||||
| 	} | ||||
|  | ||||
| 	public Result<TValue, TNewError> MapError<TNewError>(Func<TError, TNewError> errorConverter) { | ||||
| 		return hasValue ? value! : errorConverter(error!); | ||||
| 	} | ||||
|  | ||||
| 	public Utils.Result.Result Variant() { | ||||
| 		return hasValue ? new Ok<TValue>(Value) : new Err<TError>(Error); | ||||
| 	} | ||||
|  | ||||
| 	public static implicit operator Result<TValue, TError>(TValue value) { | ||||
| 		return new Result<TValue, TError>(hasValue: true, value, default); | ||||
| 	} | ||||
| 	 | ||||
| 	public static implicit operator Result<TValue, TError>(TError error) { | ||||
| 		return new Result<TValue, TError>(hasValue: false, default, error); | ||||
| 	} | ||||
|  | ||||
| 	public static implicit operator bool(Result<TValue, TError> result) { | ||||
| 		return result.hasValue; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial class Result<TError> { | ||||
| 	[MemoryPackOrder(0)] | ||||
| 	[MemoryPackInclude] | ||||
| 	private readonly bool hasValue; | ||||
| 	 | ||||
| 	[MemoryPackOrder(1)] | ||||
| 	[MemoryPackInclude] | ||||
| 	private readonly TError? error; | ||||
|  | ||||
| 	[MemoryPackIgnore] | ||||
| 	public TError Error => !hasValue ? error! : throw new InvalidOperationException("Attempted to get error from a success result."); | ||||
|  | ||||
| 	private Result(bool hasValue, TError? error) { | ||||
| 		this.hasValue = hasValue; | ||||
| 		this.error = error; | ||||
| 	} | ||||
|  | ||||
| 	public bool TryGetError([MaybeNullWhen(false)] out TError error) { | ||||
| 		if (hasValue) { | ||||
| 			error = default; | ||||
| 			return false; | ||||
| 		} | ||||
| 		else { | ||||
| 			error = this.error!; | ||||
| 			return true; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public static implicit operator Result<TError>([SuppressMessage("ReSharper", "UnusedParameter.Global")] Result.OkType _) { | ||||
| 		return new Result<TError>(hasValue: true, default); | ||||
| 	} | ||||
|  | ||||
| 	public static implicit operator Result<TError>(TError error) { | ||||
| 		return new Result<TError>(hasValue: false, error); | ||||
| 	} | ||||
|  | ||||
| 	public static implicit operator bool(Result<TError> result) { | ||||
| 		return result.hasValue; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| public static class Result { | ||||
| 	public static OkType Ok { get; }  = new (); | ||||
|  | ||||
| 	public readonly record struct OkType; | ||||
| } | ||||
| @@ -1,4 +1,5 @@ | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Messages.Agent.BiDirectional; | ||||
| using Phantom.Common.Messages.Agent.ToAgent; | ||||
| using Phantom.Common.Messages.Agent.ToController; | ||||
| @@ -16,10 +17,10 @@ public static class AgentMessageRegistries { | ||||
| 	static AgentMessageRegistries() { | ||||
| 		ToAgent.Add<RegisterAgentSuccessMessage>(0); | ||||
| 		ToAgent.Add<RegisterAgentFailureMessage>(1); | ||||
| 		ToAgent.Add<ConfigureInstanceMessage, InstanceActionResult<ConfigureInstanceResult>>(2); | ||||
| 		ToAgent.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(3); | ||||
| 		ToAgent.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(4); | ||||
| 		ToAgent.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(5); | ||||
| 		ToAgent.Add<ConfigureInstanceMessage, Result<ConfigureInstanceResult, InstanceActionFailure>>(2); | ||||
| 		ToAgent.Add<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(3); | ||||
| 		ToAgent.Add<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(4); | ||||
| 		ToAgent.Add<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(5); | ||||
| 		ToAgent.Add<ReplyMessage>(127); | ||||
| 		 | ||||
| 		ToController.Add<RegisterAgentMessage>(0); | ||||
| @@ -30,6 +31,7 @@ public static class AgentMessageRegistries { | ||||
| 		ToController.Add<InstanceOutputMessage>(5); | ||||
| 		ToController.Add<ReportAgentStatusMessage>(6); | ||||
| 		ToController.Add<ReportInstanceEventMessage>(7); | ||||
| 		ToController.Add<ReportInstancePlayerCountsMessage>(8); | ||||
| 		ToController.Add<ReplyMessage>(127); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Utils.Actor; | ||||
| @@ -11,4 +12,4 @@ public sealed partial record ConfigureInstanceMessage( | ||||
| 	[property: MemoryPackOrder(1)] InstanceConfiguration Configuration, | ||||
| 	[property: MemoryPackOrder(2)] InstanceLaunchProperties LaunchProperties, | ||||
| 	[property: MemoryPackOrder(3)] bool LaunchNow = false | ||||
| ) : IMessageToAgent, ICanReply<InstanceActionResult<ConfigureInstanceResult>>; | ||||
| ) : IMessageToAgent, ICanReply<Result<ConfigureInstanceResult, InstanceActionFailure>>; | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Utils.Actor; | ||||
|  | ||||
| @@ -7,4 +8,4 @@ namespace Phantom.Common.Messages.Agent.ToAgent; | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record LaunchInstanceMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid InstanceGuid | ||||
| ) : IMessageToAgent, ICanReply<InstanceActionResult<LaunchInstanceResult>>; | ||||
| ) : IMessageToAgent, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>; | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Utils.Actor; | ||||
|  | ||||
| @@ -8,4 +9,4 @@ namespace Phantom.Common.Messages.Agent.ToAgent; | ||||
| public sealed partial record SendCommandToInstanceMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid InstanceGuid, | ||||
| 	[property: MemoryPackOrder(1)] string Command | ||||
| ) : IMessageToAgent, ICanReply<InstanceActionResult<SendCommandToInstanceResult>>; | ||||
| ) : IMessageToAgent, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>; | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Minecraft; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Utils.Actor; | ||||
| @@ -9,4 +10,4 @@ namespace Phantom.Common.Messages.Agent.ToAgent; | ||||
| public sealed partial record StopInstanceMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid InstanceGuid, | ||||
| 	[property: MemoryPackOrder(1)] MinecraftStopStrategy StopStrategy | ||||
| ) : IMessageToAgent, ICanReply<InstanceActionResult<StopInstanceResult>>; | ||||
| ) : IMessageToAgent, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>; | ||||
|   | ||||
| @@ -0,0 +1,10 @@ | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data.Instance; | ||||
|  | ||||
| namespace Phantom.Common.Messages.Agent.ToController; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record ReportInstancePlayerCountsMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid InstanceGuid, | ||||
| 	[property: MemoryPackOrder(1)] InstancePlayerCounts? PlayerCounts | ||||
| ) : IMessageToController; | ||||
| @@ -1,5 +1,6 @@ | ||||
| using System.Collections.Immutable; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Utils.Actor; | ||||
|  | ||||
| @@ -7,8 +8,8 @@ namespace Phantom.Common.Messages.Web.ToController; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record ChangeUserRolesMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid LoggedInUserGuid, | ||||
| 	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, | ||||
| 	[property: MemoryPackOrder(1)] Guid SubjectUserGuid, | ||||
| 	[property: MemoryPackOrder(2)] ImmutableHashSet<Guid> AddToRoleGuids, | ||||
| 	[property: MemoryPackOrder(3)] ImmutableHashSet<Guid> RemoveFromRoleGuids | ||||
| ) : IMessageToController, ICanReply<ChangeUserRolesResult>; | ||||
| ) : IMessageToController, ICanReply<Result<ChangeUserRolesResult, UserActionFailure>>; | ||||
|   | ||||
| @@ -1,14 +1,16 @@ | ||||
| using MemoryPack; | ||||
| using System.Collections.Immutable; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Data.Web.Instance; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Utils.Actor; | ||||
|  | ||||
| namespace Phantom.Common.Messages.Web.ToController;  | ||||
| namespace Phantom.Common.Messages.Web.ToController; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record CreateOrUpdateInstanceMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid LoggedInUserGuid, | ||||
| 	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, | ||||
| 	[property: MemoryPackOrder(1)] Guid InstanceGuid, | ||||
| 	[property: MemoryPackOrder(2)] InstanceConfiguration Configuration | ||||
| ) : IMessageToController, ICanReply<InstanceActionResult<CreateOrUpdateInstanceResult>>; | ||||
| ) : IMessageToController, ICanReply<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>; | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| using MemoryPack; | ||||
| using System.Collections.Immutable; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Utils.Actor; | ||||
|  | ||||
| @@ -6,7 +8,7 @@ namespace Phantom.Common.Messages.Web.ToController; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record CreateUserMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid LoggedInUserGuid, | ||||
| 	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, | ||||
| 	[property: MemoryPackOrder(1)] string Username, | ||||
| 	[property: MemoryPackOrder(2)] string Password | ||||
| ) : IMessageToController, ICanReply<CreateUserResult>; | ||||
| ) : IMessageToController, ICanReply<Result<CreateUserResult, UserActionFailure>>; | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| using MemoryPack; | ||||
| using System.Collections.Immutable; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Utils.Actor; | ||||
|  | ||||
| @@ -6,6 +8,6 @@ namespace Phantom.Common.Messages.Web.ToController; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record DeleteUserMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid LoggedInUserGuid, | ||||
| 	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, | ||||
| 	[property: MemoryPackOrder(1)] Guid SubjectUserGuid | ||||
| ) : IMessageToController, ICanReply<DeleteUserResult>; | ||||
| ) : IMessageToController, ICanReply<Result<DeleteUserResult, UserActionFailure>>; | ||||
|   | ||||
| @@ -1,11 +1,14 @@ | ||||
| using System.Collections.Immutable; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.AuditLog; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Utils.Actor; | ||||
|  | ||||
| namespace Phantom.Common.Messages.Web.ToController; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record GetAuditLogMessage( | ||||
| 	[property: MemoryPackOrder(0)] int Count | ||||
| ) : IMessageToController, ICanReply<ImmutableArray<AuditLogItem>>; | ||||
| 	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, | ||||
| 	[property: MemoryPackOrder(1)] int Count | ||||
| ) : IMessageToController, ICanReply<Result<ImmutableArray<AuditLogItem>, UserActionFailure>>; | ||||
|   | ||||
| @@ -0,0 +1,13 @@ | ||||
| using System.Collections.Immutable; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Utils.Actor; | ||||
|  | ||||
| namespace Phantom.Common.Messages.Web.ToController; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record GetAuthenticatedUser( | ||||
| 	[property: MemoryPackOrder(0)] Guid UserGuid, | ||||
| 	[property: MemoryPackOrder(1)] ImmutableArray<byte> AuthToken | ||||
| ) : IMessageToController, ICanReply<Optional<AuthenticatedUserInfo>>; | ||||
| @@ -1,11 +1,14 @@ | ||||
| using System.Collections.Immutable; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.EventLog; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Utils.Actor; | ||||
|  | ||||
| namespace Phantom.Common.Messages.Web.ToController; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record GetEventLogMessage( | ||||
| 	[property: MemoryPackOrder(0)] int Count | ||||
| ) : IMessageToController, ICanReply<ImmutableArray<EventLogItem>>; | ||||
| 	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, | ||||
| 	[property: MemoryPackOrder(1)] int Count | ||||
| ) : IMessageToController, ICanReply<Result<ImmutableArray<EventLogItem>, UserActionFailure>>; | ||||
|   | ||||
| @@ -1,12 +1,15 @@ | ||||
| using MemoryPack; | ||||
| using System.Collections.Immutable; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Utils.Actor; | ||||
|  | ||||
| namespace Phantom.Common.Messages.Web.ToController;  | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record LaunchInstanceMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid LoggedInUserGuid, | ||||
| 	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, | ||||
| 	[property: MemoryPackOrder(1)] Guid AgentGuid, | ||||
| 	[property: MemoryPackOrder(2)] Guid InstanceGuid | ||||
| ) : IMessageToController, ICanReply<InstanceActionResult<LaunchInstanceResult>>; | ||||
| ) : IMessageToController, ICanReply<Result<LaunchInstanceResult, UserInstanceActionFailure>>; | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Utils.Actor; | ||||
|  | ||||
| @@ -8,4 +9,4 @@ namespace Phantom.Common.Messages.Web.ToController; | ||||
| public sealed partial record LogInMessage( | ||||
| 	[property: MemoryPackOrder(0)] string Username, | ||||
| 	[property: MemoryPackOrder(1)] string Password | ||||
| ) : IMessageToController, ICanReply<LogInSuccess?>; | ||||
| ) : IMessageToController, ICanReply<Optional<LogInSuccess>>; | ||||
|   | ||||
| @@ -0,0 +1,10 @@ | ||||
| using System.Collections.Immutable; | ||||
| using MemoryPack; | ||||
|  | ||||
| namespace Phantom.Common.Messages.Web.ToController; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record LogOutMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid UserGuid, | ||||
| 	[property: MemoryPackOrder(1)] ImmutableArray<byte> SessionToken | ||||
| ) : IMessageToController; | ||||
| @@ -1,13 +1,16 @@ | ||||
| using MemoryPack; | ||||
| using System.Collections.Immutable; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Utils.Actor; | ||||
|  | ||||
| namespace Phantom.Common.Messages.Web.ToController;  | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record SendCommandToInstanceMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid LoggedInUserGuid, | ||||
| 	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, | ||||
| 	[property: MemoryPackOrder(1)] Guid AgentGuid, | ||||
| 	[property: MemoryPackOrder(2)] Guid InstanceGuid, | ||||
| 	[property: MemoryPackOrder(3)] string Command | ||||
| ) : IMessageToController, ICanReply<InstanceActionResult<SendCommandToInstanceResult>>; | ||||
| ) : IMessageToController, ICanReply<Result<SendCommandToInstanceResult, UserInstanceActionFailure>>; | ||||
|   | ||||
| @@ -1,14 +1,17 @@ | ||||
| using MemoryPack; | ||||
| using System.Collections.Immutable; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Minecraft; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Utils.Actor; | ||||
|  | ||||
| namespace Phantom.Common.Messages.Web.ToController;  | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record StopInstanceMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid LoggedInUserGuid, | ||||
| 	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, | ||||
| 	[property: MemoryPackOrder(1)] Guid AgentGuid, | ||||
| 	[property: MemoryPackOrder(2)] Guid InstanceGuid, | ||||
| 	[property: MemoryPackOrder(3)] MinecraftStopStrategy StopStrategy | ||||
| ) : IMessageToController, ICanReply<InstanceActionResult<StopInstanceResult>>; | ||||
| ) : IMessageToController, ICanReply<Result<StopInstanceResult, UserInstanceActionFailure>>; | ||||
|   | ||||
| @@ -0,0 +1,8 @@ | ||||
| using MemoryPack; | ||||
|  | ||||
| namespace Phantom.Common.Messages.Web.ToWeb;  | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record RefreshUserSessionMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid UserGuid | ||||
| ) : IMessageToWeb; | ||||
| @@ -1,4 +1,5 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Java; | ||||
| using Phantom.Common.Data.Minecraft; | ||||
| using Phantom.Common.Data.Replies; | ||||
| @@ -23,28 +24,31 @@ public static class WebMessageRegistries { | ||||
| 	static WebMessageRegistries() { | ||||
| 		ToController.Add<RegisterWebMessage>(0); | ||||
| 		ToController.Add<UnregisterWebMessage>(1); | ||||
| 		ToController.Add<LogInMessage, LogInSuccess?>(2); | ||||
| 		ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(3); | ||||
| 		ToController.Add<CreateUserMessage, CreateUserResult>(4); | ||||
| 		ToController.Add<DeleteUserMessage, DeleteUserResult>(5); | ||||
| 		ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(6); | ||||
| 		ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(7); | ||||
| 		ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(8); | ||||
| 		ToController.Add<ChangeUserRolesMessage, ChangeUserRolesResult>(9); | ||||
| 		ToController.Add<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(10); | ||||
| 		ToController.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(11); | ||||
| 		ToController.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(12); | ||||
| 		ToController.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(13); | ||||
| 		ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(14); | ||||
| 		ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(15); | ||||
| 		ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(16); | ||||
| 		ToController.Add<GetEventLogMessage, ImmutableArray<EventLogItem>>(17); | ||||
| 		ToController.Add<LogInMessage, Optional<LogInSuccess>>(2); | ||||
| 		ToController.Add<LogOutMessage>(3); | ||||
| 		ToController.Add<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(4); | ||||
| 		ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(5); | ||||
| 		ToController.Add<CreateUserMessage, Result<CreateUserResult, UserActionFailure>>(6); | ||||
| 		ToController.Add<DeleteUserMessage, Result<DeleteUserResult, UserActionFailure>>(7); | ||||
| 		ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(8); | ||||
| 		ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(9); | ||||
| 		ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(10); | ||||
| 		ToController.Add<ChangeUserRolesMessage, Result<ChangeUserRolesResult, UserActionFailure>>(11); | ||||
| 		ToController.Add<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(12); | ||||
| 		ToController.Add<LaunchInstanceMessage, Result<LaunchInstanceResult, UserInstanceActionFailure>>(13); | ||||
| 		ToController.Add<StopInstanceMessage, Result<StopInstanceResult, UserInstanceActionFailure>>(14); | ||||
| 		ToController.Add<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>(15); | ||||
| 		ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(16); | ||||
| 		ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(17); | ||||
| 		ToController.Add<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(18); | ||||
| 		ToController.Add<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(19); | ||||
| 		ToController.Add<ReplyMessage>(127); | ||||
| 		 | ||||
| 		ToWeb.Add<RegisterWebResultMessage>(0); | ||||
| 		ToWeb.Add<RefreshAgentsMessage>(1); | ||||
| 		ToWeb.Add<RefreshInstancesMessage>(2); | ||||
| 		ToWeb.Add<InstanceOutputMessage>(3); | ||||
| 		ToWeb.Add<RefreshUserSessionMessage>(4); | ||||
| 		ToWeb.Add<ReplyMessage>(127); | ||||
| 	} | ||||
|  | ||||
|   | ||||
							
								
								
									
										353
									
								
								Controller/Phantom.Controller.Database.Postgres/Migrations/20240407211636_UserAgentAccess.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										353
									
								
								Controller/Phantom.Controller.Database.Postgres/Migrations/20240407211636_UserAgentAccess.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,353 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using System.Text.Json; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||
| using Phantom.Controller.Database; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace Phantom.Controller.Database.Postgres.Migrations | ||||
| { | ||||
|     [DbContext(typeof(ApplicationDbContext))] | ||||
|     [Migration("20240407211636_UserAgentAccess")] | ||||
|     partial class UserAgentAccess | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||
|         { | ||||
| #pragma warning disable 612, 618 | ||||
|             modelBuilder | ||||
|                 .HasAnnotation("ProductVersion", "8.0.0") | ||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||
|  | ||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||
|  | ||||
|             modelBuilder.Entity("Phantom.Controller.Database.Entities.AgentEntity", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("AgentGuid") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid"); | ||||
|  | ||||
|                     b.Property<string>("BuildVersion") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text"); | ||||
|  | ||||
|                     b.Property<int>("MaxInstances") | ||||
|                         .HasColumnType("integer"); | ||||
|  | ||||
|                     b.Property<ushort>("MaxMemory") | ||||
|                         .HasColumnType("integer"); | ||||
|  | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text"); | ||||
|  | ||||
|                     b.Property<int>("ProtocolVersion") | ||||
|                         .HasColumnType("integer"); | ||||
|  | ||||
|                     b.HasKey("AgentGuid"); | ||||
|  | ||||
|                     b.ToTable("Agents", "agents"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Phantom.Controller.Database.Entities.AuditLogEntity", b => | ||||
|                 { | ||||
|                     b.Property<long>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("bigint"); | ||||
|  | ||||
|                     NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id")); | ||||
|  | ||||
|                     b.Property<JsonDocument>("Data") | ||||
|                         .HasColumnType("jsonb"); | ||||
|  | ||||
|                     b.Property<string>("EventType") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text"); | ||||
|  | ||||
|                     b.Property<string>("SubjectId") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text"); | ||||
|  | ||||
|                     b.Property<string>("SubjectType") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text"); | ||||
|  | ||||
|                     b.Property<Guid?>("UserGuid") | ||||
|                         .HasColumnType("uuid"); | ||||
|  | ||||
|                     b.Property<DateTime>("UtcTime") | ||||
|                         .HasColumnType("timestamp with time zone"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.HasIndex("UserGuid"); | ||||
|  | ||||
|                     b.ToTable("AuditLog", "system"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Phantom.Controller.Database.Entities.EventLogEntity", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("EventGuid") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid"); | ||||
|  | ||||
|                     b.Property<Guid?>("AgentGuid") | ||||
|                         .HasColumnType("uuid"); | ||||
|  | ||||
|                     b.Property<JsonDocument>("Data") | ||||
|                         .HasColumnType("jsonb"); | ||||
|  | ||||
|                     b.Property<string>("EventType") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text"); | ||||
|  | ||||
|                     b.Property<string>("SubjectId") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text"); | ||||
|  | ||||
|                     b.Property<string>("SubjectType") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text"); | ||||
|  | ||||
|                     b.Property<DateTime>("UtcTime") | ||||
|                         .HasColumnType("timestamp with time zone"); | ||||
|  | ||||
|                     b.HasKey("EventGuid"); | ||||
|  | ||||
|                     b.ToTable("EventLog", "system"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Phantom.Controller.Database.Entities.InstanceEntity", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("InstanceGuid") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid"); | ||||
|  | ||||
|                     b.Property<Guid>("AgentGuid") | ||||
|                         .HasColumnType("uuid"); | ||||
|  | ||||
|                     b.Property<string>("InstanceName") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text"); | ||||
|  | ||||
|                     b.Property<Guid>("JavaRuntimeGuid") | ||||
|                         .HasColumnType("uuid"); | ||||
|  | ||||
|                     b.Property<string>("JvmArguments") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text"); | ||||
|  | ||||
|                     b.Property<bool>("LaunchAutomatically") | ||||
|                         .HasColumnType("boolean"); | ||||
|  | ||||
|                     b.Property<ushort>("MemoryAllocation") | ||||
|                         .HasColumnType("integer"); | ||||
|  | ||||
|                     b.Property<string>("MinecraftServerKind") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text"); | ||||
|  | ||||
|                     b.Property<string>("MinecraftVersion") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text"); | ||||
|  | ||||
|                     b.Property<int>("RconPort") | ||||
|                         .HasColumnType("integer"); | ||||
|  | ||||
|                     b.Property<int>("ServerPort") | ||||
|                         .HasColumnType("integer"); | ||||
|  | ||||
|                     b.HasKey("InstanceGuid"); | ||||
|  | ||||
|                     b.ToTable("Instances", "agents"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Phantom.Controller.Database.Entities.PermissionEntity", b => | ||||
|                 { | ||||
|                     b.Property<string>("Id") | ||||
|                         .HasColumnType("text"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.ToTable("Permissions", "identity"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Phantom.Controller.Database.Entities.RoleEntity", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("RoleGuid") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid"); | ||||
|  | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text"); | ||||
|  | ||||
|                     b.HasKey("RoleGuid"); | ||||
|  | ||||
|                     b.ToTable("Roles", "identity"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Phantom.Controller.Database.Entities.RolePermissionEntity", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("RoleGuid") | ||||
|                         .HasColumnType("uuid"); | ||||
|  | ||||
|                     b.Property<string>("PermissionId") | ||||
|                         .HasColumnType("text"); | ||||
|  | ||||
|                     b.HasKey("RoleGuid", "PermissionId"); | ||||
|  | ||||
|                     b.HasIndex("PermissionId"); | ||||
|  | ||||
|                     b.ToTable("RolePermissions", "identity"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Phantom.Controller.Database.Entities.UserAgentAccessEntity", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("UserGuid") | ||||
|                         .HasColumnType("uuid"); | ||||
|  | ||||
|                     b.Property<Guid>("AgentGuid") | ||||
|                         .HasColumnType("uuid"); | ||||
|  | ||||
|                     b.HasKey("UserGuid", "AgentGuid"); | ||||
|  | ||||
|                     b.HasIndex("AgentGuid"); | ||||
|  | ||||
|                     b.ToTable("UserAgentAccess", "identity"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Phantom.Controller.Database.Entities.UserEntity", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("UserGuid") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid"); | ||||
|  | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text"); | ||||
|  | ||||
|                     b.Property<string>("PasswordHash") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text"); | ||||
|  | ||||
|                     b.HasKey("UserGuid"); | ||||
|  | ||||
|                     b.HasIndex("Name") | ||||
|                         .IsUnique(); | ||||
|  | ||||
|                     b.ToTable("Users", "identity"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Phantom.Controller.Database.Entities.UserPermissionEntity", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("UserGuid") | ||||
|                         .HasColumnType("uuid"); | ||||
|  | ||||
|                     b.Property<string>("PermissionId") | ||||
|                         .HasColumnType("text"); | ||||
|  | ||||
|                     b.HasKey("UserGuid", "PermissionId"); | ||||
|  | ||||
|                     b.HasIndex("PermissionId"); | ||||
|  | ||||
|                     b.ToTable("UserPermissions", "identity"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Phantom.Controller.Database.Entities.UserRoleEntity", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("UserGuid") | ||||
|                         .HasColumnType("uuid"); | ||||
|  | ||||
|                     b.Property<Guid>("RoleGuid") | ||||
|                         .HasColumnType("uuid"); | ||||
|  | ||||
|                     b.HasKey("UserGuid", "RoleGuid"); | ||||
|  | ||||
|                     b.HasIndex("RoleGuid"); | ||||
|  | ||||
|                     b.ToTable("UserRoles", "identity"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Phantom.Controller.Database.Entities.AuditLogEntity", b => | ||||
|                 { | ||||
|                     b.HasOne("Phantom.Controller.Database.Entities.UserEntity", "User") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("UserGuid") | ||||
|                         .OnDelete(DeleteBehavior.SetNull); | ||||
|  | ||||
|                     b.Navigation("User"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Phantom.Controller.Database.Entities.RolePermissionEntity", b => | ||||
|                 { | ||||
|                     b.HasOne("Phantom.Controller.Database.Entities.PermissionEntity", null) | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("PermissionId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.HasOne("Phantom.Controller.Database.Entities.RoleEntity", null) | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("RoleGuid") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Phantom.Controller.Database.Entities.UserAgentAccessEntity", b => | ||||
|                 { | ||||
|                     b.HasOne("Phantom.Controller.Database.Entities.AgentEntity", null) | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AgentGuid") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.HasOne("Phantom.Controller.Database.Entities.UserEntity", null) | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("UserGuid") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Phantom.Controller.Database.Entities.UserPermissionEntity", b => | ||||
|                 { | ||||
|                     b.HasOne("Phantom.Controller.Database.Entities.PermissionEntity", null) | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("PermissionId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.HasOne("Phantom.Controller.Database.Entities.UserEntity", null) | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("UserGuid") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Phantom.Controller.Database.Entities.UserRoleEntity", b => | ||||
|                 { | ||||
|                     b.HasOne("Phantom.Controller.Database.Entities.RoleEntity", "Role") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("RoleGuid") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.HasOne("Phantom.Controller.Database.Entities.UserEntity", "User") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("UserGuid") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.Navigation("Role"); | ||||
|  | ||||
|                     b.Navigation("User"); | ||||
|                 }); | ||||
| #pragma warning restore 612, 618 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,56 @@ | ||||
| using System; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace Phantom.Controller.Database.Postgres.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class UserAgentAccess : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.CreateTable( | ||||
|                 name: "UserAgentAccess", | ||||
|                 schema: "identity", | ||||
|                 columns: table => new | ||||
|                 { | ||||
|                     UserGuid = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     AgentGuid = table.Column<Guid>(type: "uuid", nullable: false) | ||||
|                 }, | ||||
|                 constraints: table => | ||||
|                 { | ||||
|                     table.PrimaryKey("PK_UserAgentAccess", x => new { x.UserGuid, x.AgentGuid }); | ||||
|                     table.ForeignKey( | ||||
|                         name: "FK_UserAgentAccess_Agents_AgentGuid", | ||||
|                         column: x => x.AgentGuid, | ||||
|                         principalSchema: "agents", | ||||
|                         principalTable: "Agents", | ||||
|                         principalColumn: "AgentGuid", | ||||
|                         onDelete: ReferentialAction.Cascade); | ||||
|                     table.ForeignKey( | ||||
|                         name: "FK_UserAgentAccess_Users_UserGuid", | ||||
|                         column: x => x.UserGuid, | ||||
|                         principalSchema: "identity", | ||||
|                         principalTable: "Users", | ||||
|                         principalColumn: "UserGuid", | ||||
|                         onDelete: ReferentialAction.Cascade); | ||||
|                 }); | ||||
|  | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "IX_UserAgentAccess_AgentGuid", | ||||
|                 schema: "identity", | ||||
|                 table: "UserAgentAccess", | ||||
|                 column: "AgentGuid"); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropTable( | ||||
|                 name: "UserAgentAccess", | ||||
|                 schema: "identity"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -18,7 +18,7 @@ namespace Phantom.Controller.Database.Postgres.Migrations | ||||
|         { | ||||
| #pragma warning disable 612, 618 | ||||
|             modelBuilder | ||||
|                 .HasAnnotation("ProductVersion", "7.0.11") | ||||
|                 .HasAnnotation("ProductVersion", "8.0.0") | ||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||
|  | ||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||
| @@ -204,6 +204,21 @@ namespace Phantom.Controller.Database.Postgres.Migrations | ||||
|                     b.ToTable("RolePermissions", "identity"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Phantom.Controller.Database.Entities.UserAgentAccessEntity", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("UserGuid") | ||||
|                         .HasColumnType("uuid"); | ||||
|  | ||||
|                     b.Property<Guid>("AgentGuid") | ||||
|                         .HasColumnType("uuid"); | ||||
|  | ||||
|                     b.HasKey("UserGuid", "AgentGuid"); | ||||
|  | ||||
|                     b.HasIndex("AgentGuid"); | ||||
|  | ||||
|                     b.ToTable("UserAgentAccess", "identity"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Phantom.Controller.Database.Entities.UserEntity", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("UserGuid") | ||||
| @@ -281,6 +296,21 @@ namespace Phantom.Controller.Database.Postgres.Migrations | ||||
|                         .IsRequired(); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Phantom.Controller.Database.Entities.UserAgentAccessEntity", b => | ||||
|                 { | ||||
|                     b.HasOne("Phantom.Controller.Database.Entities.AgentEntity", null) | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AgentGuid") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.HasOne("Phantom.Controller.Database.Entities.UserEntity", null) | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("UserGuid") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Phantom.Controller.Database.Entities.UserPermissionEntity", b => | ||||
|                 { | ||||
|                     b.HasOne("Phantom.Controller.Database.Entities.PermissionEntity", null) | ||||
|   | ||||
| @@ -13,18 +13,19 @@ namespace Phantom.Controller.Database; | ||||
|  | ||||
| [SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")] | ||||
| public class ApplicationDbContext : DbContext { | ||||
| 	public DbSet<UserEntity> Users { get; set; } = null!; | ||||
| 	public DbSet<RoleEntity> Roles { get; set; } = null!; | ||||
| 	public DbSet<PermissionEntity> Permissions { get; set; } = null!; | ||||
| 	public DbSet<UserEntity> Users { get; init; } = null!; | ||||
| 	public DbSet<RoleEntity> Roles { get; init; } = null!; | ||||
| 	public DbSet<PermissionEntity> Permissions { get; init; } = null!; | ||||
| 	 | ||||
| 	public DbSet<UserRoleEntity> UserRoles { get; set; } = null!; | ||||
| 	public DbSet<UserPermissionEntity> UserPermissions { get; set; } = null!; | ||||
| 	public DbSet<RolePermissionEntity> RolePermissions { get; set; } = null!; | ||||
| 	 | ||||
| 	public DbSet<AgentEntity> Agents { get; set; } = null!; | ||||
| 	public DbSet<InstanceEntity> Instances { get; set; } = null!; | ||||
| 	public DbSet<AuditLogEntity> AuditLog { get; set; } = null!; | ||||
| 	public DbSet<EventLogEntity> EventLog { get; set; } = null!; | ||||
| 	public DbSet<UserRoleEntity> UserRoles { get; init; } = null!; | ||||
| 	public DbSet<UserPermissionEntity> UserPermissions { get; init; } = null!; | ||||
| 	public DbSet<RolePermissionEntity> RolePermissions { get; init; } = null!; | ||||
| 	public DbSet<UserAgentAccessEntity> UserAgentAccess { get; init; } = null!; | ||||
|  | ||||
| 	public DbSet<AgentEntity> Agents { get; init; } = null!; | ||||
| 	public DbSet<InstanceEntity> Instances { get; init; } = null!; | ||||
| 	public DbSet<AuditLogEntity> AuditLog { get; init; } = null!; | ||||
| 	public DbSet<EventLogEntity> EventLog { get; init; } = null!; | ||||
|  | ||||
| 	public AgentEntityUpsert AgentUpsert { get; } | ||||
| 	public InstanceEntityUpsert InstanceUpsert { get; } | ||||
| @@ -62,6 +63,12 @@ public class ApplicationDbContext : DbContext { | ||||
| 			b.HasOne<RoleEntity>().WithMany().HasForeignKey(static e => e.RoleGuid).IsRequired().OnDelete(DeleteBehavior.Cascade); | ||||
| 			b.HasOne<PermissionEntity>().WithMany().HasForeignKey(static e => e.PermissionId).IsRequired().OnDelete(DeleteBehavior.Cascade); | ||||
| 		}); | ||||
| 		 | ||||
| 		builder.Entity<UserAgentAccessEntity>(static b => { | ||||
| 			b.HasKey(static e => new { UserId = e.UserGuid, AgentId = e.AgentGuid }); | ||||
| 			b.HasOne<UserEntity>().WithMany().HasForeignKey(static e => e.UserGuid).IsRequired().OnDelete(DeleteBehavior.Cascade); | ||||
| 			b.HasOne<AgentEntity>().WithMany().HasForeignKey(static e => e.AgentGuid).IsRequired().OnDelete(DeleteBehavior.Cascade); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	protected override void ConfigureConventions(ModelConfigurationBuilder builder) { | ||||
|   | ||||
| @@ -9,7 +9,7 @@ namespace Phantom.Controller.Database.Entities; | ||||
| [SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")] | ||||
| public sealed class AgentEntity { | ||||
| 	[Key] | ||||
| 	public Guid AgentGuid { get; set; } | ||||
| 	public Guid AgentGuid { get; init; } | ||||
| 	 | ||||
| 	public string Name { get; set; } | ||||
| 	public ushort ProtocolVersion { get; set; } | ||||
|   | ||||
| @@ -13,16 +13,16 @@ public class AuditLogEntity : IDisposable { | ||||
| 	[Key] | ||||
| 	[DatabaseGenerated(DatabaseGeneratedOption.Identity)] | ||||
| 	[SuppressMessage("ReSharper", "UnusedMember.Global")] | ||||
| 	public long Id { get; set; } | ||||
| 	public long Id { get; init; } | ||||
|  | ||||
| 	public Guid? UserGuid { get; set; } | ||||
| 	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; set; } | ||||
| 	public AuditLogSubjectType SubjectType { get; set; } | ||||
| 	public string SubjectId { get; set; } | ||||
| 	public JsonDocument? Data { get; set; } | ||||
| 	public Guid? UserGuid { get; init; } | ||||
| 	public DateTime UtcTime { get; init; } // Note: Converting to UTC is not best practice, but for historical records it's good enough. | ||||
| 	public AuditLogEventType EventType { get; init; } | ||||
| 	public AuditLogSubjectType SubjectType { get; init; } | ||||
| 	public string SubjectId { get; init; } | ||||
| 	public JsonDocument? Data { get; init; } | ||||
|  | ||||
| 	public virtual UserEntity? User { get; set; } | ||||
| 	public virtual UserEntity? User { get; init; } | ||||
| 	 | ||||
| 	[SuppressMessage("ReSharper", "UnusedMember.Global")] | ||||
| 	internal AuditLogEntity() { | ||||
|   | ||||
| @@ -11,14 +11,14 @@ namespace Phantom.Controller.Database.Entities; | ||||
| [SuppressMessage("ReSharper", "ClassWithVirtualMembersNeverInherited.Global")] | ||||
| public sealed class EventLogEntity : IDisposable { | ||||
| 	[Key] | ||||
| 	public Guid EventGuid { get; set; } | ||||
| 	public Guid EventGuid { get; init; } | ||||
|  | ||||
| 	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; set; } | ||||
| 	public EventLogEventType EventType { get; set; } | ||||
| 	public EventLogSubjectType SubjectType { get; set; } | ||||
| 	public string SubjectId { get; set; } | ||||
| 	public JsonDocument? Data { get; set; } | ||||
| 	public DateTime UtcTime { get; init; } // Note: Converting to UTC is not best practice, but for historical records it's good enough. | ||||
| 	public Guid? AgentGuid { get; init; } | ||||
| 	public EventLogEventType EventType { get; init; } | ||||
| 	public EventLogSubjectType SubjectType { get; init; } | ||||
| 	public string SubjectId { get; init; } | ||||
| 	public JsonDocument? Data { get; init; } | ||||
| 	 | ||||
| 	[SuppressMessage("ReSharper", "UnusedMember.Global")] | ||||
| 	internal EventLogEntity() { | ||||
|   | ||||
| @@ -11,7 +11,7 @@ namespace Phantom.Controller.Database.Entities; | ||||
| [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] | ||||
| public sealed class InstanceEntity { | ||||
| 	[Key] | ||||
| 	public Guid InstanceGuid { get; set; } | ||||
| 	public Guid InstanceGuid { get; init; } | ||||
|  | ||||
| 	public Guid AgentGuid { get; set; } | ||||
|  | ||||
|   | ||||
| @@ -6,7 +6,7 @@ namespace Phantom.Controller.Database.Entities; | ||||
| [Table("Permissions", Schema = "identity")] | ||||
| public sealed class PermissionEntity { | ||||
| 	[Key] | ||||
| 	public string Id { get; set; } | ||||
| 	public string Id { get; init; } | ||||
|  | ||||
| 	public PermissionEntity(string id) { | ||||
| 		Id = id; | ||||
|   | ||||
| @@ -7,9 +7,9 @@ namespace Phantom.Controller.Database.Entities; | ||||
| [Table("Roles", Schema = "identity")] | ||||
| public sealed class RoleEntity { | ||||
| 	[Key] | ||||
| 	public Guid RoleGuid { get; set; } | ||||
| 	public Guid RoleGuid { get; init; } | ||||
|  | ||||
| 	public string Name { get; set; } | ||||
| 	public string Name { get; init; } | ||||
|  | ||||
| 	public RoleEntity(Guid roleGuid, string name) { | ||||
| 		RoleGuid = roleGuid; | ||||
|   | ||||
| @@ -4,8 +4,8 @@ namespace Phantom.Controller.Database.Entities; | ||||
|  | ||||
| [Table("RolePermissions", Schema = "identity")] | ||||
| public sealed class RolePermissionEntity { | ||||
| 	public Guid RoleGuid { get; set; } | ||||
| 	public string PermissionId { get; set; } | ||||
| 	public Guid RoleGuid { get; init; } | ||||
| 	public string PermissionId { get; init; } | ||||
| 	 | ||||
| 	public RolePermissionEntity(Guid roleGuid, string permissionId) { | ||||
| 		RoleGuid = roleGuid; | ||||
|   | ||||
| @@ -0,0 +1,14 @@ | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
|  | ||||
| namespace Phantom.Controller.Database.Entities; | ||||
|  | ||||
| [Table("UserAgentAccess", Schema = "identity")] | ||||
| public sealed class UserAgentAccessEntity { | ||||
| 	public Guid UserGuid { get; init; } | ||||
| 	public Guid AgentGuid { get; init; } | ||||
| 	 | ||||
| 	public UserAgentAccessEntity(Guid userGuid, Guid agentGuid) { | ||||
| 		UserGuid = userGuid; | ||||
| 		AgentGuid = agentGuid; | ||||
| 	} | ||||
| } | ||||
| @@ -7,9 +7,9 @@ namespace Phantom.Controller.Database.Entities; | ||||
| [Table("Users", Schema = "identity")] | ||||
| public sealed class UserEntity { | ||||
| 	[Key] | ||||
| 	public Guid UserGuid { get; set; } | ||||
| 	public Guid UserGuid { get; init; } | ||||
|  | ||||
| 	public string Name { get; set; } | ||||
| 	public string Name { get; init; } | ||||
| 	public string PasswordHash { get; set; } | ||||
|  | ||||
| 	public UserEntity(Guid userGuid, string name, string passwordHash) { | ||||
|   | ||||
| @@ -4,8 +4,8 @@ namespace Phantom.Controller.Database.Entities; | ||||
|  | ||||
| [Table("UserPermissions", Schema = "identity")] | ||||
| public sealed class UserPermissionEntity { | ||||
| 	public Guid UserGuid { get; set; } | ||||
| 	public string PermissionId { get; set; } | ||||
| 	public Guid UserGuid { get; init; } | ||||
| 	public string PermissionId { get; init; } | ||||
|  | ||||
| 	public UserPermissionEntity(Guid userGuid, string permissionId) { | ||||
| 		UserGuid = userGuid; | ||||
|   | ||||
| @@ -4,11 +4,11 @@ namespace Phantom.Controller.Database.Entities; | ||||
|  | ||||
| [Table("UserRoles", Schema = "identity")] | ||||
| public sealed class UserRoleEntity { | ||||
| 	public Guid UserGuid { get; set; } | ||||
| 	public Guid RoleGuid { get; set; } | ||||
| 	public Guid UserGuid { get; init; } | ||||
| 	public Guid RoleGuid { get; init; } | ||||
|  | ||||
| 	public UserEntity User { get; set; } | ||||
| 	public RoleEntity Role { get; set; } | ||||
| 	public UserEntity User { get; init; } | ||||
| 	public RoleEntity Role { get; init; } | ||||
|  | ||||
| 	public UserRoleEntity(Guid userGuid, Guid roleGuid) { | ||||
| 		UserGuid = userGuid; | ||||
|   | ||||
| @@ -17,11 +17,12 @@ public sealed class EventLogRepository { | ||||
| 		db.Ctx.EventLog.Add(new EventLogEntity(eventGuid, utcTime, agentGuid, eventType, subjectId, extra)); | ||||
| 	} | ||||
| 	 | ||||
| 	public Task<ImmutableArray<EventLogItem>> GetMostRecentItems(int count, CancellationToken cancellationToken) { | ||||
| 	public Task<ImmutableArray<EventLogItem>> GetMostRecentItems(ImmutableHashSet<Guid> agentGuids, int count, CancellationToken cancellationToken) { | ||||
| 		return db.Ctx | ||||
| 		         .EventLog | ||||
| 		         .AsQueryable() | ||||
| 		         .OrderByDescending(static entity => entity.UtcTime) | ||||
| 		         .Where(entity => entity.AgentGuid == null || agentGuids.Contains(entity.AgentGuid.Value)) | ||||
| 		         .Take(count) | ||||
| 		         .AsAsyncEnumerable() | ||||
| 		         .Select(static entity => new EventLogItem(entity.UtcTime, entity.AgentGuid, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data?.RootElement.ToString())) | ||||
|   | ||||
| @@ -0,0 +1,35 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Controller.Database.Entities; | ||||
| using Phantom.Utils.Collections; | ||||
|  | ||||
| namespace Phantom.Controller.Database.Repositories; | ||||
|  | ||||
| public sealed class PermissionRepository { | ||||
| 	private readonly ILazyDbContext db; | ||||
|  | ||||
| 	public PermissionRepository(ILazyDbContext db) { | ||||
| 		this.db = db; | ||||
| 	} | ||||
|  | ||||
| 	public async Task<PermissionSet> GetAllUserPermissions(UserEntity user) { | ||||
| 		var userPermissions = db.Ctx.UserPermissions | ||||
| 		                        .Where(up => up.UserGuid == user.UserGuid) | ||||
| 		                        .Select(static up => up.PermissionId); | ||||
|  | ||||
| 		var rolePermissions = db.Ctx.UserRoles | ||||
| 		                        .Where(ur => ur.UserGuid == user.UserGuid) | ||||
| 		                        .Join(db.Ctx.RolePermissions, static ur => ur.RoleGuid, static rp => rp.RoleGuid, static (ur, rp) => rp.PermissionId); | ||||
|  | ||||
| 		return new PermissionSet(await userPermissions.Union(rolePermissions).AsAsyncEnumerable().ToImmutableSetAsync()); | ||||
| 	} | ||||
|  | ||||
| 	public Task<ImmutableHashSet<Guid>> GetManagedAgentGuids(UserEntity user) { | ||||
| 		return db.Ctx.UserAgentAccess | ||||
| 		         .Where(ua => ua.UserGuid == user.UserGuid) | ||||
| 		         .Select(static ua => ua.AgentGuid) | ||||
| 		         .AsAsyncEnumerable() | ||||
| 		         .ToImmutableSetAsync(); | ||||
| 	} | ||||
| } | ||||
| @@ -1,9 +1,9 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Controller.Database.Entities; | ||||
| using Phantom.Utils.Collections; | ||||
| using Phantom.Utils.Tasks; | ||||
|  | ||||
| namespace Phantom.Controller.Database.Repositories; | ||||
|  | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Common.Data.Web.Users.AddUserErrors; | ||||
| using Phantom.Common.Data.Web.Users.PasswordRequirementViolations; | ||||
| using Phantom.Common.Data.Web.Users.UsernameRequirementViolations; | ||||
| using Phantom.Controller.Database.Entities; | ||||
| using Phantom.Utils.Collections; | ||||
| using Phantom.Utils.Tasks; | ||||
|  | ||||
| namespace Phantom.Controller.Database.Repositories; | ||||
|  | ||||
|   | ||||
| @@ -13,11 +13,13 @@ using Phantom.Common.Data.Web.Minecraft; | ||||
| using Phantom.Common.Messages.Agent; | ||||
| using Phantom.Common.Messages.Agent.ToAgent; | ||||
| using Phantom.Controller.Database; | ||||
| using Phantom.Controller.Database.Entities; | ||||
| using Phantom.Controller.Minecraft; | ||||
| using Phantom.Controller.Services.Instances; | ||||
| using Phantom.Utils.Actor; | ||||
| using Phantom.Utils.Actor.Mailbox; | ||||
| using Phantom.Utils.Actor.Tasks; | ||||
| using Phantom.Utils.Collections; | ||||
| using Phantom.Utils.Logging; | ||||
| using Phantom.Utils.Rpc.Runtime; | ||||
| using Serilog; | ||||
| @@ -92,11 +94,12 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { | ||||
| 		Receive<NotifyIsAliveCommand>(NotifyIsAlive); | ||||
| 		Receive<UpdateStatsCommand>(UpdateStats); | ||||
| 		Receive<UpdateJavaRuntimesCommand>(UpdateJavaRuntimes); | ||||
| 		ReceiveAndReplyLater<CreateOrUpdateInstanceCommand, InstanceActionResult<CreateOrUpdateInstanceResult>>(CreateOrUpdateInstance); | ||||
| 		ReceiveAndReplyLater<CreateOrUpdateInstanceCommand, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstance); | ||||
| 		Receive<UpdateInstanceStatusCommand>(UpdateInstanceStatus); | ||||
| 		ReceiveAndReplyLater<LaunchInstanceCommand, InstanceActionResult<LaunchInstanceResult>>(LaunchInstance); | ||||
| 		ReceiveAndReplyLater<StopInstanceCommand, InstanceActionResult<StopInstanceResult>>(StopInstance); | ||||
| 		ReceiveAndReplyLater<SendCommandToInstanceCommand, InstanceActionResult<SendCommandToInstanceResult>>(SendMinecraftCommand); | ||||
| 		Receive<UpdateInstancePlayerCountsCommand>(UpdateInstancePlayerCounts); | ||||
| 		ReceiveAndReplyLater<LaunchInstanceCommand, Result<LaunchInstanceResult, InstanceActionFailure>>(LaunchInstance); | ||||
| 		ReceiveAndReplyLater<StopInstanceCommand, Result<StopInstanceResult, InstanceActionFailure>>(StopInstance); | ||||
| 		ReceiveAndReplyLater<SendCommandToInstanceCommand, Result<SendCommandToInstanceResult, InstanceActionFailure>>(SendMinecraftCommand); | ||||
| 		Receive<ReceiveInstanceDataCommand>(ReceiveInstanceData); | ||||
| 	} | ||||
|  | ||||
| @@ -144,20 +147,20 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private Task<InstanceActionResult<TReply>> RequestInstance<TCommand, TReply>(Guid instanceGuid, TCommand command) where TCommand : InstanceActor.ICommand, ICanReply<InstanceActionResult<TReply>> { | ||||
| 	private async Task<Result<TReply, InstanceActionFailure>> RequestInstance<TCommand, TReply>(Guid instanceGuid, TCommand command) where TCommand : InstanceActor.ICommand, ICanReply<Result<TReply, InstanceActionFailure>> { | ||||
| 		if (instanceActorByGuid.TryGetValue(instanceGuid, out var instance)) { | ||||
| 			return instance.Request(command, cancellationToken); | ||||
| 			return await instance.Request(command, cancellationToken); | ||||
| 		} | ||||
| 		else { | ||||
| 			Logger.Warning("Could not deliver command {CommandType} to instance {InstanceGuid}, instance not found.", command.GetType().Name, instanceGuid); | ||||
| 			return Task.FromResult(InstanceActionResult.General<TReply>(InstanceActionGeneralResult.InstanceDoesNotExist)); | ||||
| 			return InstanceActionFailure.InstanceDoesNotExist; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private async Task<ImmutableArray<ConfigureInstanceMessage>> PrepareInitialConfigurationMessages() { | ||||
| 		var configurationMessages = ImmutableArray.CreateBuilder<ConfigureInstanceMessage>(); | ||||
| 		 | ||||
| 		foreach (var (instanceGuid, instanceConfiguration, _, launchAutomatically) in instanceDataByGuid.Values.ToImmutableArray()) { | ||||
| 		foreach (var (instanceGuid, instanceConfiguration, _, _, launchAutomatically) in instanceDataByGuid.Values.ToImmutableArray()) { | ||||
| 			var serverExecutableInfo = await minecraftVersions.GetServerExecutableInfo(instanceConfiguration.MinecraftVersion, cancellationToken); | ||||
| 			configurationMessages.Add(new ConfigureInstanceMessage(instanceGuid, instanceConfiguration, new InstanceLaunchProperties(serverExecutableInfo), launchAutomatically)); | ||||
| 		} | ||||
| @@ -181,34 +184,44 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { | ||||
| 	 | ||||
| 	public sealed record UpdateJavaRuntimesCommand(ImmutableArray<TaggedJavaRuntime> JavaRuntimes) : ICommand; | ||||
| 	 | ||||
| 	public sealed record CreateOrUpdateInstanceCommand(Guid AuditLogUserGuid, Guid InstanceGuid, InstanceConfiguration Configuration) : ICommand, ICanReply<InstanceActionResult<CreateOrUpdateInstanceResult>>; | ||||
| 	public sealed record CreateOrUpdateInstanceCommand(Guid LoggedInUserGuid, Guid InstanceGuid, InstanceConfiguration Configuration) : ICommand, ICanReply<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>; | ||||
| 	 | ||||
| 	public sealed record UpdateInstanceStatusCommand(Guid InstanceGuid, IInstanceStatus Status) : ICommand; | ||||
| 	 | ||||
| 	public sealed record 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; | ||||
|  | ||||
| 	private async Task Initialize(InitializeCommand command) { | ||||
| 		await using var ctx = dbProvider.Eager(); | ||||
| 		await foreach (var entity in ctx.Instances.Where(instance => instance.AgentGuid == agentGuid).AsAsyncEnumerable().WithCancellation(cancellationToken)) { | ||||
| 		ImmutableArray<InstanceEntity> instanceEntities; | ||||
| 		await using (var ctx = dbProvider.Eager()) { | ||||
| 			instanceEntities = await ctx.Instances.Where(instance => instance.AgentGuid == agentGuid).AsAsyncEnumerable().ToImmutableArrayCatchingExceptionsAsync(OnException, cancellationToken); | ||||
| 		} | ||||
|  | ||||
| 		static void OnException(Exception e) { | ||||
| 			Logger.Error(e, "Could not load instance from database."); | ||||
| 		} | ||||
|  | ||||
| 		foreach (var instanceEntity in instanceEntities) { | ||||
| 			var instanceConfiguration = new InstanceConfiguration( | ||||
| 				entity.AgentGuid, | ||||
| 				entity.InstanceName, | ||||
| 				entity.ServerPort, | ||||
| 				entity.RconPort, | ||||
| 				entity.MinecraftVersion, | ||||
| 				entity.MinecraftServerKind, | ||||
| 				entity.MemoryAllocation, | ||||
| 				entity.JavaRuntimeGuid, | ||||
| 				JvmArgumentsHelper.Split(entity.JvmArguments) | ||||
| 				instanceEntity.AgentGuid, | ||||
| 				instanceEntity.InstanceName, | ||||
| 				instanceEntity.ServerPort, | ||||
| 				instanceEntity.RconPort, | ||||
| 				instanceEntity.MinecraftVersion, | ||||
| 				instanceEntity.MinecraftServerKind, | ||||
| 				instanceEntity.MemoryAllocation, | ||||
| 				instanceEntity.JavaRuntimeGuid, | ||||
| 				JvmArgumentsHelper.Split(instanceEntity.JvmArguments) | ||||
| 			); | ||||
|  | ||||
| 			CreateNewInstance(Instance.Offline(entity.InstanceGuid, instanceConfiguration, entity.LaunchAutomatically)); | ||||
| 			CreateNewInstance(Instance.Offline(instanceEntity.InstanceGuid, instanceConfiguration, instanceEntity.LaunchAutomatically)); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -270,15 +283,15 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { | ||||
| 		controllerState.UpdateAgentJavaRuntimes(agentGuid, javaRuntimes); | ||||
| 	} | ||||
| 	 | ||||
| 	private Task<InstanceActionResult<CreateOrUpdateInstanceResult>> CreateOrUpdateInstance(CreateOrUpdateInstanceCommand command) { | ||||
| 	private Task<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>> CreateOrUpdateInstance(CreateOrUpdateInstanceCommand command) { | ||||
| 		var instanceConfiguration = command.Configuration; | ||||
|  | ||||
| 		if (string.IsNullOrWhiteSpace(instanceConfiguration.InstanceName)) { | ||||
| 			return Task.FromResult(InstanceActionResult.Concrete(CreateOrUpdateInstanceResult.InstanceNameMustNotBeEmpty)); | ||||
| 			return Task.FromResult<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstanceResult.InstanceNameMustNotBeEmpty); | ||||
| 		} | ||||
| 		 | ||||
| 		if (instanceConfiguration.MemoryAllocation <= RamAllocationUnits.Zero) { | ||||
| 			return Task.FromResult(InstanceActionResult.Concrete(CreateOrUpdateInstanceResult.InstanceMemoryMustNotBeZero)); | ||||
| 			return Task.FromResult<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstanceResult.InstanceMemoryMustNotBeZero); | ||||
| 		} | ||||
| 		 | ||||
| 		return minecraftVersions.GetServerExecutableInfo(instanceConfiguration.MinecraftVersion, cancellationToken) | ||||
| @@ -286,9 +299,9 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { | ||||
| 		                        .Unwrap(); | ||||
| 	} | ||||
|  | ||||
| 	private Task<InstanceActionResult<CreateOrUpdateInstanceResult>> CreateOrUpdateInstance1(FileDownloadInfo? serverExecutableInfo, CreateOrUpdateInstanceCommand command) { | ||||
| 	private Task<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>> CreateOrUpdateInstance1(FileDownloadInfo? serverExecutableInfo, CreateOrUpdateInstanceCommand command) { | ||||
| 		if (serverExecutableInfo == null) { | ||||
| 			return Task.FromResult(InstanceActionResult.Concrete(CreateOrUpdateInstanceResult.MinecraftVersionDownloadInfoNotFound)); | ||||
| 			return Task.FromResult<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstanceResult.MinecraftVersionDownloadInfoNotFound); | ||||
| 		} | ||||
| 		 | ||||
| 		var instanceConfiguration = command.Configuration; | ||||
| @@ -298,13 +311,13 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { | ||||
| 			instanceActorRef = CreateNewInstance(Instance.Offline(command.InstanceGuid, instanceConfiguration)); | ||||
| 		} | ||||
| 		 | ||||
| 		var configureInstanceCommand = new InstanceActor.ConfigureInstanceCommand(command.AuditLogUserGuid, command.InstanceGuid, instanceConfiguration, new InstanceLaunchProperties(serverExecutableInfo), isCreatingInstance); | ||||
| 		var configureInstanceCommand = new InstanceActor.ConfigureInstanceCommand(command.LoggedInUserGuid, command.InstanceGuid, instanceConfiguration, new InstanceLaunchProperties(serverExecutableInfo), isCreatingInstance); | ||||
| 		 | ||||
| 		return instanceActorRef.Request(configureInstanceCommand, cancellationToken) | ||||
| 		                       .ContinueOnActor(CreateOrUpdateInstance2, configureInstanceCommand); | ||||
| 	} | ||||
| 	 | ||||
| 	private InstanceActionResult<CreateOrUpdateInstanceResult> CreateOrUpdateInstance2(InstanceActionResult<ConfigureInstanceResult> result, InstanceActor.ConfigureInstanceCommand command) { | ||||
| 	private Result<CreateOrUpdateInstanceResult, InstanceActionFailure> CreateOrUpdateInstance2(Result<ConfigureInstanceResult, InstanceActionFailure> result, InstanceActor.ConfigureInstanceCommand command) { | ||||
| 		var instanceGuid = command.InstanceGuid; | ||||
| 		var instanceName = command.Configuration.InstanceName; | ||||
| 		var isCreating = command.IsCreatingInstance; | ||||
| @@ -312,34 +325,40 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { | ||||
| 		if (result.Is(ConfigureInstanceResult.Success)) { | ||||
| 			string action = isCreating ? "Added" : "Edited"; | ||||
| 			string relation = isCreating ? "to agent" : "in agent"; | ||||
| 			 | ||||
| 			Logger.Information(action + " instance \"{InstanceName}\" (GUID {InstanceGuid}) " + relation + " \"{AgentName}\".", instanceName, instanceGuid, configuration.AgentName); | ||||
| 			 | ||||
| 			return CreateOrUpdateInstanceResult.Success; | ||||
| 		} | ||||
| 		else { | ||||
| 			string action = isCreating ? "adding" : "editing"; | ||||
| 			string relation = isCreating ? "to agent" : "in agent"; | ||||
| 			Logger.Information("Failed " + action + " instance \"{InstanceName}\" (GUID {InstanceGuid}) " + relation + " \"{AgentName}\". {ErrorMessage}", instanceName, instanceGuid, configuration.AgentName, result.ToSentence(ConfigureInstanceResultExtensions.ToSentence)); | ||||
| 			string reason = result.Into(ConfigureInstanceResultExtensions.ToSentence, InstanceActionFailureExtensions.ToSentence); | ||||
| 			 | ||||
| 			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) { | ||||
| 		TellInstance(command.InstanceGuid, new InstanceActor.SetStatusCommand(command.Status)); | ||||
| 	} | ||||
|  | ||||
| 	private Task<InstanceActionResult<LaunchInstanceResult>> LaunchInstance(LaunchInstanceCommand command) { | ||||
| 		return RequestInstance<InstanceActor.LaunchInstanceCommand, LaunchInstanceResult>(command.InstanceGuid, new InstanceActor.LaunchInstanceCommand(command.AuditLogUserGuid)); | ||||
| 	 | ||||
| 	private void UpdateInstancePlayerCounts(UpdateInstancePlayerCountsCommand command) { | ||||
| 		TellInstance(command.InstanceGuid, new InstanceActor.SetPlayerCountsCommand(command.PlayerCounts)); | ||||
| 	} | ||||
|  | ||||
| 	private Task<InstanceActionResult<StopInstanceResult>> StopInstance(StopInstanceCommand command) { | ||||
| 		return RequestInstance<InstanceActor.StopInstanceCommand, StopInstanceResult>(command.InstanceGuid, new InstanceActor.StopInstanceCommand(command.AuditLogUserGuid, command.StopStrategy)); | ||||
| 	private Task<Result<LaunchInstanceResult, InstanceActionFailure>> LaunchInstance(LaunchInstanceCommand command) { | ||||
| 		return RequestInstance<InstanceActor.LaunchInstanceCommand, LaunchInstanceResult>(command.InstanceGuid, new InstanceActor.LaunchInstanceCommand(command.LoggedInUserGuid)); | ||||
| 	} | ||||
|  | ||||
| 	private Task<InstanceActionResult<SendCommandToInstanceResult>> SendMinecraftCommand(SendCommandToInstanceCommand command) { | ||||
| 		return RequestInstance<InstanceActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(command.InstanceGuid, new InstanceActor.SendCommandToInstanceCommand(command.AuditLogUserGuid, command.Command)); | ||||
| 	private Task<Result<StopInstanceResult, InstanceActionFailure>> StopInstance(StopInstanceCommand command) { | ||||
| 		return RequestInstance<InstanceActor.StopInstanceCommand, StopInstanceResult>(command.InstanceGuid, new InstanceActor.StopInstanceCommand(command.LoggedInUserGuid, command.StopStrategy)); | ||||
| 	} | ||||
|  | ||||
| 	private Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> SendMinecraftCommand(SendCommandToInstanceCommand command) { | ||||
| 		return RequestInstance<InstanceActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(command.InstanceGuid, new InstanceActor.SendCommandToInstanceCommand(command.LoggedInUserGuid, command.Command)); | ||||
| 	} | ||||
|  | ||||
| 	private void ReceiveInstanceData(ReceiveInstanceDataCommand command) { | ||||
|   | ||||
| @@ -1,13 +1,16 @@ | ||||
| using System.Collections.Concurrent; | ||||
| using System.Collections.Immutable; | ||||
| using Akka.Actor; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Agent; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Data.Web.Agent; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Common.Messages.Agent; | ||||
| using Phantom.Common.Messages.Agent.ToAgent; | ||||
| using Phantom.Controller.Database; | ||||
| using Phantom.Controller.Minecraft; | ||||
| using Phantom.Controller.Services.Users.Sessions; | ||||
| using Phantom.Utils.Actor; | ||||
| using Phantom.Utils.Logging; | ||||
| using Phantom.Utils.Rpc.Runtime; | ||||
| @@ -22,17 +25,19 @@ sealed class AgentManager { | ||||
| 	private readonly AuthToken authToken; | ||||
| 	private readonly ControllerState controllerState; | ||||
| 	private readonly MinecraftVersions minecraftVersions; | ||||
| 	private readonly UserLoginManager userLoginManager; | ||||
| 	private readonly IDbContextProvider dbProvider; | ||||
| 	private readonly CancellationToken cancellationToken; | ||||
| 	 | ||||
| 	private readonly ConcurrentDictionary<Guid, ActorRef<AgentActor.ICommand>> agentsByGuid = new (); | ||||
| 	private readonly Func<Guid, AgentConfiguration, ActorRef<AgentActor.ICommand>> addAgentActorFactory; | ||||
| 	 | ||||
| 	public AgentManager(IActorRefFactory actorSystem, AuthToken authToken, ControllerState controllerState, MinecraftVersions minecraftVersions, IDbContextProvider dbProvider, CancellationToken cancellationToken) { | ||||
| 	public AgentManager(IActorRefFactory actorSystem, AuthToken authToken, ControllerState controllerState, MinecraftVersions minecraftVersions, UserLoginManager userLoginManager, IDbContextProvider dbProvider, CancellationToken cancellationToken) { | ||||
| 		this.actorSystem = actorSystem; | ||||
| 		this.authToken = authToken; | ||||
| 		this.controllerState = controllerState; | ||||
| 		this.minecraftVersions = minecraftVersions; | ||||
| 		this.userLoginManager = userLoginManager; | ||||
| 		this.dbProvider = dbProvider; | ||||
| 		this.cancellationToken = cancellationToken; | ||||
| 		 | ||||
| @@ -83,12 +88,18 @@ sealed class AgentManager { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public async Task<InstanceActionResult<TReply>> DoInstanceAction<TCommand, TReply>(Guid agentGuid, TCommand command) where TCommand : class, AgentActor.ICommand, ICanReply<InstanceActionResult<TReply>> { | ||||
| 		if (agentsByGuid.TryGetValue(agentGuid, out var agent)) { | ||||
| 			return await agent.Request(command, cancellationToken); | ||||
| 	public async Task<Result<TReply, UserInstanceActionFailure>> DoInstanceAction<TCommand, TReply>(Permission requiredPermission, ImmutableArray<byte> authToken, Guid agentGuid, Func<Guid, TCommand> commandFactoryFromLoggedInUserGuid) where TCommand : class, AgentActor.ICommand, ICanReply<Result<TReply, InstanceActionFailure>> { | ||||
| 		var loggedInUser = userLoginManager.GetLoggedInUser(authToken); | ||||
| 		if (!loggedInUser.HasAccessToAgent(agentGuid) || !loggedInUser.CheckPermission(requiredPermission)) { | ||||
| 			return (UserInstanceActionFailure) UserActionFailure.NotAuthorized; | ||||
| 		} | ||||
| 		else { | ||||
| 			return InstanceActionResult.General<TReply>(InstanceActionGeneralResult.AgentDoesNotExist); | ||||
| 		 | ||||
| 		if (!agentsByGuid.TryGetValue(agentGuid, out var agent)) { | ||||
| 			return (UserInstanceActionFailure) InstanceActionFailure.AgentDoesNotExist; | ||||
| 		} | ||||
| 		 | ||||
| 		var command = commandFactoryFromLoggedInUserGuid(loggedInUser.Guid!.Value); | ||||
| 		var result = await agent.Request(command, cancellationToken); | ||||
| 		return result.MapError(static error => (UserInstanceActionFailure) error); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -11,6 +11,7 @@ using Phantom.Controller.Services.Events; | ||||
| using Phantom.Controller.Services.Instances; | ||||
| using Phantom.Controller.Services.Rpc; | ||||
| using Phantom.Controller.Services.Users; | ||||
| using Phantom.Controller.Services.Users.Sessions; | ||||
| using Phantom.Utils.Actor; | ||||
| using Phantom.Utils.Rpc.Runtime; | ||||
| using IMessageFromAgentToController = Phantom.Common.Messages.Agent.IMessageToController; | ||||
| @@ -24,17 +25,18 @@ public sealed class ControllerServices : IDisposable { | ||||
| 	private ControllerState ControllerState { get; } | ||||
| 	private MinecraftVersions MinecraftVersions { get; } | ||||
|  | ||||
| 	private AgentManager AgentManager { get; } | ||||
| 	private InstanceLogManager InstanceLogManager { get; } | ||||
| 	private EventLogManager EventLogManager { get; } | ||||
|  | ||||
| 	private AuthenticatedUserCache AuthenticatedUserCache { get; } | ||||
| 	private UserManager UserManager { get; } | ||||
| 	private RoleManager RoleManager { get; } | ||||
| 	private PermissionManager PermissionManager { get; } | ||||
|  | ||||
| 	private UserRoleManager UserRoleManager { get; } | ||||
| 	private UserLoginManager UserLoginManager { get; } | ||||
| 	private PermissionManager PermissionManager { get; } | ||||
|  | ||||
| 	private AgentManager AgentManager { get; } | ||||
| 	private InstanceLogManager InstanceLogManager { get; } | ||||
| 	 | ||||
| 	private AuditLogManager AuditLogManager { get; } | ||||
| 	private EventLogManager EventLogManager { get; } | ||||
|  | ||||
| 	public IRegistrationHandler<IMessageToAgent, IMessageFromAgentToController, RegisterAgentMessage> AgentRegistrationHandler { get; } | ||||
| 	public IRegistrationHandler<IMessageToWeb, IMessageFromWebToController, RegisterWebMessage> WebRegistrationHandler { get; } | ||||
| @@ -51,17 +53,18 @@ public sealed class ControllerServices : IDisposable { | ||||
| 		this.ControllerState = new ControllerState(); | ||||
| 		this.MinecraftVersions = new MinecraftVersions(); | ||||
| 		 | ||||
| 		this.AgentManager = new AgentManager(ActorSystem, agentAuthToken, ControllerState, MinecraftVersions, dbProvider, cancellationToken); | ||||
| 		this.AuthenticatedUserCache = new AuthenticatedUserCache(); | ||||
| 		this.UserManager = new UserManager(AuthenticatedUserCache, ControllerState, dbProvider); | ||||
| 		this.RoleManager = new RoleManager(dbProvider); | ||||
| 		this.UserRoleManager = new UserRoleManager(AuthenticatedUserCache, ControllerState, dbProvider); | ||||
| 		this.UserLoginManager = new UserLoginManager(AuthenticatedUserCache, UserManager, dbProvider); | ||||
| 		this.PermissionManager = new PermissionManager(dbProvider); | ||||
| 		 | ||||
| 		this.AgentManager = new AgentManager(ActorSystem, agentAuthToken, ControllerState, MinecraftVersions, UserLoginManager, dbProvider, cancellationToken); | ||||
| 		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.EventLogManager = new EventLogManager(ActorSystem, dbProvider, shutdownCancellationToken); | ||||
| 		this.EventLogManager = new EventLogManager(ControllerState, ActorSystem, dbProvider, shutdownCancellationToken); | ||||
| 		 | ||||
| 		this.AgentRegistrationHandler = new AgentRegistrationHandler(AgentManager, InstanceLogManager, EventLogManager); | ||||
| 		this.WebRegistrationHandler = new WebRegistrationHandler(webAuthToken, ControllerState, InstanceLogManager, UserManager, RoleManager, UserRoleManager, UserLoginManager, AuditLogManager, AgentManager, MinecraftVersions, EventLogManager); | ||||
|   | ||||
| @@ -19,6 +19,8 @@ sealed class ControllerState { | ||||
| 	public ObservableState<ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>.Receiver AgentJavaRuntimesByGuidReceiver => agentJavaRuntimesByGuid.ReceiverSide; | ||||
| 	public ObservableState<ImmutableDictionary<Guid, Instance>>.Receiver InstancesByGuidReceiver => instancesByGuid.ReceiverSide; | ||||
| 	 | ||||
| 	public event EventHandler<Guid>? UserUpdatedOrDeleted; | ||||
|  | ||||
| 	public void UpdateAgent(Agent agent) { | ||||
| 		agentsByGuid.PublisherSide.Publish(static (agentsByGuid, agent) => agentsByGuid.SetItem(agent.AgentGuid, agent), agent); | ||||
| 	} | ||||
| @@ -30,4 +32,8 @@ sealed class ControllerState { | ||||
| 	public void UpdateInstance(Instance instance) { | ||||
| 		instancesByGuid.PublisherSide.Publish(static (instancesByGuid, instance) => instancesByGuid.SetItem(instance.InstanceGuid, instance), instance); | ||||
| 	} | ||||
|  | ||||
| 	public void UpdateOrDeleteUser(Guid userGuid) { | ||||
| 		UserUpdatedOrDeleted?.Invoke(null, userGuid); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,18 +1,23 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Akka.Actor; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.EventLog; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Controller.Database; | ||||
| using Phantom.Controller.Database.Repositories; | ||||
| using Phantom.Controller.Services.Users.Sessions; | ||||
| using Phantom.Utils.Actor; | ||||
|  | ||||
| namespace Phantom.Controller.Services.Events;  | ||||
|  | ||||
| sealed partial class EventLogManager { | ||||
| 	private readonly ControllerState controllerState; | ||||
| 	private readonly ActorRef<EventLogDatabaseStorageActor.ICommand> databaseStorageActor; | ||||
| 	private readonly IDbContextProvider dbProvider; | ||||
| 	private readonly CancellationToken cancellationToken; | ||||
|  | ||||
| 	public EventLogManager(IActorRefFactory actorSystem, IDbContextProvider dbProvider, CancellationToken cancellationToken) { | ||||
| 	public EventLogManager(ControllerState controllerState, IActorRefFactory actorSystem, IDbContextProvider dbProvider, CancellationToken cancellationToken) { | ||||
| 		this.controllerState = controllerState; | ||||
| 		this.databaseStorageActor = actorSystem.ActorOf(EventLogDatabaseStorageActor.Factory(new EventLogDatabaseStorageActor.Init(dbProvider, cancellationToken)), "EventLogDatabaseStorage"); | ||||
| 		this.dbProvider = dbProvider; | ||||
| 		this.cancellationToken = cancellationToken; | ||||
| @@ -22,8 +27,14 @@ sealed partial class EventLogManager { | ||||
| 		databaseStorageActor.Tell(new EventLogDatabaseStorageActor.StoreEventCommand(eventGuid, utcTime, agentGuid, eventType, subjectId, extra)); | ||||
| 	} | ||||
| 	 | ||||
| 	public async Task<ImmutableArray<EventLogItem>> GetMostRecentItems(int count) { | ||||
| 	public async Task<Result<ImmutableArray<EventLogItem>, UserActionFailure>> GetMostRecentItems(LoggedInUser loggedInUser, int count) { | ||||
| 		if (!loggedInUser.CheckPermission(Permission.ViewEvents)) { | ||||
| 			return UserActionFailure.NotAuthorized; | ||||
| 		} | ||||
| 		 | ||||
| 		var accessibleAgentGuids = loggedInUser.FilterAccessibleAgentGuids(controllerState.AgentsByGuid.Keys.ToImmutableHashSet()); | ||||
| 		 | ||||
| 		await using var db = dbProvider.Lazy(); | ||||
| 		return await new EventLogRepository(db).GetMostRecentItems(count, cancellationToken); | ||||
| 		return await new EventLogRepository(db).GetMostRecentItems(accessibleAgentGuids, count, cancellationToken); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Common.Data.Minecraft; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Data.Web.Instance; | ||||
| @@ -25,6 +26,7 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> { | ||||
| 	 | ||||
| 	private InstanceConfiguration configuration; | ||||
| 	private IInstanceStatus status; | ||||
| 	private InstancePlayerCounts? playerCounts; | ||||
| 	private bool launchAutomatically; | ||||
|  | ||||
| 	private readonly ActorRef<InstanceDatabaseStorageActor.ICommand> databaseStorageActor; | ||||
| @@ -34,19 +36,20 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> { | ||||
| 		this.agentConnection = init.AgentConnection; | ||||
| 		this.cancellationToken = init.CancellationToken; | ||||
| 		 | ||||
| 		(this.instanceGuid, this.configuration, this.status, this.launchAutomatically) = init.Instance; | ||||
| 		(this.instanceGuid, this.configuration, this.status, this.playerCounts, this.launchAutomatically) = init.Instance; | ||||
|  | ||||
| 		this.databaseStorageActor = Context.ActorOf(InstanceDatabaseStorageActor.Factory(new InstanceDatabaseStorageActor.Init(instanceGuid, init.DbProvider, init.CancellationToken)), "DatabaseStorage"); | ||||
|  | ||||
| 		Receive<SetStatusCommand>(SetStatus); | ||||
| 		ReceiveAsyncAndReply<ConfigureInstanceCommand, InstanceActionResult<ConfigureInstanceResult>>(ConfigureInstance); | ||||
| 		ReceiveAsyncAndReply<LaunchInstanceCommand, InstanceActionResult<LaunchInstanceResult>>(LaunchInstance); | ||||
| 		ReceiveAsyncAndReply<StopInstanceCommand, InstanceActionResult<StopInstanceResult>>(StopInstance); | ||||
| 		ReceiveAsyncAndReply<SendCommandToInstanceCommand, InstanceActionResult<SendCommandToInstanceResult>>(SendMinecraftCommand); | ||||
| 		Receive<SetPlayerCountsCommand>(SetPlayerCounts); | ||||
| 		ReceiveAsyncAndReply<ConfigureInstanceCommand, Result<ConfigureInstanceResult, InstanceActionFailure>>(ConfigureInstance); | ||||
| 		ReceiveAsyncAndReply<LaunchInstanceCommand, Result<LaunchInstanceResult, InstanceActionFailure>>(LaunchInstance); | ||||
| 		ReceiveAsyncAndReply<StopInstanceCommand, Result<StopInstanceResult, InstanceActionFailure>>(StopInstance); | ||||
| 		ReceiveAsyncAndReply<SendCommandToInstanceCommand, Result<SendCommandToInstanceResult, InstanceActionFailure>>(SendMinecraftCommand); | ||||
| 	} | ||||
|  | ||||
| 	private void NotifyInstanceUpdated() { | ||||
| 		agentActor.Tell(new AgentActor.ReceiveInstanceDataCommand(new Instance(instanceGuid, configuration, status, launchAutomatically))); | ||||
| 		agentActor.Tell(new AgentActor.ReceiveInstanceDataCommand(new Instance(instanceGuid, configuration, status, playerCounts, launchAutomatically))); | ||||
| 	} | ||||
|  | ||||
| 	private void SetLaunchAutomatically(bool newValue) { | ||||
| @@ -56,29 +59,41 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private async Task<InstanceActionResult<TReply>> SendInstanceActionMessage<TMessage, TReply>(TMessage message) where TMessage : IMessageToAgent, ICanReply<InstanceActionResult<TReply>> { | ||||
| 		var reply = await agentConnection.Send<TMessage, InstanceActionResult<TReply>>(message, TimeSpan.FromSeconds(10), cancellationToken); | ||||
| 		return reply.DidNotReplyIfNull(); | ||||
| 	private async Task<Result<TReply, InstanceActionFailure>> SendInstanceActionMessage<TMessage, TReply>(TMessage message) where TMessage : IMessageToAgent, ICanReply<Result<TReply, InstanceActionFailure>> { | ||||
| 		var reply = await agentConnection.Send<TMessage, Result<TReply, InstanceActionFailure>>(message, TimeSpan.FromSeconds(10), cancellationToken); | ||||
| 		return reply ?? InstanceActionFailure.AgentIsNotResponding; | ||||
| 	} | ||||
|  | ||||
| 	public interface ICommand {} | ||||
|  | ||||
| 	public sealed record SetStatusCommand(IInstanceStatus Status) : ICommand; | ||||
|  | ||||
| 	public sealed record ConfigureInstanceCommand(Guid AuditLogUserGuid, Guid InstanceGuid, InstanceConfiguration Configuration, InstanceLaunchProperties LaunchProperties, bool IsCreatingInstance) : ICommand, ICanReply<InstanceActionResult<ConfigureInstanceResult>>; | ||||
|  | ||||
| 	public sealed record LaunchInstanceCommand(Guid AuditLogUserGuid) : ICommand, ICanReply<InstanceActionResult<LaunchInstanceResult>>; | ||||
| 	 | ||||
| 	public sealed record StopInstanceCommand(Guid AuditLogUserGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<InstanceActionResult<StopInstanceResult>>; | ||||
| 	public sealed record SetPlayerCountsCommand(InstancePlayerCounts? PlayerCounts) : ICommand; | ||||
|  | ||||
| 	public sealed record ConfigureInstanceCommand(Guid AuditLogUserGuid, Guid InstanceGuid, InstanceConfiguration Configuration, InstanceLaunchProperties LaunchProperties, bool IsCreatingInstance) : ICommand, ICanReply<Result<ConfigureInstanceResult, InstanceActionFailure>>; | ||||
|  | ||||
| 	public sealed record LaunchInstanceCommand(Guid AuditLogUserGuid) : ICommand, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>; | ||||
| 	 | ||||
| 	public sealed record SendCommandToInstanceCommand(Guid AuditLogUserGuid, string Command) : ICommand, ICanReply<InstanceActionResult<SendCommandToInstanceResult>>; | ||||
| 	public sealed record StopInstanceCommand(Guid AuditLogUserGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>; | ||||
| 	 | ||||
| 	public sealed record SendCommandToInstanceCommand(Guid AuditLogUserGuid, string Command) : ICommand, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>; | ||||
|  | ||||
| 	private void SetStatus(SetStatusCommand command) { | ||||
| 		status = command.Status; | ||||
| 		 | ||||
| 		if (!status.IsRunning() && status != InstanceStatus.Offline /* Guard against temporary disconnects */) { | ||||
| 			playerCounts = null; | ||||
| 		} | ||||
| 		 | ||||
| 		NotifyInstanceUpdated(); | ||||
| 	} | ||||
| 	 | ||||
| 	private void SetPlayerCounts(SetPlayerCountsCommand command) { | ||||
| 		playerCounts = command.PlayerCounts; | ||||
| 		NotifyInstanceUpdated(); | ||||
| 	} | ||||
|  | ||||
| 	private async Task<InstanceActionResult<ConfigureInstanceResult>> ConfigureInstance(ConfigureInstanceCommand command) { | ||||
| 	private async Task<Result<ConfigureInstanceResult, InstanceActionFailure>> ConfigureInstance(ConfigureInstanceCommand command) { | ||||
| 		var message = new ConfigureInstanceMessage(command.InstanceGuid, command.Configuration, command.LaunchProperties); | ||||
| 		var result = await SendInstanceActionMessage<ConfigureInstanceMessage, ConfigureInstanceResult>(message); | ||||
| 		 | ||||
| @@ -98,7 +113,7 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> { | ||||
| 		return result; | ||||
| 	} | ||||
|  | ||||
| 	private async Task<InstanceActionResult<LaunchInstanceResult>> LaunchInstance(LaunchInstanceCommand command) { | ||||
| 	private async Task<Result<LaunchInstanceResult, InstanceActionFailure>> LaunchInstance(LaunchInstanceCommand command) { | ||||
| 		var message = new LaunchInstanceMessage(instanceGuid); | ||||
| 		var result = await SendInstanceActionMessage<LaunchInstanceMessage, LaunchInstanceResult>(message); | ||||
| 		 | ||||
| @@ -110,7 +125,7 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> { | ||||
| 		return result; | ||||
| 	} | ||||
|  | ||||
| 	private async Task<InstanceActionResult<StopInstanceResult>> StopInstance(StopInstanceCommand command) { | ||||
| 	private async Task<Result<StopInstanceResult, InstanceActionFailure>> StopInstance(StopInstanceCommand command) { | ||||
| 		var message = new StopInstanceMessage(instanceGuid, command.StopStrategy); | ||||
| 		var result = await SendInstanceActionMessage<StopInstanceMessage, StopInstanceResult>(message); | ||||
| 		 | ||||
| @@ -122,7 +137,7 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> { | ||||
| 		return result; | ||||
| 	} | ||||
|  | ||||
| 	private async Task<InstanceActionResult<SendCommandToInstanceResult>> SendMinecraftCommand(SendCommandToInstanceCommand command) { | ||||
| 	private async Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> SendMinecraftCommand(SendCommandToInstanceCommand command) { | ||||
| 		var message = new SendCommandToInstanceMessage(instanceGuid, command.Command); | ||||
| 		var result = await SendInstanceActionMessage<SendCommandToInstanceMessage, SendCommandToInstanceResult>(message); | ||||
| 		 | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| using Akka.Actor; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Messages.Agent; | ||||
| using Phantom.Common.Messages.Agent.BiDirectional; | ||||
| using Phantom.Common.Messages.Agent.ToAgent; | ||||
| @@ -19,8 +18,6 @@ sealed class AgentMessageHandlerActor : ReceiveActor<IMessageToController> { | ||||
| 		return Props<IMessageToController>.Create(() => new AgentMessageHandlerActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume }); | ||||
| 	} | ||||
|  | ||||
| 	public IStash Stash { get; set; } = null!; | ||||
| 	 | ||||
| 	private readonly Guid agentGuid; | ||||
| 	private readonly RpcConnectionToClient<IMessageToAgent> connection; | ||||
| 	private readonly AgentRegistrationHandler agentRegistrationHandler; | ||||
| @@ -42,6 +39,7 @@ sealed class AgentMessageHandlerActor : ReceiveActor<IMessageToController> { | ||||
| 		Receive<AdvertiseJavaRuntimesMessage>(HandleAdvertiseJavaRuntimes); | ||||
| 		Receive<ReportAgentStatusMessage>(HandleReportAgentStatus); | ||||
| 		Receive<ReportInstanceStatusMessage>(HandleReportInstanceStatus); | ||||
| 		Receive<ReportInstancePlayerCountsMessage>(HandleReportInstancePlayerCounts); | ||||
| 		Receive<ReportInstanceEventMessage>(HandleReportInstanceEvent); | ||||
| 		Receive<InstanceOutputMessage>(HandleInstanceOutput); | ||||
| 		Receive<ReplyMessage>(HandleReply); | ||||
| @@ -77,6 +75,10 @@ sealed class AgentMessageHandlerActor : ReceiveActor<IMessageToController> { | ||||
| 		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) { | ||||
| 		message.Event.Accept(eventLogManager.CreateInstanceEventVisitor(message.EventGuid, message.UtcTime, agentGuid, message.InstanceGuid)); | ||||
| 	} | ||||
|   | ||||
| @@ -30,22 +30,31 @@ sealed class WebMessageDataUpdateSenderActor : ReceiveActor<WebMessageDataUpdate | ||||
| 		ReceiveAsync<RefreshAgentsCommand>(RefreshAgents); | ||||
| 		ReceiveAsync<RefreshInstancesCommand>(RefreshInstances); | ||||
| 		ReceiveAsync<ReceiveInstanceLogsCommand>(ReceiveInstanceLogs); | ||||
| 		ReceiveAsync<RefreshUserSessionCommand>(RefreshUserSession); | ||||
| 	} | ||||
|  | ||||
| 	protected override void PreStart() { | ||||
| 		controllerState.AgentsByGuidReceiver.Register(SelfTyped, static state => new RefreshAgentsCommand(state)); | ||||
| 		controllerState.InstancesByGuidReceiver.Register(SelfTyped, static state => new RefreshInstancesCommand(state)); | ||||
|  | ||||
| 		 | ||||
| 		controllerState.UserUpdatedOrDeleted += OnUserUpdatedOrDeleted; | ||||
| 		 | ||||
| 		instanceLogManager.LogsReceived += OnInstanceLogsReceived; | ||||
| 	} | ||||
|  | ||||
| 	protected override void PostStop() { | ||||
| 		instanceLogManager.LogsReceived -= OnInstanceLogsReceived; | ||||
| 		 | ||||
| 		controllerState.UserUpdatedOrDeleted -= OnUserUpdatedOrDeleted; | ||||
|  | ||||
| 		controllerState.AgentsByGuidReceiver.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) { | ||||
| 		selfCached.Tell(new ReceiveInstanceLogsCommand(e.InstanceGuid, e.Lines)); | ||||
| 	} | ||||
| @@ -57,6 +66,8 @@ sealed class WebMessageDataUpdateSenderActor : ReceiveActor<WebMessageDataUpdate | ||||
| 	private sealed record RefreshInstancesCommand(ImmutableDictionary<Guid, Instance> Instances) : ICommand; | ||||
| 	 | ||||
| 	private sealed record ReceiveInstanceLogsCommand(Guid InstanceGuid, ImmutableArray<string> Lines) : ICommand; | ||||
| 	 | ||||
| 	private sealed record RefreshUserSessionCommand(Guid UserGuid) : ICommand; | ||||
|  | ||||
| 	private Task RefreshAgents(RefreshAgentsCommand command) { | ||||
| 		return connection.Send(new RefreshAgentsMessage(command.Agents.Values.ToImmutableArray())); | ||||
| @@ -69,4 +80,8 @@ sealed class WebMessageDataUpdateSenderActor : ReceiveActor<WebMessageDataUpdate | ||||
| 	private Task ReceiveInstanceLogs(ReceiveInstanceLogsCommand command) { | ||||
| 		return connection.Send(new InstanceOutputMessage(command.InstanceGuid, command.Lines)); | ||||
| 	} | ||||
| 	 | ||||
| 	private Task RefreshUserSession(RefreshUserSessionCommand command) { | ||||
| 		return connection.Send(new RefreshUserSessionMessage(command.UserGuid)); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Java; | ||||
| using Phantom.Common.Data.Minecraft; | ||||
| using Phantom.Common.Data.Replies; | ||||
| @@ -14,6 +15,7 @@ using Phantom.Controller.Services.Agents; | ||||
| using Phantom.Controller.Services.Events; | ||||
| using Phantom.Controller.Services.Instances; | ||||
| using Phantom.Controller.Services.Users; | ||||
| using Phantom.Controller.Services.Users.Sessions; | ||||
| using Phantom.Utils.Actor; | ||||
| using Phantom.Utils.Rpc.Runtime; | ||||
|  | ||||
| @@ -66,25 +68,27 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> { | ||||
|  | ||||
| 		var senderActorInit = new WebMessageDataUpdateSenderActor.Init(connection, controllerState, init.InstanceLogManager); | ||||
| 		Context.ActorOf(WebMessageDataUpdateSenderActor.Factory(senderActorInit), "DataUpdateSender"); | ||||
| 		 | ||||
|  | ||||
| 		ReceiveAsync<RegisterWebMessage>(HandleRegisterWeb); | ||||
| 		Receive<UnregisterWebMessage>(HandleUnregisterWeb); | ||||
| 		ReceiveAndReplyLater<LogInMessage, Optional<LogInSuccess>>(HandleLogIn); | ||||
| 		Receive<LogOutMessage>(HandleLogOut); | ||||
| 		ReceiveAndReply<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(GetAuthenticatedUser); | ||||
| 		ReceiveAndReplyLater<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(HandleCreateOrUpdateAdministratorUser); | ||||
| 		ReceiveAndReplyLater<CreateUserMessage, CreateUserResult>(HandleCreateUser); | ||||
| 		ReceiveAndReplyLater<CreateUserMessage, Result<CreateUserResult, UserActionFailure>>(HandleCreateUser); | ||||
| 		ReceiveAndReplyLater<GetUsersMessage, ImmutableArray<UserInfo>>(HandleGetUsers); | ||||
| 		ReceiveAndReplyLater<GetRolesMessage, ImmutableArray<RoleInfo>>(HandleGetRoles); | ||||
| 		ReceiveAndReplyLater<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(HandleGetUserRoles); | ||||
| 		ReceiveAndReplyLater<ChangeUserRolesMessage, ChangeUserRolesResult>(HandleChangeUserRoles); | ||||
| 		ReceiveAndReplyLater<DeleteUserMessage, DeleteUserResult>(HandleDeleteUser); | ||||
| 		ReceiveAndReplyLater<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(HandleCreateOrUpdateInstance); | ||||
| 		ReceiveAndReplyLater<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(HandleLaunchInstance); | ||||
| 		ReceiveAndReplyLater<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(HandleStopInstance); | ||||
| 		ReceiveAndReplyLater<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(HandleSendCommandToInstance); | ||||
| 		ReceiveAndReplyLater<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(HandleGetMinecraftVersions);  | ||||
| 		ReceiveAndReplyLater<ChangeUserRolesMessage, Result<ChangeUserRolesResult, UserActionFailure>>(HandleChangeUserRoles); | ||||
| 		ReceiveAndReplyLater<DeleteUserMessage, Result<DeleteUserResult, UserActionFailure>>(HandleDeleteUser); | ||||
| 		ReceiveAndReplyLater<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(HandleCreateOrUpdateInstance); | ||||
| 		ReceiveAndReplyLater<LaunchInstanceMessage, Result<LaunchInstanceResult, UserInstanceActionFailure>>(HandleLaunchInstance); | ||||
| 		ReceiveAndReplyLater<StopInstanceMessage, Result<StopInstanceResult, UserInstanceActionFailure>>(HandleStopInstance); | ||||
| 		ReceiveAndReplyLater<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>(HandleSendCommandToInstance); | ||||
| 		ReceiveAndReplyLater<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(HandleGetMinecraftVersions); | ||||
| 		ReceiveAndReply<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(HandleGetAgentJavaRuntimes); | ||||
| 		ReceiveAndReplyLater<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(HandleGetAuditLog); | ||||
| 		ReceiveAndReplyLater<GetEventLogMessage, ImmutableArray<EventLogItem>>(HandleGetEventLog); | ||||
| 		ReceiveAndReplyLater<LogInMessage, LogInSuccess?>(HandleLogIn); | ||||
| 		ReceiveAndReplyLater<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(HandleGetAuditLog); | ||||
| 		ReceiveAndReplyLater<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(HandleGetEventLog); | ||||
| 		Receive<ReplyMessage>(HandleReply); | ||||
| 	} | ||||
|  | ||||
| @@ -96,12 +100,24 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> { | ||||
| 		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) { | ||||
| 		return userManager.CreateOrUpdateAdministrator(message.Username, message.Password); | ||||
| 	} | ||||
|  | ||||
| 	private Task<CreateUserResult> HandleCreateUser(CreateUserMessage message) { | ||||
| 		return userManager.Create(message.LoggedInUserGuid, message.Username, message.Password); | ||||
| 	private Task<Result<CreateUserResult, UserActionFailure>> HandleCreateUser(CreateUserMessage message) { | ||||
| 		return userManager.Create(userLoginManager.GetLoggedInUser(message.AuthToken), message.Username, message.Password); | ||||
| 	} | ||||
|  | ||||
| 	private Task<ImmutableArray<UserInfo>> HandleGetUsers(GetUsersMessage message) { | ||||
| @@ -116,28 +132,48 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> { | ||||
| 		return userRoleManager.GetUserRoles(message.UserGuids); | ||||
| 	} | ||||
|  | ||||
| 	private Task<ChangeUserRolesResult> HandleChangeUserRoles(ChangeUserRolesMessage message) { | ||||
| 		return userRoleManager.ChangeUserRoles(message.LoggedInUserGuid, message.SubjectUserGuid, message.AddToRoleGuids, message.RemoveFromRoleGuids); | ||||
| 	private Task<Result<ChangeUserRolesResult, UserActionFailure>> HandleChangeUserRoles(ChangeUserRolesMessage message) { | ||||
| 		return userRoleManager.ChangeUserRoles(userLoginManager.GetLoggedInUser(message.AuthToken), message.SubjectUserGuid, message.AddToRoleGuids, message.RemoveFromRoleGuids); | ||||
| 	} | ||||
|  | ||||
| 	private Task<DeleteUserResult> HandleDeleteUser(DeleteUserMessage message) { | ||||
| 		return userManager.DeleteByGuid(message.LoggedInUserGuid, message.SubjectUserGuid); | ||||
| 	private Task<Result<DeleteUserResult, UserActionFailure>> HandleDeleteUser(DeleteUserMessage message) { | ||||
| 		return userManager.DeleteByGuid(userLoginManager.GetLoggedInUser(message.AuthToken), message.SubjectUserGuid); | ||||
| 	} | ||||
|  | ||||
| 	private Task<InstanceActionResult<CreateOrUpdateInstanceResult>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message) { | ||||
| 		return agentManager.DoInstanceAction<AgentActor.CreateOrUpdateInstanceCommand, CreateOrUpdateInstanceResult>(message.Configuration.AgentGuid, new AgentActor.CreateOrUpdateInstanceCommand(message.LoggedInUserGuid, message.InstanceGuid, message.Configuration)); | ||||
| 	private Task<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message) { | ||||
| 		return agentManager.DoInstanceAction<AgentActor.CreateOrUpdateInstanceCommand, CreateOrUpdateInstanceResult>( | ||||
| 			Permission.CreateInstances, | ||||
| 			message.AuthToken, | ||||
| 			message.Configuration.AgentGuid, | ||||
| 			loggedInUserGuid => new AgentActor.CreateOrUpdateInstanceCommand(loggedInUserGuid, message.InstanceGuid, message.Configuration) | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	private Task<InstanceActionResult<LaunchInstanceResult>> HandleLaunchInstance(LaunchInstanceMessage message) { | ||||
| 		return agentManager.DoInstanceAction<AgentActor.LaunchInstanceCommand, LaunchInstanceResult>(message.AgentGuid, new AgentActor.LaunchInstanceCommand(message.InstanceGuid, message.LoggedInUserGuid)); | ||||
| 	private Task<Result<LaunchInstanceResult, UserInstanceActionFailure>> HandleLaunchInstance(LaunchInstanceMessage message) { | ||||
| 		return agentManager.DoInstanceAction<AgentActor.LaunchInstanceCommand, LaunchInstanceResult>( | ||||
| 			Permission.ControlInstances, | ||||
| 			message.AuthToken, | ||||
| 			message.AgentGuid, | ||||
| 			loggedInUserGuid => new AgentActor.LaunchInstanceCommand(loggedInUserGuid, message.InstanceGuid) | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	private Task<InstanceActionResult<StopInstanceResult>> HandleStopInstance(StopInstanceMessage message) { | ||||
| 		return agentManager.DoInstanceAction<AgentActor.StopInstanceCommand, StopInstanceResult>(message.AgentGuid, new AgentActor.StopInstanceCommand(message.InstanceGuid, message.LoggedInUserGuid, message.StopStrategy)); | ||||
| 	private Task<Result<StopInstanceResult, UserInstanceActionFailure>> HandleStopInstance(StopInstanceMessage message) { | ||||
| 		return agentManager.DoInstanceAction<AgentActor.StopInstanceCommand, StopInstanceResult>( | ||||
| 			Permission.ControlInstances, | ||||
| 			message.AuthToken, | ||||
| 			message.AgentGuid, | ||||
| 			loggedInUserGuid => new AgentActor.StopInstanceCommand(loggedInUserGuid, message.InstanceGuid, message.StopStrategy) | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	private Task<InstanceActionResult<SendCommandToInstanceResult>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) { | ||||
| 		return agentManager.DoInstanceAction<AgentActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(message.AgentGuid, new AgentActor.SendCommandToInstanceCommand(message.InstanceGuid, message.LoggedInUserGuid, message.Command)); | ||||
| 	private Task<Result<SendCommandToInstanceResult, UserInstanceActionFailure>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) { | ||||
| 		return agentManager.DoInstanceAction<AgentActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>( | ||||
| 			Permission.ControlInstances, | ||||
| 			message.AuthToken, | ||||
| 			message.AgentGuid, | ||||
| 			loggedInUserGuid => new AgentActor.SendCommandToInstanceCommand(loggedInUserGuid, message.InstanceGuid, message.Command) | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	private Task<ImmutableArray<MinecraftVersion>> HandleGetMinecraftVersions(GetMinecraftVersionsMessage message) { | ||||
| @@ -148,16 +184,12 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> { | ||||
| 		return controllerState.AgentJavaRuntimesByGuid; | ||||
| 	} | ||||
|  | ||||
| 	private Task<ImmutableArray<AuditLogItem>> HandleGetAuditLog(GetAuditLogMessage message) { | ||||
| 		return auditLogManager.GetMostRecentItems(message.Count); | ||||
| 	private Task<Result<ImmutableArray<AuditLogItem>, UserActionFailure>> HandleGetAuditLog(GetAuditLogMessage message) { | ||||
| 		return auditLogManager.GetMostRecentItems(userLoginManager.GetLoggedInUser(message.AuthToken), message.Count); | ||||
| 	} | ||||
|  | ||||
| 	private Task<ImmutableArray<EventLogItem>> HandleGetEventLog(GetEventLogMessage message) { | ||||
| 		return eventLogManager.GetMostRecentItems(message.Count); | ||||
| 	} | ||||
|  | ||||
| 	private Task<LogInSuccess?> HandleLogIn(LogInMessage message) { | ||||
| 		return userLoginManager.LogIn(message.Username, message.Password); | ||||
| 	private Task<Result<ImmutableArray<EventLogItem>, UserActionFailure>> HandleGetEventLog(GetEventLogMessage message) { | ||||
| 		return eventLogManager.GetMostRecentItems(userLoginManager.GetLoggedInUser(message.AuthToken), message.Count); | ||||
| 	} | ||||
|  | ||||
| 	private void HandleReply(ReplyMessage message) { | ||||
|   | ||||
| @@ -7,6 +7,7 @@ using Phantom.Controller.Services.Agents; | ||||
| using Phantom.Controller.Services.Events; | ||||
| using Phantom.Controller.Services.Instances; | ||||
| using Phantom.Controller.Services.Users; | ||||
| using Phantom.Controller.Services.Users.Sessions; | ||||
| using Phantom.Utils.Actor; | ||||
| using Phantom.Utils.Logging; | ||||
| using Phantom.Utils.Rpc.Runtime; | ||||
|   | ||||
| @@ -1,7 +1,10 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.AuditLog; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Controller.Database; | ||||
| using Phantom.Controller.Database.Repositories; | ||||
| using Phantom.Controller.Services.Users.Sessions; | ||||
|  | ||||
| namespace Phantom.Controller.Services.Users;  | ||||
|  | ||||
| @@ -12,7 +15,11 @@ sealed class AuditLogManager { | ||||
| 		this.dbProvider = dbProvider; | ||||
| 	} | ||||
|  | ||||
| 	public async Task<ImmutableArray<AuditLogItem>> GetMostRecentItems(int count) { | ||||
| 	public async Task<Result<ImmutableArray<AuditLogItem>, UserActionFailure>> GetMostRecentItems(LoggedInUser loggedInUser, int count) { | ||||
| 		if (!loggedInUser.CheckPermission(Permission.ViewAudit)) { | ||||
| 			return UserActionFailure.NotAuthorized; | ||||
| 		} | ||||
| 		 | ||||
| 		await using var db = dbProvider.Lazy(); | ||||
| 		return await new AuditLogRepository(db).GetMostRecentItems(count, CancellationToken.None); | ||||
| 	} | ||||
|   | ||||
| @@ -36,34 +36,6 @@ 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) { | ||||
| 		return allPermissions.Select(static permission => permission.Id).Except(existingPermissionIds).Order().ToImmutableArray(); | ||||
| 	} | ||||
|   | ||||
| @@ -0,0 +1,29 @@ | ||||
| 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 _); | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,20 @@ | ||||
| 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; | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,140 @@ | ||||
| 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(); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,34 +0,0 @@ | ||||
| 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); | ||||
| 	} | ||||
| } | ||||
| @@ -1,8 +1,10 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Controller.Database; | ||||
| using Phantom.Controller.Database.Entities; | ||||
| using Phantom.Controller.Database.Repositories; | ||||
| using Phantom.Controller.Services.Users.Sessions; | ||||
| using Phantom.Utils.Logging; | ||||
| using Serilog; | ||||
|  | ||||
| @@ -11,9 +13,13 @@ namespace Phantom.Controller.Services.Users; | ||||
| sealed class UserManager { | ||||
| 	private static readonly ILogger Logger = PhantomLogger.Create<UserManager>(); | ||||
|  | ||||
| 	private readonly AuthenticatedUserCache authenticatedUserCache; | ||||
| 	private readonly ControllerState controllerState; | ||||
| 	private readonly IDbContextProvider dbProvider; | ||||
|  | ||||
| 	public UserManager(IDbContextProvider dbProvider) { | ||||
| 	public UserManager(AuthenticatedUserCache authenticatedUserCache, ControllerState controllerState, IDbContextProvider dbProvider) { | ||||
| 		this.authenticatedUserCache = authenticatedUserCache; | ||||
| 		this.controllerState = controllerState; | ||||
| 		this.dbProvider = dbProvider; | ||||
| 	} | ||||
|  | ||||
| @@ -54,9 +60,8 @@ sealed class UserManager { | ||||
| 				} | ||||
| 			} | ||||
| 			else { | ||||
| 				var result = userRepository.SetUserPassword(user, password); | ||||
| 				if (!result) { | ||||
| 					return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.UpdatingFailed(result.Error); | ||||
| 				if (userRepository.SetUserPassword(user, password).TryGetError(out var error)) { | ||||
| 					return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.UpdatingFailed(error); | ||||
| 				} | ||||
|  | ||||
| 				auditLogWriter.AdministratorUserModified(user); | ||||
| @@ -86,10 +91,14 @@ sealed class UserManager { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public async Task<CreateUserResult> Create(Guid loggedInUserGuid, string username, string password) { | ||||
| 	public async Task<Result<CreateUserResult, UserActionFailure>> Create(LoggedInUser loggedInUser, string username, string password) { | ||||
| 		if (!loggedInUser.CheckPermission(Permission.EditUsers)) { | ||||
| 			return UserActionFailure.NotAuthorized; | ||||
| 		} | ||||
| 		 | ||||
| 		await using var db = dbProvider.Lazy(); | ||||
| 		var userRepository = new UserRepository(db); | ||||
| 		var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUserGuid); | ||||
| 		var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUser.Guid); | ||||
|  | ||||
| 		try { | ||||
| 			var result = await userRepository.CreateUser(username, password); | ||||
| @@ -110,7 +119,11 @@ sealed class UserManager { | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	public async Task<DeleteUserResult> DeleteByGuid(Guid loggedInUserGuid, Guid userGuid) { | ||||
| 	public async Task<Result<DeleteUserResult, UserActionFailure>> DeleteByGuid(LoggedInUser loggedInUser, Guid userGuid) { | ||||
| 		if (!loggedInUser.CheckPermission(Permission.EditUsers)) { | ||||
| 			return UserActionFailure.NotAuthorized; | ||||
| 		} | ||||
| 		 | ||||
| 		await using var db = dbProvider.Lazy(); | ||||
| 		var userRepository = new UserRepository(db); | ||||
|  | ||||
| @@ -119,11 +132,17 @@ sealed class UserManager { | ||||
| 			return DeleteUserResult.NotFound; | ||||
| 		} | ||||
|  | ||||
| 		var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUserGuid); | ||||
| 		authenticatedUserCache.Remove(userGuid); | ||||
| 		 | ||||
| 		var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUser.Guid); | ||||
| 		try { | ||||
| 			userRepository.DeleteUser(user); | ||||
| 			auditLogWriter.UserDeleted(user); | ||||
| 			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); | ||||
| 			return DeleteUserResult.Deleted; | ||||
|   | ||||
| @@ -1,7 +1,9 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Controller.Database; | ||||
| using Phantom.Controller.Database.Repositories; | ||||
| using Phantom.Controller.Services.Users.Sessions; | ||||
| using Phantom.Utils.Logging; | ||||
| using Serilog; | ||||
|  | ||||
| @@ -9,10 +11,14 @@ namespace Phantom.Controller.Services.Users; | ||||
|  | ||||
| sealed class UserRoleManager { | ||||
| 	private static readonly ILogger Logger = PhantomLogger.Create<UserRoleManager>(); | ||||
| 	 | ||||
|  | ||||
| 	private readonly AuthenticatedUserCache authenticatedUserCache; | ||||
| 	private readonly ControllerState controllerState; | ||||
| 	private readonly IDbContextProvider dbProvider; | ||||
| 	 | ||||
| 	public UserRoleManager(IDbContextProvider dbProvider) { | ||||
| 	public UserRoleManager(AuthenticatedUserCache authenticatedUserCache, ControllerState controllerState, IDbContextProvider dbProvider) { | ||||
| 		this.authenticatedUserCache = authenticatedUserCache; | ||||
| 		this.controllerState = controllerState; | ||||
| 		this.dbProvider = dbProvider; | ||||
| 	} | ||||
|  | ||||
| @@ -21,7 +27,11 @@ sealed class UserRoleManager { | ||||
| 		return await new UserRoleRepository(db).GetRoleGuidsByUserGuid(userGuids); | ||||
| 	} | ||||
|  | ||||
| 	public async Task<ChangeUserRolesResult> ChangeUserRoles(Guid loggedInUserGuid, Guid subjectUserGuid, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids) { | ||||
| 	public async Task<Result<ChangeUserRolesResult, UserActionFailure>> ChangeUserRoles(LoggedInUser loggedInUser, Guid subjectUserGuid, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids) { | ||||
| 		if (!loggedInUser.CheckPermission(Permission.EditUsers)) { | ||||
| 			return UserActionFailure.NotAuthorized; | ||||
| 		} | ||||
| 		 | ||||
| 		await using var db = dbProvider.Lazy(); | ||||
| 		var userRepository = new UserRepository(db); | ||||
| 		 | ||||
| @@ -32,7 +42,7 @@ sealed class UserRoleManager { | ||||
|  | ||||
| 		var roleRepository = new RoleRepository(db); | ||||
| 		var userRoleRepository = new UserRoleRepository(db); | ||||
| 		var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUserGuid); | ||||
| 		var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUser.Guid); | ||||
| 		 | ||||
| 		var rolesByGuid = await roleRepository.GetByGuids(addToRoleGuids.Union(removeFromRoleGuids)); | ||||
| 		 | ||||
| @@ -41,7 +51,7 @@ sealed class UserRoleManager { | ||||
| 		 | ||||
| 		var removedFromRoleGuids = ImmutableHashSet.CreateBuilder<Guid>(); | ||||
| 		var removedFromRoleNames = new List<string>(); | ||||
|          | ||||
|  | ||||
| 		try { | ||||
| 			foreach (var roleGuid in addToRoleGuids) { | ||||
| 				if (rolesByGuid.TryGetValue(roleGuid, out var role)) { | ||||
| @@ -62,6 +72,9 @@ sealed class UserRoleManager { | ||||
| 			auditLogWriter.UserRolesChanged(user, addedToRoleNames, removedFromRoleNames); | ||||
| 			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); | ||||
| 			return new ChangeUserRolesResult(addedToRoleGuids.ToImmutable(), removedFromRoleGuids.ToImmutable()); | ||||
| 		} catch (Exception e) { | ||||
|   | ||||
| @@ -10,7 +10,6 @@ using Phantom.Utils.Logging; | ||||
| using Phantom.Utils.Rpc; | ||||
| using Phantom.Utils.Rpc.Runtime; | ||||
| using Phantom.Utils.Runtime; | ||||
| using Phantom.Utils.Tasks; | ||||
|  | ||||
| var shutdownCancellationTokenSource = new CancellationTokenSource(); | ||||
| var shutdownCancellationToken = shutdownCancellationTokenSource.Token; | ||||
| @@ -64,14 +63,12 @@ try { | ||||
| 		return new RpcConfiguration(serviceName, host, port, connectionKey.Certificate); | ||||
| 	} | ||||
|  | ||||
| 	var rpcTaskManager = new TaskManager(PhantomLogger.Create<TaskManager>("Rpc")); | ||||
| 	try { | ||||
| 		await Task.WhenAll( | ||||
| 			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) | ||||
| 		); | ||||
| 	} finally { | ||||
| 		await rpcTaskManager.Stop(); | ||||
| 		NetMQConfig.Cleanup(); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -27,7 +27,7 @@ WORKDIR /data | ||||
| 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:17-jre /opt/java/openjdk /opt/java/17 | ||||
| COPY --from=eclipse-temurin:20-jre /opt/java/openjdk /opt/java/20 | ||||
| COPY --from=eclipse-temurin:21-jre /opt/java/openjdk /opt/java/21 | ||||
|  | ||||
| ARG DEBIAN_FRONTEND=noninteractive | ||||
|  | ||||
|   | ||||
| @@ -1,10 +1,12 @@ | ||||
| using Akka.Actor; | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using Akka.Actor; | ||||
| using Akka.Configuration; | ||||
| using Akka.Dispatch; | ||||
| using Akka.Dispatch.MessageQueues; | ||||
|  | ||||
| namespace Phantom.Utils.Actor.Mailbox; | ||||
|  | ||||
| [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] | ||||
| public sealed class UnboundedJumpAheadMailbox : MailboxType, IProducesMessageQueue<UnboundedJumpAheadMessageQueue> { | ||||
| 	public const string Name = "unbounded-jump-ahead-mailbox"; | ||||
| 	 | ||||
|   | ||||
| @@ -17,6 +17,10 @@ public static class TaskExtensions { | ||||
| 		return task.ContinueOnActor(result => mapper(result, arg)); | ||||
| 	} | ||||
| 	 | ||||
| 	public static Task<TResult> ContinueOnActor<TSource, TArg1, TArg2, TResult>(this Task<TSource> task, Func<TSource, TArg1, TArg2, TResult> mapper, TArg1 arg1, TArg2 arg2) { | ||||
| 		return task.ContinueOnActor(result => mapper(result, arg1, arg2)); | ||||
| 	} | ||||
| 	 | ||||
| 	private static Task<TResult> MapResult<TSource, TResult>(Task<TSource> task, Func<TSource, TResult> mapper, TaskCompletionSource<TResult> completionSource) { | ||||
| 		if (task.IsFaulted) { | ||||
| 			completionSource.SetException(task.Exception.InnerExceptions); | ||||
|   | ||||
| @@ -9,7 +9,7 @@ public sealed class RpcConnectionToClient<TMessageBase> : RpcConnection<TMessage | ||||
| 	private readonly uint routingId; | ||||
|  | ||||
| 	internal event EventHandler<RpcClientConnectionClosedEventArgs>? Closed; | ||||
| 	public bool IsClosed { get; private set; } | ||||
| 	private bool isClosed; | ||||
|  | ||||
| 	internal RpcConnectionToClient(ServerSocket socket, uint routingId, MessageRegistry<TMessageBase> messageRegistry, MessageReplyTracker replyTracker) : base(messageRegistry, replyTracker) { | ||||
| 		this.socket = socket; | ||||
| @@ -24,8 +24,8 @@ public sealed class RpcConnectionToClient<TMessageBase> : RpcConnection<TMessage | ||||
| 		bool hasClosed = false; | ||||
| 		 | ||||
| 		lock (this) { | ||||
| 			if (!IsClosed) { | ||||
| 				IsClosed = true; | ||||
| 			if (!isClosed) { | ||||
| 				isClosed = true; | ||||
| 				hasClosed = true; | ||||
| 			} | ||||
| 		} | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user