mirror of
				https://github.com/chylex/Minecraft-Phantom-Panel.git
				synced 2025-10-31 20:17:16 +01:00 
			
		
		
		
	Compare commits
	
		
			76 Commits
		
	
	
		
			experiment
			...
			d2e7f4f876
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| d2e7f4f876 | |||
| c4cf45776d | |||
| 3c10e1a8f9 | |||
| f4aec6f11d | |||
| c8a2a539e8 | |||
| b1758fb2bb | |||
| 2cc7975193 | |||
| 6472134f9a | |||
| c0243dc749 | |||
| d57546bb71 | |||
| ab20e46af2 | |||
| d4f244a3db | |||
| e2ed060044 | |||
| e62bd430b9 | |||
| c618a8d045 | |||
| 8a87d7aff6 | |||
| 8ac8971f7f | |||
| c582aefb05 | |||
| fd0097214b | |||
| 1c5940dd23 | |||
| 55b643c513 | |||
| 36dbc6f984 | |||
| 205b1f0697 | |||
| 1c2c32c2e6 | |||
| 0e6d506cb4 | |||
| 8d3e4442d7 | |||
| 59cf71e3e1 | |||
| 98ec0e001c | |||
| 4728820b0f | |||
| 663aa8fded | |||
| bcb53528b9 | |||
| 69f3fbcbf4 | |||
| f5e01716ed | |||
| 751d914d12 | |||
| bcfc2c8240 | |||
| e699513036 | |||
| 5f4e7f0280 | |||
| 4725ce27dd | |||
| dbd57a1ee0 | |||
| 3b19cbd985 | |||
| 24e08f1943 | |||
| ff5d31bf05 | |||
| de22e5695f | |||
| dbba829e21 | |||
| 4fc5214418 | |||
| 524b0a4cd9 | |||
| 0018b1f9b4 | |||
| 3d2b0d5373 | |||
| 02e121d6ed | |||
| f10a754efb | |||
| e51844d798 | |||
| 1c96afaa3c | |||
| cde29e990d | |||
| 7a495888aa | |||
| c4b1d3c920 | |||
| 2b661fd170 | |||
| ae4f4af2eb | |||
| bfb60219ea | |||
| 3497f73d59 | |||
| e41be61945 | |||
| 8c9925921c | |||
| 315f6b181c | |||
| 46446ea5d5 | |||
| 0b51a4509e | |||
| f880a46887 | |||
| 3b34ae1eca | |||
| d2b085ec15 | |||
| e1cfb36bd1 | |||
| e229e3dccf | |||
| adf0dd6853 | |||
| adea2021ba | |||
| 9e47351799 | |||
| 6ded2575cb | |||
| 7b39ff2b2e | |||
| 32ec2cc9db | |||
| 640731634b | 
							
								
								
									
										9
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| # Ignore hidden files | ||||
| .* | ||||
|  | ||||
| # Include .git for build version information | ||||
| !.git | ||||
|  | ||||
| # Not needed for building | ||||
| AddMigration.* | ||||
| *.DotSettings.user | ||||
| @@ -5,13 +5,13 @@ | ||||
|     <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent1" /> | ||||
|     <option name="PASS_PARENT_ENVS" value="1" /> | ||||
|     <envs> | ||||
|       <env name="AGENT_KEY" value="JXBQQYG5T267RQS75MXWBTCJZY5CKTCCGQY22MCZPHSQQSJYCHH2NG2TCNXQY6TBSXM9NQDRS2CMX" /> | ||||
|       <env name="AGENT_NAME" value="Agent 1" /> | ||||
|       <env name="ALLOWED_RCON_PORTS" value="25575,27000,27001" /> | ||||
|       <env name="ALLOWED_SERVER_PORTS" value="25565,26000,26001" /> | ||||
|       <env name="JAVA_SEARCH_PATH" value="~/.jdks" /> | ||||
|       <env name="MAX_INSTANCES" value="3" /> | ||||
|       <env name="MAX_MEMORY" value="12G" /> | ||||
|       <env name="SERVER_AUTH_TOKEN_FILE" value="./secrets/agent.token" /> | ||||
|       <env name="SERVER_HOST" value="localhost" /> | ||||
|     </envs> | ||||
|     <option name="USE_EXTERNAL_CONSOLE" value="0" /> | ||||
|   | ||||
| @@ -5,13 +5,13 @@ | ||||
|     <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent2" /> | ||||
|     <option name="PASS_PARENT_ENVS" value="1" /> | ||||
|     <envs> | ||||
|       <env name="AGENT_KEY" value="JXBQQYG5T267RQS75MXWBTCJZY5CKTCCGQY22MCZPHSQQSJYCHH2NG2TCNXQY6TBSXM9NQDRS2CMX" /> | ||||
|       <env name="AGENT_NAME" value="Agent 2" /> | ||||
|       <env name="ALLOWED_RCON_PORTS" value="27002-27006" /> | ||||
|       <env name="ALLOWED_SERVER_PORTS" value="26002-26006" /> | ||||
|       <env name="JAVA_SEARCH_PATH" value="~/.jdks" /> | ||||
|       <env name="MAX_INSTANCES" value="5" /> | ||||
|       <env name="MAX_MEMORY" value="10G" /> | ||||
|       <env name="SERVER_AUTH_TOKEN_FILE" value="./secrets/agent.token" /> | ||||
|       <env name="SERVER_HOST" value="localhost" /> | ||||
|     </envs> | ||||
|     <option name="USE_EXTERNAL_CONSOLE" value="0" /> | ||||
|   | ||||
| @@ -5,13 +5,13 @@ | ||||
|     <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent3" /> | ||||
|     <option name="PASS_PARENT_ENVS" value="1" /> | ||||
|     <envs> | ||||
|       <env name="AGENT_KEY" value="JXBQQYG5T267RQS75MXWBTCJZY5CKTCCGQY22MCZPHSQQSJYCHH2NG2TCNXQY6TBSXM9NQDRS2CMX" /> | ||||
|       <env name="AGENT_NAME" value="Agent 3" /> | ||||
|       <env name="ALLOWED_RCON_PORTS" value="27007" /> | ||||
|       <env name="ALLOWED_SERVER_PORTS" value="26007" /> | ||||
|       <env name="JAVA_SEARCH_PATH" value="~/.jdks" /> | ||||
|       <env name="MAX_INSTANCES" value="1" /> | ||||
|       <env name="MAX_MEMORY" value="2560M" /> | ||||
|       <env name="SERVER_AUTH_TOKEN_FILE" value="./secrets/agent.token" /> | ||||
|       <env name="SERVER_HOST" value="localhost" /> | ||||
|     </envs> | ||||
|     <option name="USE_EXTERNAL_CONSOLE" value="0" /> | ||||
|   | ||||
							
								
								
									
										2
									
								
								.workdir/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.workdir/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,2 +0,0 @@ | ||||
| /Agent*/data | ||||
| /Agent*/temp | ||||
							
								
								
									
										2
									
								
								.workdir/Agent1/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.workdir/Agent1/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| * | ||||
| !.gitignore | ||||
| @@ -1 +0,0 @@ | ||||
| tLkn<EFBFBD>Z<EFBFBD><EFBFBD>}ы<><D18B>|~2<><32>><3E><>р<EFBFBD><D180>O*<2A>	p | ||||
| @@ -1 +0,0 @@ | ||||
| TH7Z2MJKMR975N6HFBKQP9WQFMWWN5 | ||||
							
								
								
									
										2
									
								
								.workdir/Agent2/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.workdir/Agent2/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| * | ||||
| !.gitignore | ||||
| @@ -1 +0,0 @@ | ||||
| tLkn<EFBFBD>Z<EFBFBD><EFBFBD>}ы<><D18B>|~2<><32>><3E><>р<EFBFBD><D180>O*<2A>	p | ||||
| @@ -1 +0,0 @@ | ||||
| TH7Z2MJKMR975N6HFBKQP9WQFMWWN5 | ||||
							
								
								
									
										2
									
								
								.workdir/Agent3/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.workdir/Agent3/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| * | ||||
| !.gitignore | ||||
| @@ -1 +0,0 @@ | ||||
| tLkn<EFBFBD>Z<EFBFBD><EFBFBD>}ы<><D18B>|~2<><32>><3E><>р<EFBFBD><D180>O*<2A>	p | ||||
| @@ -1 +0,0 @@ | ||||
| TH7Z2MJKMR975N6HFBKQP9WQFMWWN5 | ||||
							
								
								
									
										1
									
								
								.workdir/Server/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.workdir/Server/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| /keys | ||||
										
											Binary file not shown.
										
									
								
							| @@ -1 +0,0 @@ | ||||
| TH7Z2MJKMR975N6HFBKQP9WQFMWWN5 | ||||
| @@ -1 +1 @@ | ||||
| +<2B><><EFBFBD><EFBFBD><<3C>f:<3A>bJ"e<18>ބ<D7B8><1F><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> | ||||
| <EFBFBD>Z<EFBFBD>t<>MPI<49>GMZ<4D><5A><EFBFBD><EFBFBD>kN<6B>VF1X<EFBFBD><EFBFBD>p | ||||
| @@ -0,0 +1,9 @@ | ||||
| namespace Phantom.Agent.Minecraft.Command;  | ||||
|  | ||||
| public static class MinecraftCommand { | ||||
| 	public const string Stop = "stop"; | ||||
|  | ||||
| 	public static string Say(string message) { | ||||
| 		return "say " + message; | ||||
| 	} | ||||
| } | ||||
| @@ -1,4 +1,5 @@ | ||||
| using Phantom.Agent.Minecraft.Java; | ||||
| using System.Collections.Immutable; | ||||
| using Phantom.Agent.Minecraft.Java; | ||||
| using Phantom.Agent.Minecraft.Properties; | ||||
|  | ||||
| namespace Phantom.Agent.Minecraft.Instance; | ||||
| @@ -6,6 +7,7 @@ namespace Phantom.Agent.Minecraft.Instance; | ||||
| public sealed record InstanceProperties( | ||||
| 	Guid JavaRuntimeGuid, | ||||
| 	JvmProperties JvmProperties, | ||||
| 	ImmutableArray<string> JvmArguments, | ||||
| 	string InstanceFolder, | ||||
| 	string ServerVersion, | ||||
| 	ServerProperties ServerProperties | ||||
|   | ||||
| @@ -7,7 +7,7 @@ using Serilog; | ||||
| namespace Phantom.Agent.Minecraft.Java; | ||||
|  | ||||
| public sealed class JavaRuntimeDiscovery { | ||||
| 	private static readonly ILogger Logger = PhantomLogger.Create(typeof(JavaRuntimeDiscovery)); | ||||
| 	private static readonly ILogger Logger = PhantomLogger.Create(nameof(JavaRuntimeDiscovery)); | ||||
|  | ||||
| 	public static string? GetSystemSearchPath() { | ||||
| 		const string LinuxJavaPath = "/usr/lib/jvm"; | ||||
| @@ -38,28 +38,38 @@ public sealed class JavaRuntimeDiscovery { | ||||
| 			AttributesToSkip = FileAttributes.Hidden | FileAttributes.ReparsePoint | FileAttributes.System | ||||
| 		}).Order()) { | ||||
| 			var javaExecutablePath = Paths.NormalizeSlashes(Path.Combine(binFolderPath, javaExecutableName)); | ||||
| 			if (File.Exists(javaExecutablePath)) { | ||||
| 				Logger.Information("Found candidate Java executable: {JavaExecutablePath}", javaExecutablePath); | ||||
|  | ||||
| 				JavaRuntime? foundRuntime; | ||||
| 				try { | ||||
| 					foundRuntime = await TryReadJavaRuntimeInformationFromProcess(javaExecutablePath); | ||||
| 				} catch (OperationCanceledException) { | ||||
| 					Logger.Error("Java process did not exit in time."); | ||||
| 					continue; | ||||
| 				} catch (Exception e) { | ||||
| 					Logger.Error(e, "Caught exception while reading Java version information."); | ||||
| 					continue; | ||||
| 				} | ||||
|  | ||||
| 				if (foundRuntime == null) { | ||||
| 					Logger.Error("Java executable did not output version information."); | ||||
| 					continue; | ||||
| 				} | ||||
|  | ||||
| 				Logger.Information("Found Java {DisplayName} at: {Path}", foundRuntime.DisplayName, javaExecutablePath); | ||||
| 				yield return new JavaRuntimeExecutable(javaExecutablePath, foundRuntime); | ||||
| 			 | ||||
| 			FileAttributes javaExecutableAttributes; | ||||
| 			try { | ||||
| 				javaExecutableAttributes = File.GetAttributes(javaExecutablePath); | ||||
| 			} catch (Exception) { | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			if (javaExecutableAttributes.HasFlag(FileAttributes.ReparsePoint)) { | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			Logger.Information("Found candidate Java executable: {JavaExecutablePath}", javaExecutablePath); | ||||
|  | ||||
| 			JavaRuntime? foundRuntime; | ||||
| 			try { | ||||
| 				foundRuntime = await TryReadJavaRuntimeInformationFromProcess(javaExecutablePath); | ||||
| 			} catch (OperationCanceledException) { | ||||
| 				Logger.Error("Java process did not exit in time."); | ||||
| 				continue; | ||||
| 			} catch (Exception e) { | ||||
| 				Logger.Error(e, "Caught exception while reading Java version information."); | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			if (foundRuntime == null) { | ||||
| 				Logger.Error("Java executable did not output version information."); | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			Logger.Information("Found Java {DisplayName} at: {Path}", foundRuntime.DisplayName, javaExecutablePath); | ||||
| 			yield return new JavaRuntimeExecutable(javaExecutablePath, foundRuntime); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -1,25 +1,31 @@ | ||||
| using System.Collections.ObjectModel; | ||||
| using System.Collections.Immutable; | ||||
| using System.Collections.ObjectModel; | ||||
|  | ||||
| namespace Phantom.Agent.Minecraft.Java; | ||||
|  | ||||
| sealed class JvmArgumentBuilder { | ||||
| 	private readonly JvmProperties basicProperties; | ||||
| 	private readonly List<string> customProperties = new (); | ||||
| 	private readonly List<string> customArguments = new (); | ||||
|  | ||||
| 	public JvmArgumentBuilder(JvmProperties basicProperties) { | ||||
| 	public JvmArgumentBuilder(JvmProperties basicProperties, ImmutableArray<string> customArguments) { | ||||
| 		this.basicProperties = basicProperties; | ||||
|  | ||||
| 		foreach (var jvmArgument in customArguments) { | ||||
| 			this.customArguments.Add(jvmArgument); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public void AddProperty(string key, string value) { | ||||
| 		customProperties.Add("-D" + key + "=\"" + value + "\""); // TODO test quoting? | ||||
| 		customArguments.Add("-D" + key + "=\"" + value + "\""); // TODO test quoting? | ||||
| 	} | ||||
|  | ||||
| 	public void Build(Collection<string> target) { | ||||
| 		target.Add("-Xms" + basicProperties.InitialHeapMegabytes + "M"); | ||||
| 		target.Add("-Xmx" + basicProperties.MaximumHeapMegabytes + "M"); | ||||
|  | ||||
| 		foreach (var property in customProperties) { | ||||
| 		foreach (var property in customArguments) { | ||||
| 			target.Add(property); | ||||
| 		} | ||||
|  | ||||
| 		target.Add("-Xms" + basicProperties.InitialHeapMegabytes + "M"); | ||||
| 		target.Add("-Xmx" + basicProperties.MaximumHeapMegabytes + "M"); | ||||
| 		target.Add("-Xrs"); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -4,6 +4,8 @@ using Kajabity.Tools.Java; | ||||
| using Phantom.Agent.Minecraft.Instance; | ||||
| using Phantom.Agent.Minecraft.Java; | ||||
| using Phantom.Agent.Minecraft.Server; | ||||
| using Phantom.Common.Minecraft; | ||||
| using Serilog; | ||||
|  | ||||
| namespace Phantom.Agent.Minecraft.Launcher; | ||||
|  | ||||
| @@ -14,11 +16,15 @@ public abstract class BaseLauncher { | ||||
| 		this.instanceProperties = instanceProperties; | ||||
| 	} | ||||
|  | ||||
| 	public async Task<LaunchResult> Launch(LaunchServices services, EventHandler<DownloadProgressEventArgs> downloadProgressEventHandler, CancellationToken cancellationToken) { | ||||
| 	public async Task<LaunchResult> Launch(ILogger logger, LaunchServices services, EventHandler<DownloadProgressEventArgs> downloadProgressEventHandler, CancellationToken cancellationToken) { | ||||
| 		if (!services.JavaRuntimeRepository.TryGetByGuid(instanceProperties.JavaRuntimeGuid, out var javaRuntimeExecutable)) { | ||||
| 			return new LaunchResult.InvalidJavaRuntime(); | ||||
| 		} | ||||
|  | ||||
| 		if (JvmArgumentsHelper.Validate(instanceProperties.JvmArguments) != null) { | ||||
| 			return new LaunchResult.InvalidJvmArguments(); | ||||
| 		} | ||||
|  | ||||
| 		var vanillaServerJarPath = await services.ServerExecutables.DownloadAndGetPath(instanceProperties.ServerVersion, downloadProgressEventHandler, cancellationToken); | ||||
| 		if (vanillaServerJarPath == null) { | ||||
| 			return new LaunchResult.CouldNotDownloadMinecraftServer(); | ||||
| @@ -33,8 +39,8 @@ public abstract class BaseLauncher { | ||||
| 			UseShellExecute = false, | ||||
| 			CreateNoWindow = false | ||||
| 		}; | ||||
|  | ||||
| 		var jvmArguments = new JvmArgumentBuilder(instanceProperties.JvmProperties); | ||||
| 		 | ||||
| 		var jvmArguments = new JvmArgumentBuilder(instanceProperties.JvmProperties, instanceProperties.JvmArguments); | ||||
| 		CustomizeJvmArguments(jvmArguments); | ||||
|  | ||||
| 		var serverJarPath = await PrepareServerJar(vanillaServerJarPath, instanceProperties.InstanceFolder, cancellationToken); | ||||
| @@ -47,12 +53,29 @@ public abstract class BaseLauncher { | ||||
| 		var process = new Process { StartInfo = startInfo }; | ||||
| 		var session = new InstanceSession(process); | ||||
|  | ||||
| 		await AcceptEula(instanceProperties); | ||||
| 		await UpdateServerProperties(instanceProperties); | ||||
| 		try { | ||||
| 			await AcceptEula(instanceProperties); | ||||
| 			await UpdateServerProperties(instanceProperties); | ||||
| 		} catch (Exception e) { | ||||
| 			logger.Error(e, "Caught exception while configuring the server."); | ||||
| 			return new LaunchResult.CouldNotConfigureMinecraftServer(); | ||||
| 		} | ||||
|  | ||||
| 		process.Start(); | ||||
| 		process.BeginOutputReadLine(); | ||||
| 		process.BeginErrorReadLine(); | ||||
| 		try { | ||||
| 			process.Start(); | ||||
| 			process.BeginOutputReadLine(); | ||||
| 			process.BeginErrorReadLine(); | ||||
| 		} catch (Exception launchException) { | ||||
| 			logger.Error(launchException, "Caught exception launching the server process."); | ||||
| 			 | ||||
| 			try { | ||||
| 				process.Kill(); | ||||
| 			} catch (Exception killException) { | ||||
| 				logger.Error(killException, "Caught exception trying to kill the server process after a failed launch."); | ||||
| 			} | ||||
|  | ||||
| 			return new LaunchResult.CouldNotStartMinecraftServer(); | ||||
| 		} | ||||
|  | ||||
| 		return new LaunchResult.Success(session); | ||||
| 	} | ||||
| @@ -72,16 +95,18 @@ public abstract class BaseLauncher { | ||||
| 		var serverPropertiesFilePath = Path.Combine(instanceProperties.InstanceFolder, "server.properties"); | ||||
| 		var serverPropertiesData = new JavaProperties(); | ||||
|  | ||||
| 		await using var fileStream = new FileStream(serverPropertiesFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); | ||||
| 		try { | ||||
| 			await using var readStream = new FileStream(serverPropertiesFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); | ||||
| 			serverPropertiesData.Load(readStream); | ||||
| 		} catch (FileNotFoundException) { | ||||
| 			// ignore | ||||
| 			serverPropertiesData.Load(fileStream); | ||||
| 		} catch (ParseException e) { | ||||
| 			throw new Exception("Could not parse server.properties file: " + serverPropertiesFilePath, e); | ||||
| 		} | ||||
| 		 | ||||
| 		instanceProperties.ServerProperties.SetTo(serverPropertiesData); | ||||
|  | ||||
| 		await using var writeStream = new FileStream(serverPropertiesFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None); | ||||
| 		serverPropertiesData.Store(writeStream, true); | ||||
| 		fileStream.Seek(0L, SeekOrigin.Begin); | ||||
| 		fileStream.SetLength(0L); | ||||
| 		 | ||||
| 		serverPropertiesData.Store(fileStream, true); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -8,6 +8,12 @@ public abstract record LaunchResult { | ||||
| 	public sealed record Success(InstanceSession Session) : LaunchResult; | ||||
|  | ||||
| 	public sealed record InvalidJavaRuntime : LaunchResult; | ||||
| 	 | ||||
| 	public sealed record InvalidJvmArguments : LaunchResult; | ||||
|  | ||||
| 	public sealed record CouldNotDownloadMinecraftServer : LaunchResult; | ||||
| 	 | ||||
| 	public sealed record CouldNotConfigureMinecraftServer : LaunchResult; | ||||
| 	 | ||||
| 	public sealed record CouldNotStartMinecraftServer : LaunchResult; | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| using Phantom.Agent.Minecraft.Java; | ||||
| using Phantom.Agent.Minecraft.Server; | ||||
| using Phantom.Utils.Runtime; | ||||
|  | ||||
| namespace Phantom.Agent.Minecraft.Launcher;  | ||||
|  | ||||
| public sealed record LaunchServices(MinecraftServerExecutables ServerExecutables, JavaRuntimeRepository JavaRuntimeRepository); | ||||
| public sealed record LaunchServices(TaskManager TaskManager, MinecraftServerExecutables ServerExecutables, JavaRuntimeRepository JavaRuntimeRepository); | ||||
|   | ||||
| @@ -7,15 +7,17 @@ | ||||
|   </PropertyGroup> | ||||
|    | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Kajabity.Tools.Java" Version="0.3.7879.40798" /> | ||||
|     <PackageReference Include="Kajabity.Tools.Java" /> | ||||
|   </ItemGroup> | ||||
|    | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\..\Common\Phantom.Common.Data\Phantom.Common.Data.csproj" /> | ||||
|     <ProjectReference Include="..\..\Common\Phantom.Common.Logging\Phantom.Common.Logging.csproj" /> | ||||
|     <ProjectReference Include="..\..\Common\Phantom.Common.Minecraft\Phantom.Common.Minecraft.csproj" /> | ||||
|     <ProjectReference Include="..\..\Utils\Phantom.Utils.Collections\Phantom.Utils.Collections.csproj" /> | ||||
|     <ProjectReference Include="..\..\Utils\Phantom.Utils.Cryptography\Phantom.Utils.Cryptography.csproj" /> | ||||
|     <ProjectReference Include="..\..\Utils\Phantom.Utils.IO\Phantom.Utils.IO.csproj" /> | ||||
|     <ProjectReference Include="..\..\Utils\Phantom.Utils.Runtime\Phantom.Utils.Runtime.csproj" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| using System.Net.Http.Json; | ||||
| using System.Security.Cryptography; | ||||
| using System.Text.Json; | ||||
| using System.Security.Cryptography; | ||||
| using Phantom.Common.Logging; | ||||
| using Phantom.Common.Minecraft; | ||||
| using Phantom.Utils.Cryptography; | ||||
| using Phantom.Utils.IO; | ||||
| using Phantom.Utils.Runtime; | ||||
| using Serilog; | ||||
|  | ||||
| namespace Phantom.Agent.Minecraft.Server; | ||||
| @@ -11,8 +11,8 @@ namespace Phantom.Agent.Minecraft.Server; | ||||
| sealed class MinecraftServerExecutableDownloader { | ||||
| 	private static readonly ILogger Logger = PhantomLogger.Create<MinecraftServerExecutableDownloader>(); | ||||
|  | ||||
| 	private const string VersionManifestUrl = "https://launchermeta.mojang.com/mc/game/version_manifest.json"; | ||||
|  | ||||
| 	private readonly MinecraftVersions minecraftVersions; | ||||
| 	 | ||||
| 	public Task<string?> Task { get; } | ||||
| 	public event EventHandler<DownloadProgressEventArgs>? DownloadProgress; | ||||
| 	public event EventHandler? Completed; | ||||
| @@ -20,7 +20,9 @@ sealed class MinecraftServerExecutableDownloader { | ||||
| 	private readonly CancellationTokenSource cancellationTokenSource = new (); | ||||
| 	private int listeners = 0; | ||||
|  | ||||
| 	public MinecraftServerExecutableDownloader(string version, string filePath, MinecraftServerExecutableDownloadListener listener) { | ||||
| 	public MinecraftServerExecutableDownloader(MinecraftVersions minecraftVersions, string version, string filePath, MinecraftServerExecutableDownloadListener listener) { | ||||
| 		this.minecraftVersions = minecraftVersions; | ||||
| 		 | ||||
| 		Register(listener); | ||||
| 		Task = DownloadAndGetPath(version, filePath); | ||||
| 		Task.ContinueWith(OnCompleted, TaskScheduler.Default); | ||||
| @@ -73,21 +75,18 @@ sealed class MinecraftServerExecutableDownloader { | ||||
| 	private async Task<string?> DownloadAndGetPath(string version, string filePath) { | ||||
| 		Logger.Information("Downloading server version {Version}...", version); | ||||
|  | ||||
| 		HttpClient http = new HttpClient(); | ||||
| 		string tmpFilePath = filePath + ".tmp"; | ||||
|  | ||||
| 		var cancellationToken = cancellationTokenSource.Token; | ||||
| 		try { | ||||
| 			Logger.Information("Fetching version manifest from: {Url}", VersionManifestUrl); | ||||
| 			var versionManifest = await FetchVersionManifest(http, cancellationToken); | ||||
| 			var metadataUrl = GetVersionMetadataUrlFromManifest(version, versionManifest); | ||||
|  | ||||
| 			Logger.Information("Fetching metadata for version {Version} from: {Url}", version, metadataUrl); | ||||
| 			var versionMetadata = await FetchVersionMetadata(http, metadataUrl, cancellationToken); | ||||
| 			var serverExecutableInfo = GetServerExecutableUrlFromMetadata(versionMetadata); | ||||
| 			var serverExecutableInfo = await minecraftVersions.GetServerExecutableInfo(version, cancellationToken); | ||||
| 			if (serverExecutableInfo == null) { | ||||
| 				return null; | ||||
| 			} | ||||
|  | ||||
| 			Logger.Information("Downloading server executable from: {Url} ({Size})", serverExecutableInfo.DownloadUrl, serverExecutableInfo.Size.ToHumanReadable(decimalPlaces: 1)); | ||||
| 			try { | ||||
| 				using var http = new HttpClient(); | ||||
| 				await FetchServerExecutableFile(http, new DownloadProgressCallback(this), serverExecutableInfo, tmpFilePath, cancellationToken); | ||||
| 			} catch (Exception) { | ||||
| 				TryDeleteExecutableAfterFailure(tmpFilePath); | ||||
| @@ -111,31 +110,7 @@ sealed class MinecraftServerExecutableDownloader { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private static async Task<JsonElement> FetchVersionManifest(HttpClient http, CancellationToken cancellationToken) { | ||||
| 		try { | ||||
| 			return await http.GetFromJsonAsync<JsonElement>(VersionManifestUrl, cancellationToken); | ||||
| 		} catch (HttpRequestException e) { | ||||
| 			Logger.Error(e, "Unable to download version manifest."); | ||||
| 			throw StopProcedureException.Instance; | ||||
| 		} catch (Exception e) { | ||||
| 			Logger.Error(e, "Unable to parse version manifest as JSON."); | ||||
| 			throw StopProcedureException.Instance; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private static async Task<JsonElement> FetchVersionMetadata(HttpClient http, string metadataUrl, CancellationToken cancellationToken) { | ||||
| 		try { | ||||
| 			return await http.GetFromJsonAsync<JsonElement>(metadataUrl, cancellationToken); | ||||
| 		} catch (HttpRequestException e) { | ||||
| 			Logger.Error(e, "Unable to download version metadata."); | ||||
| 			throw StopProcedureException.Instance; | ||||
| 		} catch (Exception e) { | ||||
| 			Logger.Error(e, "Unable to parse version metadata as JSON."); | ||||
| 			throw StopProcedureException.Instance; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private static async Task FetchServerExecutableFile(HttpClient http, DownloadProgressCallback progressCallback, ServerExecutableInfo info, string filePath, CancellationToken cancellationToken) { | ||||
| 	private static async Task FetchServerExecutableFile(HttpClient http, DownloadProgressCallback progressCallback, MinecraftServerExecutableInfo info, string filePath, CancellationToken cancellationToken) { | ||||
| 		Sha1String downloadedFileHash; | ||||
|  | ||||
| 		try { | ||||
| @@ -170,83 +145,6 @@ sealed class MinecraftServerExecutableDownloader { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private static string GetVersionMetadataUrlFromManifest(string serverVersion, JsonElement versionManifest) { | ||||
| 		JsonElement versionsElement = GetJsonPropertyOrThrow(versionManifest, "versions", JsonValueKind.Array, "version manifest"); | ||||
| 		JsonElement versionElement; | ||||
| 		try { | ||||
| 			versionElement = versionsElement.EnumerateArray().Single(ele => ele.TryGetProperty("id", out var id) && id.ValueKind == JsonValueKind.String && id.GetString() == serverVersion); | ||||
| 		} catch (Exception) { | ||||
| 			Logger.Error("Version {Version} was not found in version manifest.", serverVersion); | ||||
| 			throw StopProcedureException.Instance; | ||||
| 		} | ||||
|  | ||||
| 		JsonElement urlElement = GetJsonPropertyOrThrow(versionElement, "url", JsonValueKind.String, "version entry in version manifest"); | ||||
| 		string? url = urlElement.GetString(); | ||||
|  | ||||
| 		if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) { | ||||
| 			Logger.Error("The \"url\" key in version entry in version manifest does not contain a valid URL: {Url}", url); | ||||
| 			throw StopProcedureException.Instance; | ||||
| 		} | ||||
|  | ||||
| 		if (uri.Scheme != "https" || !uri.AbsolutePath.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) { | ||||
| 			Logger.Error("The \"url\" key in version entry in version manifest does not contain a accepted URL: {Url}", url); | ||||
| 			throw StopProcedureException.Instance; | ||||
| 		} | ||||
|  | ||||
| 		return url; | ||||
| 	} | ||||
|  | ||||
| 	private static ServerExecutableInfo GetServerExecutableUrlFromMetadata(JsonElement versionMetadata) { | ||||
| 		JsonElement downloadsElement = GetJsonPropertyOrThrow(versionMetadata, "downloads", JsonValueKind.Object, "version metadata"); | ||||
| 		JsonElement serverElement = GetJsonPropertyOrThrow(downloadsElement, "server", JsonValueKind.Object, "downloads object in version metadata"); | ||||
| 		JsonElement urlElement = GetJsonPropertyOrThrow(serverElement, "url", JsonValueKind.String, "downloads.server object in version metadata"); | ||||
| 		string? url = urlElement.GetString(); | ||||
|  | ||||
| 		if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) { | ||||
| 			Logger.Error("The \"url\" key in downloads.server object in version metadata does not contain a valid URL: {Url}", url); | ||||
| 			throw StopProcedureException.Instance; | ||||
| 		} | ||||
|  | ||||
| 		if (uri.Scheme != "https" || !uri.AbsolutePath.EndsWith(".jar", StringComparison.OrdinalIgnoreCase)) { | ||||
| 			Logger.Error("The \"url\" key in downloads.server object in version metadata does not contain a accepted URL: {Url}", url); | ||||
| 			throw StopProcedureException.Instance; | ||||
| 		} | ||||
|  | ||||
| 		JsonElement sizeElement = GetJsonPropertyOrThrow(serverElement, "size", JsonValueKind.Number, "downloads.server object in version metadata"); | ||||
| 		ulong size; | ||||
| 		try { | ||||
| 			size = sizeElement.GetUInt64(); | ||||
| 		} catch (FormatException) { | ||||
| 			Logger.Error("The \"size\" key in downloads.server object in version metadata contains an invalid file size: {Size}", sizeElement); | ||||
| 			throw StopProcedureException.Instance; | ||||
| 		} | ||||
|  | ||||
| 		JsonElement sha1Element = GetJsonPropertyOrThrow(serverElement, "sha1", JsonValueKind.String, "downloads.server object in version metadata"); | ||||
| 		Sha1String hash; | ||||
| 		try { | ||||
| 			hash = Sha1String.FromString(sha1Element.GetString()); | ||||
| 		} catch (Exception) { | ||||
| 			Logger.Error("The \"sha1\" key in downloads.server object in version metadata does not contain a valid SHA-1 hash: {Sha1}", sha1Element.GetString()); | ||||
| 			throw StopProcedureException.Instance; | ||||
| 		} | ||||
|  | ||||
| 		return new ServerExecutableInfo(url, hash, new FileSize(size)); | ||||
| 	} | ||||
|  | ||||
| 	private static JsonElement GetJsonPropertyOrThrow(JsonElement parentElement, string propertyKey, JsonValueKind expectedKind, string location) { | ||||
| 		if (!parentElement.TryGetProperty(propertyKey, out var valueElement)) { | ||||
| 			Logger.Error("Missing \"{Property}\" key in " + location + ".", propertyKey); | ||||
| 			throw StopProcedureException.Instance; | ||||
| 		} | ||||
|  | ||||
| 		if (valueElement.ValueKind != expectedKind) { | ||||
| 			Logger.Error("The \"{Property}\" key in " + location + " does not contain a JSON {ExpectedType}. Actual type: {ActualType}", propertyKey, expectedKind, valueElement.ValueKind); | ||||
| 			throw StopProcedureException.Instance; | ||||
| 		} | ||||
|  | ||||
| 		return valueElement; | ||||
| 	} | ||||
|  | ||||
| 	private sealed class MinecraftServerDownloadStreamCopier : IDisposable { | ||||
| 		private readonly StreamCopier streamCopier = new (); | ||||
| 		private readonly IncrementalHash sha1 = IncrementalHash.CreateHash(HashAlgorithmName.SHA1); | ||||
| @@ -278,10 +176,4 @@ sealed class MinecraftServerExecutableDownloader { | ||||
| 			streamCopier.Dispose(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private sealed class StopProcedureException : Exception { | ||||
| 		public static StopProcedureException Instance { get; } = new (); | ||||
|  | ||||
| 		private StopProcedureException() {} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,15 +1,19 @@ | ||||
| using System.Text.RegularExpressions; | ||||
| using Phantom.Common.Logging; | ||||
| using Phantom.Common.Minecraft; | ||||
| using Phantom.Utils.IO; | ||||
| using Serilog; | ||||
|  | ||||
| namespace Phantom.Agent.Minecraft.Server; | ||||
|  | ||||
| public sealed class MinecraftServerExecutables { | ||||
| public sealed partial class MinecraftServerExecutables : IDisposable { | ||||
| 	private static readonly ILogger Logger = PhantomLogger.Create<MinecraftServerExecutables>(); | ||||
|  | ||||
| 	private static readonly Regex VersionFolderSanitizeRegex = new (@"[^a-zA-Z0-9_\-\.]", RegexOptions.Compiled); | ||||
| 	[GeneratedRegex(@"[^a-zA-Z0-9_\-\.]", RegexOptions.Compiled)] | ||||
| 	private static partial Regex VersionFolderSanitizeRegex(); | ||||
|  | ||||
| 	private readonly string basePath; | ||||
| 	private readonly MinecraftVersions minecraftVersions = new (); | ||||
| 	private readonly Dictionary<string, MinecraftServerExecutableDownloader> runningDownloadersByVersion = new (); | ||||
|  | ||||
| 	public MinecraftServerExecutables(string basePath) { | ||||
| @@ -17,7 +21,7 @@ public sealed class MinecraftServerExecutables { | ||||
| 	} | ||||
|  | ||||
| 	internal async Task<string?> DownloadAndGetPath(string version, EventHandler<DownloadProgressEventArgs> progressEventHandler, CancellationToken cancellationToken) { | ||||
| 		string serverExecutableFolderPath = Path.Combine(basePath, VersionFolderSanitizeRegex.Replace(version, "_")); | ||||
| 		string serverExecutableFolderPath = Path.Combine(basePath, VersionFolderSanitizeRegex().Replace(version, "_")); | ||||
| 		string serverExecutableFilePath = Path.Combine(serverExecutableFolderPath, "server.jar"); | ||||
|  | ||||
| 		if (File.Exists(serverExecutableFilePath)) { | ||||
| @@ -25,7 +29,7 @@ public sealed class MinecraftServerExecutables { | ||||
| 		} | ||||
|  | ||||
| 		try { | ||||
| 			Directory.CreateDirectory(serverExecutableFolderPath); | ||||
| 			Directories.Create(serverExecutableFolderPath, Chmod.URWX_GRX); | ||||
| 		} catch (Exception e) { | ||||
| 			Logger.Error(e, "Unable to create folder for server executable: {ServerExecutableFolderPath}", serverExecutableFolderPath); | ||||
| 			return null; | ||||
| @@ -40,17 +44,21 @@ public sealed class MinecraftServerExecutables { | ||||
| 				downloader.Register(listener); | ||||
| 			} | ||||
| 			else { | ||||
| 				downloader = new MinecraftServerExecutableDownloader(version, serverExecutableFilePath, listener); | ||||
| 				downloader = new MinecraftServerExecutableDownloader(minecraftVersions, version, serverExecutableFilePath, listener); | ||||
| 				downloader.Completed += (_, _) => { | ||||
| 					lock (this) { | ||||
| 						runningDownloadersByVersion.Remove(version); | ||||
| 					} | ||||
| 				}; | ||||
| 				 | ||||
|  | ||||
| 				runningDownloadersByVersion[version] = downloader; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return await downloader.Task.WaitAsync(cancellationToken); | ||||
| 	} | ||||
|  | ||||
| 	public void Dispose() { | ||||
| 		minecraftVersions.Dispose(); | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										40
									
								
								Agent/Phantom.Agent.Rpc/KeepAliveLoop.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								Agent/Phantom.Agent.Rpc/KeepAliveLoop.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| using Phantom.Common.Logging; | ||||
| using Phantom.Common.Messages.ToServer; | ||||
| using Serilog; | ||||
|  | ||||
| namespace Phantom.Agent.Rpc; | ||||
|  | ||||
| sealed class KeepAliveLoop { | ||||
| 	private static readonly ILogger Logger = PhantomLogger.Create<KeepAliveLoop>(); | ||||
|  | ||||
| 	private static readonly TimeSpan KeepAliveInterval = TimeSpan.FromSeconds(10); | ||||
|  | ||||
| 	private readonly RpcServerConnection connection; | ||||
| 	private readonly CancellationTokenSource cancellationTokenSource = new (); | ||||
|  | ||||
| 	public KeepAliveLoop(RpcServerConnection connection) { | ||||
| 		this.connection = connection; | ||||
| 		Task.Run(Run); | ||||
| 	} | ||||
|  | ||||
| 	private async Task Run() { | ||||
| 		var cancellationToken = cancellationTokenSource.Token; | ||||
|  | ||||
| 		Logger.Information("Started keep-alive loop."); | ||||
| 		try { | ||||
| 			while (true) { | ||||
| 				await Task.Delay(KeepAliveInterval, cancellationToken); | ||||
| 				await connection.Send(new AgentIsAliveMessage()); | ||||
| 			} | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			// Ignore. | ||||
| 		} finally { | ||||
| 			cancellationTokenSource.Dispose(); | ||||
| 			Logger.Information("Stopped keep-alive loop."); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public void Cancel() { | ||||
| 		cancellationTokenSource.Cancel(); | ||||
| 	} | ||||
| } | ||||
| @@ -6,10 +6,6 @@ | ||||
|     <Nullable>enable</Nullable> | ||||
|   </PropertyGroup> | ||||
|    | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Serilog" Version="2.12.0" /> | ||||
|   </ItemGroup> | ||||
|    | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\..\Common\Phantom.Common.Messages\Phantom.Common.Messages.csproj" /> | ||||
|   </ItemGroup> | ||||
|   | ||||
| @@ -1,19 +0,0 @@ | ||||
| using NetMQ; | ||||
| using NetMQ.Sockets; | ||||
| using Phantom.Common.Messages; | ||||
| using Phantom.Common.Messages.ToServer; | ||||
|  | ||||
| namespace Phantom.Agent.Rpc;  | ||||
|  | ||||
| public static class RpcExtensions { | ||||
| 	internal static async Task SendMessage<TMessage>(this ClientSocket socket, TMessage message) where TMessage : IMessageToServer { | ||||
| 		byte[] bytes = MessageRegistries.ToServer.Write(message).ToArray(); | ||||
| 		if (bytes.Length > 0) { | ||||
| 			await socket.SendAsync(bytes); | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	public static Task SendSimpleReply<TMessage, TReplyEnum>(this ClientSocket socket, TMessage message, TReplyEnum reply) where TMessage : IMessageWithReply where TReplyEnum : Enum { | ||||
| 		return SendMessage(socket, SimpleReplyMessage.FromEnum(message.SequenceId, reply)); | ||||
| 	} | ||||
| } | ||||
| @@ -2,36 +2,45 @@ | ||||
| using NetMQ.Sockets; | ||||
| using Phantom.Common.Data.Agent; | ||||
| using Phantom.Common.Messages; | ||||
| using Phantom.Common.Messages.BiDirectional; | ||||
| using Phantom.Common.Messages.ToServer; | ||||
| using Phantom.Utils.Rpc; | ||||
| using Phantom.Utils.Rpc.Message; | ||||
| using Phantom.Utils.Runtime; | ||||
| using Serilog; | ||||
| using Serilog.Events; | ||||
|  | ||||
| namespace Phantom.Agent.Rpc; | ||||
|  | ||||
| public sealed class RpcLauncher : RpcRuntime<ClientSocket> { | ||||
| 	public static async Task Launch(RpcConfiguration config, AgentAuthToken authToken, AgentInfo agentInfo, Func<ClientSocket, IMessageToAgentListener> listenerFactory) { | ||||
| 	public static async Task Launch(RpcConfiguration config, AgentAuthToken authToken, AgentInfo agentInfo, Func<RpcServerConnection, IMessageToAgentListener> listenerFactory, SemaphoreSlim disconnectSemaphore, CancellationToken receiveCancellationToken) { | ||||
| 		var socket = new ClientSocket(); | ||||
| 		var options = socket.Options; | ||||
|  | ||||
| 		options.CurveServerCertificate = config.ServerCertificate; | ||||
| 		options.CurveCertificate = new NetMQCertificate(); | ||||
| 		options.HelloMessage = MessageRegistries.ToServer.Write(new RegisterAgentMessage(authToken, agentInfo)).ToArray(); | ||||
| 		 | ||||
| 		await new RpcLauncher(config, socket, agentInfo.Guid, listenerFactory).Launch(); | ||||
|  | ||||
| 		await new RpcLauncher(config, socket, agentInfo.Guid, listenerFactory, disconnectSemaphore, receiveCancellationToken).Launch(); | ||||
| 	} | ||||
|  | ||||
| 	private readonly RpcConfiguration config; | ||||
| 	private readonly Guid agentGuid; | ||||
| 	private readonly Func<ClientSocket, IMessageToAgentListener> messageListenerFactory; | ||||
| 	private readonly Func<RpcServerConnection, IMessageToAgentListener> messageListenerFactory; | ||||
|  | ||||
| 	private RpcLauncher(RpcConfiguration config, ClientSocket socket, Guid agentGuid, Func<ClientSocket, IMessageToAgentListener> messageListenerFactory) : base(socket, config.CancellationToken) { | ||||
| 	private readonly SemaphoreSlim disconnectSemaphore; | ||||
| 	private readonly CancellationToken receiveCancellationToken; | ||||
|  | ||||
| 	private RpcLauncher(RpcConfiguration config, ClientSocket socket, Guid agentGuid, Func<RpcServerConnection, IMessageToAgentListener> messageListenerFactory, SemaphoreSlim disconnectSemaphore, CancellationToken receiveCancellationToken) : base(config, socket) { | ||||
| 		this.config = config; | ||||
| 		this.agentGuid = agentGuid; | ||||
| 		this.messageListenerFactory = messageListenerFactory; | ||||
| 		this.disconnectSemaphore = disconnectSemaphore; | ||||
| 		this.receiveCancellationToken = receiveCancellationToken; | ||||
| 	} | ||||
|  | ||||
| 	protected override void Connect(ClientSocket socket) { | ||||
| 		var logger = config.Logger; | ||||
| 		var logger = config.RuntimeLogger; | ||||
| 		var url = config.TcpUrl; | ||||
|  | ||||
| 		logger.Information("Starting ZeroMQ client and connecting to {Url}...", url); | ||||
| @@ -39,35 +48,60 @@ public sealed class RpcLauncher : RpcRuntime<ClientSocket> { | ||||
| 		logger.Information("ZeroMQ client ready."); | ||||
| 	} | ||||
|  | ||||
| 	protected override async Task Run(ClientSocket socket, CancellationToken cancellationToken) { | ||||
| 		var logger = config.Logger; | ||||
| 	protected override void Run(ClientSocket socket, MessageReplyTracker replyTracker, TaskManager taskManager) { | ||||
| 		var connection = new RpcServerConnection(socket, replyTracker); | ||||
| 		ServerMessaging.SetCurrentConnection(connection); | ||||
| 		 | ||||
| 		var listener = messageListenerFactory(socket); | ||||
| 		 | ||||
| 		ServerMessaging.SetCurrentSocket(socket, cancellationToken); | ||||
| 		var logger = config.RuntimeLogger; | ||||
| 		var handler = new MessageToAgentHandler(messageListenerFactory(connection), logger, taskManager, receiveCancellationToken); | ||||
| 		var keepAliveLoop = new KeepAliveLoop(connection); | ||||
|  | ||||
| 		// TODO optimize msg | ||||
| 		await foreach (var bytes in socket.ReceiveBytesAsyncEnumerable(cancellationToken)) { | ||||
| 			if (logger.IsEnabled(LogEventLevel.Verbose)) { | ||||
| 				if (bytes.Length > 0 && MessageRegistries.ToAgent.TryGetType(bytes, out var type)) { | ||||
| 					logger.Verbose("Received {MessageType} ({Bytes} B) from server.", type.Name, bytes.Length); | ||||
| 				} | ||||
| 				else { | ||||
| 					logger.Verbose("Received {Bytes} B message from server.", bytes.Length); | ||||
| 		try { | ||||
| 			while (!receiveCancellationToken.IsCancellationRequested) { | ||||
| 				var data = socket.Receive(receiveCancellationToken); | ||||
| 				 | ||||
| 				LogMessageType(logger, data); | ||||
| 				 | ||||
| 				if (data.Length > 0) { | ||||
| 					MessageRegistries.ToAgent.Handle(data, handler); | ||||
| 				} | ||||
| 			} | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			// Ignore. | ||||
| 		} finally { | ||||
| 			logger.Verbose("ZeroMQ client stopped receiving messages."); | ||||
|  | ||||
| 			if (bytes.Length > 0) { | ||||
| 				MessageRegistries.ToAgent.Handle(bytes, listener, cancellationToken); | ||||
| 			} | ||||
| 			disconnectSemaphore.Wait(CancellationToken.None); | ||||
| 			keepAliveLoop.Cancel(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	protected override async Task Disconnect(ClientSocket socket) { | ||||
| 		var timeoutTask = Task.Delay(TimeSpan.FromSeconds(5)); | ||||
| 		var finishedTask = await Task.WhenAny(socket.SendMessage(new UnregisterAgentMessage(agentGuid)), timeoutTask); | ||||
| 		if (finishedTask == timeoutTask) { | ||||
| 			config.Logger.Error("Timed out communicating agent shutdown with the server."); | ||||
| 	private static void LogMessageType(ILogger logger, ReadOnlyMemory<byte> data) { | ||||
| 		if (!logger.IsEnabled(LogEventLevel.Verbose)) { | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		if (data.Length > 0 && MessageRegistries.ToAgent.TryGetType(data, out var type)) { | ||||
| 			logger.Verbose("Received {MessageType} ({Bytes} B) from server.", type.Name, data.Length); | ||||
| 		} | ||||
| 		else { | ||||
| 			logger.Verbose("Received {Bytes} B message from server.", data.Length); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	protected override async Task Disconnect() { | ||||
| 		var unregisterTimeoutTask = Task.Delay(TimeSpan.FromSeconds(5), CancellationToken.None); | ||||
| 		var finishedTask = await Task.WhenAny(ServerMessaging.Send(new UnregisterAgentMessage(agentGuid)), unregisterTimeoutTask); | ||||
| 		if (finishedTask == unregisterTimeoutTask) { | ||||
| 			config.RuntimeLogger.Error("Timed out communicating agent shutdown with the server."); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private sealed class MessageToAgentHandler : MessageHandler<IMessageToAgentListener> { | ||||
| 		public MessageToAgentHandler(IMessageToAgentListener listener, ILogger logger, TaskManager taskManager, CancellationToken cancellationToken) : base(listener, logger, taskManager, cancellationToken) {} | ||||
| 		 | ||||
| 		protected override Task SendReply(uint sequenceId, byte[] serializedReply) { | ||||
| 			return ServerMessaging.Send(new ReplyMessage(sequenceId, serializedReply)); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										41
									
								
								Agent/Phantom.Agent.Rpc/RpcServerConnection.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								Agent/Phantom.Agent.Rpc/RpcServerConnection.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| using NetMQ; | ||||
| using NetMQ.Sockets; | ||||
| using Phantom.Common.Messages; | ||||
| using Phantom.Common.Messages.BiDirectional; | ||||
| using Phantom.Utils.Rpc.Message; | ||||
|  | ||||
| namespace Phantom.Agent.Rpc;  | ||||
|  | ||||
| public sealed class RpcServerConnection { | ||||
| 	private readonly ClientSocket socket; | ||||
| 	private readonly MessageReplyTracker replyTracker; | ||||
|  | ||||
| 	internal RpcServerConnection(ClientSocket socket, MessageReplyTracker replyTracker) { | ||||
| 		this.socket = socket; | ||||
| 		this.replyTracker = replyTracker; | ||||
| 	} | ||||
|  | ||||
| 	internal async Task Send<TMessage>(TMessage message) where TMessage : IMessageToServer { | ||||
| 		var bytes = MessageRegistries.ToServer.Write(message).ToArray(); | ||||
| 		if (bytes.Length > 0) { | ||||
| 			await socket.SendAsync(bytes); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	internal async Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken cancellationToken) where TMessage : IMessageToServer<TReply> where TReply : class { | ||||
| 		var sequenceId = replyTracker.RegisterReply(); | ||||
| 		 | ||||
| 		var bytes = MessageRegistries.ToServer.Write<TMessage, TReply>(sequenceId, message).ToArray(); | ||||
| 		if (bytes.Length == 0) { | ||||
| 			replyTracker.ForgetReply(sequenceId); | ||||
| 			return null; | ||||
| 		} | ||||
|  | ||||
| 		await socket.SendAsync(bytes); | ||||
| 		return await replyTracker.WaitForReply<TReply>(sequenceId, waitForReplyTime, cancellationToken); | ||||
| 	} | ||||
|  | ||||
| 	public void Receive(ReplyMessage message) { | ||||
| 		replyTracker.ReceiveReply(message.SequenceId, message.SerializedReply); | ||||
| 	} | ||||
| } | ||||
| @@ -1,55 +1,34 @@ | ||||
| using NetMQ.Sockets; | ||||
| using Phantom.Common.Logging; | ||||
| using Phantom.Common.Logging; | ||||
| using Phantom.Common.Messages; | ||||
| using Phantom.Common.Messages.ToServer; | ||||
| using Serilog; | ||||
|  | ||||
| namespace Phantom.Agent.Rpc; | ||||
|  | ||||
| public static class ServerMessaging { | ||||
| 	private static readonly ILogger Logger = PhantomLogger.Create(typeof(ServerMessaging)); | ||||
| 	private static readonly ILogger Logger = PhantomLogger.Create(nameof(ServerMessaging)); | ||||
| 	 | ||||
| 	private static readonly TimeSpan KeepAliveInterval = TimeSpan.FromSeconds(10); | ||||
| 	private static RpcServerConnection? CurrentConnection { get; set; } | ||||
| 	private static RpcServerConnection CurrentConnectionOrThrow => CurrentConnection ?? throw new InvalidOperationException("Server connection not ready."); | ||||
| 	 | ||||
| 	private static readonly object SetCurrentConnectionLock = new (); | ||||
|  | ||||
| 	private static ClientSocket? CurrentSocket { get; set; } | ||||
| 	private static readonly object SetCurrentSocketLock = new (); | ||||
|  | ||||
| 	internal static void SetCurrentSocket(ClientSocket socket, CancellationToken cancellationToken) { | ||||
| 		Logger.Information("Server socket ready."); | ||||
| 	internal static void SetCurrentConnection(RpcServerConnection connection) { | ||||
| 		lock (SetCurrentConnectionLock) { | ||||
| 			if (CurrentConnection != null) { | ||||
| 				throw new InvalidOperationException("Server connection can only be set once."); | ||||
| 			} | ||||
| 			 | ||||
| 			CurrentConnection = connection; | ||||
| 		} | ||||
| 		 | ||||
| 		bool isFirstSet = false; | ||||
| 		lock (SetCurrentSocketLock) { | ||||
| 			if (CurrentSocket == null) { | ||||
| 				isFirstSet = true; | ||||
| 			} | ||||
|  | ||||
| 			CurrentSocket = socket; | ||||
| 		} | ||||
|  | ||||
| 		if (isFirstSet) { | ||||
| 			Task.Factory.StartNew(static o => SendKeepAliveLoop((CancellationToken) o!), cancellationToken, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default); | ||||
| 		} | ||||
| 		Logger.Information("Server connection ready."); | ||||
| 	} | ||||
|  | ||||
| 	public static async Task SendMessage<TMessage>(TMessage message) where TMessage : IMessageToServer { | ||||
| 		var currentSocket = CurrentSocket ?? throw new InvalidOperationException("Server socket not ready."); | ||||
| 		await currentSocket.SendMessage(message); | ||||
| 	public static Task Send<TMessage>(TMessage message) where TMessage : IMessageToServer { | ||||
| 		return CurrentConnectionOrThrow.Send(message); | ||||
| 	} | ||||
|  | ||||
| 	private static async Task SendKeepAliveLoop(CancellationToken cancellationToken) { | ||||
| 		try { | ||||
| 			while (true) { | ||||
| 				await Task.Delay(KeepAliveInterval, cancellationToken); | ||||
|  | ||||
| 				var currentSocket = CurrentSocket; | ||||
| 				if (currentSocket != null) { | ||||
| 					await currentSocket.SendMessage(new AgentIsAliveMessage()); | ||||
| 				} | ||||
| 			} | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			// Ignore. | ||||
| 		} finally { | ||||
| 			Logger.Information("Stopped keep-alive loop."); | ||||
| 		} | ||||
| 	public static Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken cancellationToken) where TMessage : IMessageToServer<TReply> where TReply : class { | ||||
| 		return CurrentConnectionOrThrow.Send<TMessage, TReply>(message, waitForReplyTime, cancellationToken); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,19 +1,26 @@ | ||||
| using Phantom.Agent.Minecraft.Java; | ||||
| using Phantom.Agent.Services.Instances; | ||||
| using Phantom.Common.Data.Agent; | ||||
| using Phantom.Common.Logging; | ||||
| using Phantom.Utils.Runtime; | ||||
| using Serilog; | ||||
|  | ||||
| namespace Phantom.Agent.Services; | ||||
|  | ||||
| public sealed class AgentServices { | ||||
| 	private AgentFolders AgentFolders { get; } | ||||
| 	private static readonly ILogger Logger = PhantomLogger.Create<AgentServices>(); | ||||
| 	 | ||||
| 	private AgentFolders AgentFolders { get; } | ||||
| 	private TaskManager TaskManager { get; } | ||||
|  | ||||
| 	internal JavaRuntimeRepository JavaRuntimeRepository { get; } | ||||
| 	internal InstanceSessionManager InstanceSessionManager { get; } | ||||
|  | ||||
| 	public AgentServices(AgentInfo agentInfo, AgentFolders agentFolders) { | ||||
| 		this.AgentFolders = agentFolders; | ||||
| 		this.TaskManager = new TaskManager(PhantomLogger.Create<TaskManager, AgentServices>()); | ||||
| 		this.JavaRuntimeRepository = new JavaRuntimeRepository(); | ||||
| 		this.InstanceSessionManager = new InstanceSessionManager(agentInfo, agentFolders, JavaRuntimeRepository); | ||||
| 		this.InstanceSessionManager = new InstanceSessionManager(agentInfo, agentFolders, JavaRuntimeRepository, TaskManager); | ||||
| 	} | ||||
|  | ||||
| 	public async Task Initialize() { | ||||
| @@ -23,6 +30,11 @@ public sealed class AgentServices { | ||||
| 	} | ||||
|  | ||||
| 	public async Task Shutdown() { | ||||
| 		Logger.Information("Stopping services..."); | ||||
| 		 | ||||
| 		await InstanceSessionManager.StopAll(); | ||||
| 		await TaskManager.Stop(); | ||||
| 		 | ||||
| 		Logger.Information("Services stopped."); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
| using Phantom.Agent.Rpc; | ||||
| using Phantom.Agent.Services.Instances.States; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Common.Data.Minecraft; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Logging; | ||||
| using Phantom.Common.Messages.ToServer; | ||||
| @@ -32,7 +33,7 @@ sealed class Instance : IDisposable { | ||||
| 	private readonly LaunchServices launchServices; | ||||
| 	private readonly PortManager portManager; | ||||
|  | ||||
| 	private InstanceStatus currentStatus; | ||||
| 	private IInstanceStatus currentStatus; | ||||
| 	private IInstanceState currentState; | ||||
| 	private readonly SemaphoreSlim stateTransitioningActionSemaphore = new (1, 1); | ||||
|  | ||||
| @@ -46,24 +47,31 @@ sealed class Instance : IDisposable { | ||||
| 		this.launchServices = launchServices; | ||||
| 		this.portManager = portManager; | ||||
| 		this.currentState = new InstanceNotRunningState(); | ||||
| 		this.currentStatus = InstanceStatus.IsNotRunning; | ||||
| 		this.currentStatus = InstanceStatus.NotRunning; | ||||
| 	} | ||||
|  | ||||
| 	private async Task ReportLastStatus() { | ||||
| 		await ServerMessaging.SendMessage(new ReportInstanceStatusMessage(Configuration.InstanceGuid, currentStatus)); | ||||
| 		await ServerMessaging.Send(new ReportInstanceStatusMessage(Configuration.InstanceGuid, currentStatus)); | ||||
| 	} | ||||
| 	 | ||||
| 	private bool TransitionState(IInstanceState newState) { | ||||
| 	private void TransitionState(IInstanceState newState) { | ||||
| 		if (currentState == newState) { | ||||
| 			return false; | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		if (currentState is IDisposable disposable) { | ||||
| 			disposable.Dispose(); | ||||
| 		} | ||||
|  | ||||
| 		logger.Verbose("Transitioning instance state to: {NewState}", newState.GetType().Name); | ||||
| 		 | ||||
| 		currentState = newState; | ||||
| 		return true; | ||||
| 		currentState.Initialize(); | ||||
| 	} | ||||
|  | ||||
| 	private T TransitionStateAndReturn<T>((IInstanceState State, T Result) newStateAndResult) { | ||||
| 		TransitionState(newStateAndResult.State); | ||||
| 		return newStateAndResult.Result; | ||||
| 	} | ||||
|  | ||||
| 	public async Task Reconfigure(InstanceConfiguration configuration, BaseLauncher launcher, CancellationToken cancellationToken) { | ||||
| @@ -77,44 +85,32 @@ sealed class Instance : IDisposable { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public async Task<LaunchInstanceResult> Launch(CancellationToken cancellationToken) { | ||||
| 		await stateTransitioningActionSemaphore.WaitAsync(cancellationToken); | ||||
| 	public async Task<LaunchInstanceResult> Launch(CancellationToken shutdownCancellationToken) { | ||||
| 		await stateTransitioningActionSemaphore.WaitAsync(shutdownCancellationToken); | ||||
| 		try { | ||||
| 			if (TransitionState(currentState.Launch(new InstanceContextImpl(this)))) { | ||||
| 				return LaunchInstanceResult.LaunchInitiated; | ||||
| 			} | ||||
|  | ||||
| 			return currentState switch { | ||||
| 				InstanceLaunchingState => LaunchInstanceResult.InstanceAlreadyLaunching, | ||||
| 				InstanceRunningState   => LaunchInstanceResult.InstanceAlreadyRunning, | ||||
| 				InstanceStoppingState  => LaunchInstanceResult.InstanceIsStopping, | ||||
| 				_                      => LaunchInstanceResult.UnknownError | ||||
| 			}; | ||||
| 			return TransitionStateAndReturn(currentState.Launch(new InstanceContextImpl(this, shutdownCancellationToken))); | ||||
| 		} catch (Exception e) { | ||||
| 			logger.Error(e, "Caught exception while launching instance."); | ||||
| 			return LaunchInstanceResult.UnknownError; | ||||
| 		} finally { | ||||
| 			stateTransitioningActionSemaphore.Release(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public async Task<StopInstanceResult> Stop() { | ||||
| 	public async Task<StopInstanceResult> Stop(MinecraftStopStrategy stopStrategy) { | ||||
| 		await stateTransitioningActionSemaphore.WaitAsync(); | ||||
| 		try { | ||||
| 			if (TransitionState(currentState.Stop())) { | ||||
| 				return StopInstanceResult.StopInitiated; | ||||
| 			} | ||||
|  | ||||
| 			return currentState switch { | ||||
| 				InstanceNotRunningState => StopInstanceResult.InstanceAlreadyStopped, | ||||
| 				InstanceLaunchingState  => StopInstanceResult.StopInitiated, | ||||
| 				InstanceStoppingState   => StopInstanceResult.InstanceAlreadyStopping, | ||||
| 				_                       => StopInstanceResult.UnknownError | ||||
| 			}; | ||||
| 			return TransitionStateAndReturn(currentState.Stop(stopStrategy)); | ||||
| 		} catch (Exception e) { | ||||
| 			logger.Error(e, "Caught exception while stopping instance."); | ||||
| 			return StopInstanceResult.UnknownError; | ||||
| 		} finally { | ||||
| 			stateTransitioningActionSemaphore.Release(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public async Task StopAndWait(TimeSpan waitTime) { | ||||
| 		await Stop(); | ||||
| 		await Stop(MinecraftStopStrategy.Instant); | ||||
|  | ||||
| 		using var waitTokenSource = new CancellationTokenSource(waitTime); | ||||
| 		var waitToken = waitTokenSource.Token; | ||||
| @@ -130,10 +126,13 @@ sealed class Instance : IDisposable { | ||||
|  | ||||
| 	private sealed class InstanceContextImpl : InstanceContext { | ||||
| 		private readonly Instance instance; | ||||
| 		private readonly CancellationToken shutdownCancellationToken; | ||||
| 		 | ||||
| 		private int statusUpdateCounter; | ||||
|  | ||||
| 		public InstanceContextImpl(Instance instance) : base(instance.Configuration, instance.Launcher) { | ||||
| 		public InstanceContextImpl(Instance instance, CancellationToken shutdownCancellationToken) : base(instance.Configuration, instance.Launcher) { | ||||
| 			this.instance = instance; | ||||
| 			this.shutdownCancellationToken = shutdownCancellationToken; | ||||
| 		} | ||||
|  | ||||
| 		public override LaunchServices LaunchServices => instance.launchServices; | ||||
| @@ -141,21 +140,33 @@ sealed class Instance : IDisposable { | ||||
| 		public override ILogger Logger => instance.logger; | ||||
| 		public override string ShortName => instance.shortName; | ||||
|  | ||||
| 		public override void ReportStatus(InstanceStatus newStatus) { | ||||
| 		public override void ReportStatus(IInstanceStatus newStatus) { | ||||
| 			int myStatusUpdateCounter = Interlocked.Increment(ref statusUpdateCounter); | ||||
| 			 | ||||
| 			Task.Run(async () => { | ||||
| 			instance.launchServices.TaskManager.Run("Report status of instance " + instance.shortName + " as " + newStatus.GetType().Name, async () => { | ||||
| 				if (myStatusUpdateCounter == statusUpdateCounter) { | ||||
| 					instance.currentStatus = newStatus; | ||||
| 					await ServerMessaging.SendMessage(new ReportInstanceStatusMessage(Configuration.InstanceGuid, newStatus)); | ||||
| 					await ServerMessaging.Send(new ReportInstanceStatusMessage(Configuration.InstanceGuid, newStatus)); | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
|  | ||||
| 		public override void TransitionState(Func<IInstanceState> newState) { | ||||
| 			instance.stateTransitioningActionSemaphore.Wait(); | ||||
| 		public override void TransitionState(Func<(IInstanceState, IInstanceStatus?)> newStateAndStatus) { | ||||
| 			instance.stateTransitioningActionSemaphore.Wait(CancellationToken.None); | ||||
| 			try { | ||||
| 				instance.TransitionState(newState()); | ||||
| 				var (state, status) = newStateAndStatus(); | ||||
| 				if (state is not InstanceNotRunningState && shutdownCancellationToken.IsCancellationRequested) { | ||||
| 					instance.logger.Verbose("Cancelled state transition to {State} due to Agent shutdown.", state.GetType().Name); | ||||
| 					return; | ||||
| 				} | ||||
|  | ||||
| 				if (status != null) { | ||||
| 					ReportStatus(status); | ||||
| 				} | ||||
|  | ||||
| 				instance.TransitionState(state); | ||||
| 			} catch (Exception e) { | ||||
| 				instance.logger.Error(e, "Caught exception during state transition."); | ||||
| 			} finally { | ||||
| 				instance.stateTransitioningActionSemaphore.Release(); | ||||
| 			} | ||||
|   | ||||
| @@ -19,10 +19,10 @@ abstract class InstanceContext { | ||||
| 		Launcher = launcher; | ||||
| 	} | ||||
|  | ||||
| 	public abstract void ReportStatus(InstanceStatus newStatus); | ||||
| 	public abstract void TransitionState(Func<IInstanceState> newState); | ||||
| 	public abstract void ReportStatus(IInstanceStatus newStatus); | ||||
| 	public abstract void TransitionState(Func<(IInstanceState, IInstanceStatus?)> newStateAndStatus); | ||||
|  | ||||
| 	public void TransitionState(IInstanceState newState) { | ||||
| 		TransitionState(() => newState); | ||||
| 	public void TransitionState(IInstanceState newState, IInstanceStatus? newStatus = null) { | ||||
| 		TransitionState(() => (newState, newStatus)); | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										96
									
								
								Agent/Phantom.Agent.Services/Instances/InstanceLogSender.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								Agent/Phantom.Agent.Services/Instances/InstanceLogSender.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Phantom.Agent.Rpc; | ||||
| using Phantom.Common.Logging; | ||||
| using Phantom.Common.Messages.ToServer; | ||||
| using Phantom.Utils.Collections; | ||||
| using Phantom.Utils.Runtime; | ||||
| using Serilog; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances; | ||||
|  | ||||
| sealed class InstanceLogSender { | ||||
| 	private static readonly TimeSpan SendDelay = TimeSpan.FromMilliseconds(200); | ||||
|  | ||||
| 	private readonly Guid instanceGuid; | ||||
| 	private readonly ILogger logger; | ||||
| 	private readonly CancellationTokenSource cancellationTokenSource; | ||||
| 	private readonly CancellationToken cancellationToken; | ||||
|  | ||||
| 	private readonly SemaphoreSlim semaphore = new (1, 1); | ||||
| 	private readonly RingBuffer<string> buffer = new (1000); | ||||
|  | ||||
| 	public InstanceLogSender(TaskManager taskManager, Guid instanceGuid, string name) { | ||||
| 		this.instanceGuid = instanceGuid; | ||||
| 		this.logger = PhantomLogger.Create<InstanceLogSender>(name); | ||||
| 		this.cancellationTokenSource = new CancellationTokenSource(); | ||||
| 		this.cancellationToken = cancellationTokenSource.Token; | ||||
| 		taskManager.Run("Instance log sender for " + name, Run); | ||||
| 	} | ||||
|  | ||||
| 	private async Task Run() { | ||||
| 		logger.Verbose("Task started."); | ||||
|  | ||||
| 		try { | ||||
| 			try { | ||||
| 				while (!cancellationToken.IsCancellationRequested) { | ||||
| 					await SendOutputToServer(await DequeueOrThrow()); | ||||
| 					await Task.Delay(SendDelay, cancellationToken); | ||||
| 				} | ||||
| 			} catch (OperationCanceledException) { | ||||
| 				// Ignore. | ||||
| 			} | ||||
|  | ||||
| 			// Flush remaining lines. | ||||
| 			await SendOutputToServer(DequeueWithoutSemaphore()); | ||||
| 		} catch (Exception e) { | ||||
| 			logger.Error(e, "Caught exception in task."); | ||||
| 		} finally { | ||||
| 			cancellationTokenSource.Dispose(); | ||||
| 			logger.Verbose("Task stopped."); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private async Task SendOutputToServer(ImmutableArray<string> lines) { | ||||
| 		if (!lines.IsEmpty) { | ||||
| 			await ServerMessaging.Send(new InstanceOutputMessage(instanceGuid, lines)); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private ImmutableArray<string> DequeueWithoutSemaphore() { | ||||
| 		ImmutableArray<string> lines = buffer.Count > 0 ? buffer.EnumerateLast(uint.MaxValue).ToImmutableArray() : ImmutableArray<string>.Empty; | ||||
| 		buffer.Clear(); | ||||
| 		return lines; | ||||
| 	} | ||||
|  | ||||
| 	private async Task<ImmutableArray<string>> DequeueOrThrow() { | ||||
| 		await semaphore.WaitAsync(cancellationToken); | ||||
|  | ||||
| 		try { | ||||
| 			return DequeueWithoutSemaphore(); | ||||
| 		} finally { | ||||
| 			semaphore.Release(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public void Enqueue(string line) { | ||||
| 		try { | ||||
| 			semaphore.Wait(cancellationToken); | ||||
| 		} catch (Exception) { | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		try { | ||||
| 			buffer.Add(line); | ||||
| 		} finally { | ||||
| 			semaphore.Release(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public void Cancel() { | ||||
| 		try { | ||||
| 			cancellationTokenSource.Cancel(); | ||||
| 		} catch (ObjectDisposedException) { | ||||
| 			// Ignore. | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,84 +0,0 @@ | ||||
| using System.Collections.Immutable; | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using Phantom.Agent.Rpc; | ||||
| using Phantom.Common.Logging; | ||||
| using Phantom.Common.Messages.ToServer; | ||||
| using Phantom.Utils.Collections; | ||||
| using Serilog; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances;  | ||||
|  | ||||
| sealed class InstanceLogSenderThread { | ||||
| 	private readonly Guid instanceGuid; | ||||
| 	private readonly ILogger logger; | ||||
| 	private readonly CancellationTokenSource cancellationTokenSource; | ||||
| 	private readonly CancellationToken cancellationToken; | ||||
| 	 | ||||
| 	private readonly SemaphoreSlim semaphore = new (1, 1); | ||||
| 	private readonly RingBuffer<string> buffer = new (1000); | ||||
| 	 | ||||
| 	public InstanceLogSenderThread(Guid instanceGuid, string name) { | ||||
| 		this.instanceGuid = instanceGuid; | ||||
| 		this.logger = PhantomLogger.Create<InstanceLogSenderThread>(name); | ||||
| 		this.cancellationTokenSource = new CancellationTokenSource(); | ||||
| 		this.cancellationToken = cancellationTokenSource.Token; | ||||
| 		 | ||||
| 		var thread = new Thread(Run) { | ||||
| 			IsBackground = true, | ||||
| 			Name = "Instance Log Sender (" + name + ")" | ||||
| 		}; | ||||
| 		 | ||||
| 		thread.Start(); | ||||
| 	} | ||||
|  | ||||
| 	[SuppressMessage("ReSharper", "LocalVariableHidesMember")] | ||||
| 	private async void Run() { | ||||
| 		logger.Verbose("Thread started."); | ||||
| 		 | ||||
| 		try { | ||||
| 			while (!cancellationToken.IsCancellationRequested) { | ||||
| 				await semaphore.WaitAsync(cancellationToken); | ||||
|  | ||||
| 				ImmutableArray<string> lines; | ||||
| 				 | ||||
| 				try { | ||||
| 					lines = buffer.Count > 0 ? buffer.EnumerateLast(uint.MaxValue).ToImmutableArray() : ImmutableArray<string>.Empty; | ||||
| 					buffer.Clear(); | ||||
| 				} finally { | ||||
| 					semaphore.Release(); | ||||
| 				} | ||||
|  | ||||
| 				if (lines.Length > 0) { | ||||
| 					await ServerMessaging.SendMessage(new InstanceOutputMessage(instanceGuid, lines)); | ||||
| 				} | ||||
|  | ||||
| 				await Task.Delay(TimeSpan.FromMilliseconds(200), cancellationToken); | ||||
| 			} | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			// Ignore. | ||||
| 		} catch (Exception e) { | ||||
| 			logger.Error(e, "Caught exception in thread."); | ||||
| 		} finally { | ||||
| 			cancellationTokenSource.Dispose(); | ||||
| 			logger.Verbose("Thread stopped."); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public void Enqueue(string line) { | ||||
| 		try { | ||||
| 			semaphore.Wait(cancellationToken); | ||||
| 		} catch (Exception) { | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		try { | ||||
| 			buffer.Add(line); | ||||
| 		} finally { | ||||
| 			semaphore.Release(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public void Cancel() { | ||||
| 		cancellationTokenSource.Cancel(); | ||||
| 	} | ||||
| } | ||||
| @@ -1,4 +1,5 @@ | ||||
| using Phantom.Agent.Minecraft.Instance; | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using Phantom.Agent.Minecraft.Instance; | ||||
| using Phantom.Agent.Minecraft.Java; | ||||
| using Phantom.Agent.Minecraft.Launcher; | ||||
| using Phantom.Agent.Minecraft.Launcher.Types; | ||||
| @@ -7,8 +8,11 @@ using Phantom.Agent.Minecraft.Server; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Agent; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Common.Data.Minecraft; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Logging; | ||||
| using Phantom.Utils.IO; | ||||
| using Phantom.Utils.Runtime; | ||||
| using Serilog; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances; | ||||
| @@ -19,6 +23,7 @@ sealed class InstanceSessionManager : IDisposable { | ||||
| 	private readonly AgentInfo agentInfo; | ||||
| 	private readonly string basePath; | ||||
|  | ||||
| 	private readonly MinecraftServerExecutables minecraftServerExecutables; | ||||
| 	private readonly LaunchServices launchServices; | ||||
| 	private readonly PortManager portManager; | ||||
| 	private readonly Dictionary<Guid, Instance> instances = new (); | ||||
| @@ -27,32 +32,53 @@ sealed class InstanceSessionManager : IDisposable { | ||||
| 	private readonly CancellationToken shutdownCancellationToken; | ||||
| 	private readonly SemaphoreSlim semaphore = new (1, 1); | ||||
|  | ||||
| 	public InstanceSessionManager(AgentInfo agentInfo, AgentFolders agentFolders, JavaRuntimeRepository javaRuntimeRepository) { | ||||
| 	public InstanceSessionManager(AgentInfo agentInfo, AgentFolders agentFolders, JavaRuntimeRepository javaRuntimeRepository, TaskManager taskManager) { | ||||
| 		this.agentInfo = agentInfo; | ||||
| 		this.basePath = agentFolders.InstancesFolderPath; | ||||
| 		this.launchServices = new LaunchServices(new MinecraftServerExecutables(agentFolders.ServerExecutableFolderPath), javaRuntimeRepository); | ||||
| 		this.minecraftServerExecutables = new MinecraftServerExecutables(agentFolders.ServerExecutableFolderPath); | ||||
| 		this.launchServices = new LaunchServices(taskManager, minecraftServerExecutables, javaRuntimeRepository); | ||||
| 		this.portManager = new PortManager(agentInfo.AllowedServerPorts, agentInfo.AllowedRconPorts); | ||||
| 		this.shutdownCancellationToken = shutdownCancellationTokenSource.Token; | ||||
| 	} | ||||
|  | ||||
| 	public async Task<ConfigureInstanceResult> Configure(InstanceConfiguration configuration) { | ||||
| 	private async Task<InstanceActionResult<T>> AcquireSemaphoreAndRun<T>(Func<Task<InstanceActionResult<T>>> func) { | ||||
| 		try { | ||||
| 			await semaphore.WaitAsync(shutdownCancellationToken); | ||||
|  | ||||
| 			try { | ||||
| 				return await func(); | ||||
| 			} finally { | ||||
| 				semaphore.Release(); | ||||
| 			} | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			return ConfigureInstanceResult.AgentShuttingDown; | ||||
| 			return InstanceActionResult.General<T>(InstanceActionGeneralResult.AgentShuttingDown); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 		var instanceGuid = configuration.InstanceGuid; | ||||
| 	[SuppressMessage("ReSharper", "ConvertIfStatementToReturnStatement")] | ||||
| 	private Task<InstanceActionResult<T>> AcquireSemaphoreAndRunWithInstance<T>(Guid instanceGuid, Func<Instance, Task<T>> func) { | ||||
| 		return AcquireSemaphoreAndRun(async () => { | ||||
| 			if (instances.TryGetValue(instanceGuid, out var instance)) { | ||||
| 				return InstanceActionResult.Concrete(await func(instance)); | ||||
| 			} | ||||
| 			else { | ||||
| 				return InstanceActionResult.General<T>(InstanceActionGeneralResult.InstanceDoesNotExist); | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 		try { | ||||
| 	public async Task<InstanceActionResult<ConfigureInstanceResult>> Configure(InstanceConfiguration configuration) { | ||||
| 		return await AcquireSemaphoreAndRun(async () => { | ||||
| 			var instanceGuid = configuration.InstanceGuid; | ||||
| 			 | ||||
| 			var otherInstances = instances.Values.Where(inst => inst.Configuration.InstanceGuid != instanceGuid).ToArray(); | ||||
| 			if (otherInstances.Length + 1 > agentInfo.MaxInstances) { | ||||
| 				return ConfigureInstanceResult.InstanceLimitExceeded; | ||||
| 				return InstanceActionResult.Concrete(ConfigureInstanceResult.InstanceLimitExceeded); | ||||
| 			} | ||||
|  | ||||
| 			var availableMemory = agentInfo.MaxMemory - otherInstances.Aggregate(RamAllocationUnits.Zero, static (total, instance) => total + instance.Configuration.MemoryAllocation); | ||||
| 			if (availableMemory < configuration.MemoryAllocation) { | ||||
| 				return ConfigureInstanceResult.MemoryLimitExceeded; | ||||
| 				return InstanceActionResult.Concrete(ConfigureInstanceResult.MemoryLimitExceeded); | ||||
| 			} | ||||
|  | ||||
| 			var heapMegabytes = configuration.MemoryAllocation.InMegabytes; | ||||
| @@ -62,11 +88,12 @@ sealed class InstanceSessionManager : IDisposable { | ||||
| 			); | ||||
|  | ||||
| 			var instanceFolder = Path.Combine(basePath, instanceGuid.ToString()); | ||||
| 			Directory.CreateDirectory(instanceFolder); | ||||
| 			Directories.Create(instanceFolder, Chmod.URWX_GRX); | ||||
|  | ||||
| 			var properties = new InstanceProperties( | ||||
| 				configuration.JavaRuntimeGuid, | ||||
| 				jvmProperties, | ||||
| 				configuration.JvmArguments, | ||||
| 				instanceFolder, | ||||
| 				configuration.MinecraftVersion, | ||||
| 				new ServerProperties(configuration.ServerPort, configuration.RconPort) | ||||
| @@ -87,74 +114,26 @@ sealed class InstanceSessionManager : IDisposable { | ||||
| 				await instance.Launch(shutdownCancellationToken); | ||||
| 			} | ||||
|  | ||||
| 			return ConfigureInstanceResult.Success; | ||||
| 		} finally { | ||||
| 			semaphore.Release(); | ||||
| 		} | ||||
| 			return InstanceActionResult.Concrete(ConfigureInstanceResult.Success); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	public async Task<LaunchInstanceResult> Launch(Guid instanceGuid) { | ||||
| 		try { | ||||
| 			await semaphore.WaitAsync(shutdownCancellationToken); | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			return LaunchInstanceResult.AgentShuttingDown; | ||||
| 		} | ||||
|  | ||||
| 		try { | ||||
| 			if (!instances.TryGetValue(instanceGuid, out var instance)) { | ||||
| 				return LaunchInstanceResult.InstanceDoesNotExist; | ||||
| 			} | ||||
| 			else { | ||||
| 				return await instance.Launch(shutdownCancellationToken); | ||||
| 			} | ||||
| 		} finally { | ||||
| 			semaphore.Release(); | ||||
| 		} | ||||
| 	public Task<InstanceActionResult<LaunchInstanceResult>> Launch(Guid instanceGuid) { | ||||
| 		return AcquireSemaphoreAndRunWithInstance(instanceGuid, instance => instance.Launch(shutdownCancellationToken)); | ||||
| 	} | ||||
|  | ||||
| 	public async Task<StopInstanceResult> Stop(Guid instanceGuid) { | ||||
| 		try { | ||||
| 			await semaphore.WaitAsync(shutdownCancellationToken); | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			return StopInstanceResult.AgentShuttingDown; | ||||
| 		} | ||||
|  | ||||
| 		try { | ||||
| 			if (!instances.TryGetValue(instanceGuid, out var instance)) { | ||||
| 				return StopInstanceResult.InstanceDoesNotExist; | ||||
| 			} | ||||
| 			else { | ||||
| 				return await instance.Stop(); | ||||
| 			} | ||||
| 		} finally { | ||||
| 			semaphore.Release(); | ||||
| 		} | ||||
| 	public Task<InstanceActionResult<StopInstanceResult>> Stop(Guid instanceGuid, MinecraftStopStrategy stopStrategy) { | ||||
| 		return AcquireSemaphoreAndRunWithInstance(instanceGuid, instance => instance.Stop(stopStrategy)); | ||||
| 	} | ||||
|  | ||||
| 	public async Task<SendCommandToInstanceResult> SendCommand(Guid instanceGuid, string command) { | ||||
| 		try { | ||||
| 			await semaphore.WaitAsync(shutdownCancellationToken); | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			return SendCommandToInstanceResult.AgentShuttingDown; | ||||
| 		} | ||||
|  | ||||
| 		try { | ||||
| 			if (!instances.TryGetValue(instanceGuid, out var instance)) { | ||||
| 				return SendCommandToInstanceResult.InstanceDoesNotExist; | ||||
| 			} | ||||
|  | ||||
| 			if (!await instance.SendCommand(command, shutdownCancellationToken)) { | ||||
| 				return SendCommandToInstanceResult.UnknownError; | ||||
| 			} | ||||
|  | ||||
| 			return SendCommandToInstanceResult.Success; | ||||
| 		} finally { | ||||
| 			semaphore.Release(); | ||||
| 		} | ||||
| 	public Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommand(Guid instanceGuid, string command) { | ||||
| 		return AcquireSemaphoreAndRunWithInstance(instanceGuid, async instance => await instance.SendCommand(command, shutdownCancellationToken) ? SendCommandToInstanceResult.Success : SendCommandToInstanceResult.UnknownError); | ||||
| 	} | ||||
|  | ||||
| 	public async Task StopAll() { | ||||
| 		shutdownCancellationTokenSource.Cancel(); | ||||
| 		 | ||||
| 		Logger.Information("Stopping all instances..."); | ||||
|  | ||||
| 		await semaphore.WaitAsync(CancellationToken.None); | ||||
| 		try { | ||||
| @@ -166,6 +145,7 @@ sealed class InstanceSessionManager : IDisposable { | ||||
| 	} | ||||
|  | ||||
| 	public void Dispose() { | ||||
| 		minecraftServerExecutables.Dispose(); | ||||
| 		shutdownCancellationTokenSource.Dispose(); | ||||
| 		semaphore.Dispose(); | ||||
| 	} | ||||
|   | ||||
| @@ -14,17 +14,28 @@ sealed class PortManager { | ||||
| 	} | ||||
|  | ||||
| 	public Result Reserve(InstanceConfiguration configuration) { | ||||
| 		var serverPort = configuration.ServerPort; | ||||
| 		var rconPort = configuration.RconPort; | ||||
| 		 | ||||
| 		if (!allowedServerPorts.Contains(serverPort)) { | ||||
| 			return Result.ServerPortNotAllowed; | ||||
| 		} | ||||
|  | ||||
| 		if (!allowedRconPorts.Contains(rconPort)) { | ||||
| 			return Result.RconPortNotAllowed; | ||||
| 		} | ||||
| 		 | ||||
| 		lock (usedPorts) { | ||||
| 			if (usedPorts.Contains(configuration.ServerPort)) { | ||||
| 			if (usedPorts.Contains(serverPort)) { | ||||
| 				return Result.ServerPortAlreadyInUse; | ||||
| 			} | ||||
|  | ||||
| 			if (usedPorts.Contains(configuration.RconPort)) { | ||||
| 			if (usedPorts.Contains(rconPort)) { | ||||
| 				return Result.RconPortAlreadyInUse; | ||||
| 			} | ||||
|  | ||||
| 			usedPorts.Add(configuration.ServerPort); | ||||
| 			usedPorts.Add(configuration.RconPort); | ||||
| 			usedPorts.Add(serverPort); | ||||
| 			usedPorts.Add(rconPort); | ||||
| 		} | ||||
|  | ||||
| 		return Result.Success; | ||||
| @@ -42,6 +53,6 @@ sealed class PortManager { | ||||
| 		ServerPortNotAllowed, | ||||
| 		ServerPortAlreadyInUse, | ||||
| 		RconPortNotAllowed, | ||||
| 		RconPortAlreadyInUse, | ||||
| 		RconPortAlreadyInUse | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,11 @@ | ||||
| namespace Phantom.Agent.Services.Instances.States;  | ||||
| using Phantom.Common.Data.Minecraft; | ||||
| using Phantom.Common.Data.Replies; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances.States;  | ||||
|  | ||||
| interface IInstanceState { | ||||
| 	IInstanceState Launch(InstanceContext context); | ||||
| 	IInstanceState Stop(); | ||||
| 	void Initialize(); | ||||
| 	(IInstanceState, LaunchInstanceResult) Launch(InstanceContext context); | ||||
| 	(IInstanceState, StopInstanceResult) Stop(MinecraftStopStrategy stopStrategy); | ||||
| 	Task<bool> SendCommand(string command, CancellationToken cancellationToken); | ||||
| } | ||||
|   | ||||
| @@ -2,6 +2,8 @@ | ||||
| using Phantom.Agent.Minecraft.Launcher; | ||||
| using Phantom.Agent.Minecraft.Server; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Common.Data.Minecraft; | ||||
| using Phantom.Common.Data.Replies; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances.States; | ||||
|  | ||||
| @@ -12,10 +14,12 @@ sealed class InstanceLaunchingState : IInstanceState, IDisposable { | ||||
|  | ||||
| 	public InstanceLaunchingState(InstanceContext context) { | ||||
| 		this.context = context; | ||||
| 		this.context.Logger.Information("Session starting..."); | ||||
| 		this.context.ReportStatus(InstanceStatus.IsLaunching); | ||||
| 		 | ||||
| 		var launchTask = Task.Run(DoLaunch); | ||||
| 	} | ||||
|  | ||||
| 	public void Initialize() { | ||||
| 		context.Logger.Information("Session starting..."); | ||||
|  | ||||
| 		var launchTask = context.LaunchServices.TaskManager.Run("Launch procedure for instance " + context.ShortName, DoLaunch); | ||||
| 		launchTask.ContinueWith(OnLaunchSuccess, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default); | ||||
| 		launchTask.ContinueWith(OnLaunchFailure, CancellationToken.None, TaskContinuationOptions.NotOnRanToCompletion, TaskScheduler.Default); | ||||
| 	} | ||||
| @@ -23,29 +27,38 @@ sealed class InstanceLaunchingState : IInstanceState, IDisposable { | ||||
| 	private async Task<InstanceSession> DoLaunch() { | ||||
| 		var cancellationToken = cancellationTokenSource.Token; | ||||
| 		cancellationToken.ThrowIfCancellationRequested(); | ||||
| 		 | ||||
|  | ||||
| 		void OnDownloadProgress(object? sender, DownloadProgressEventArgs args) { | ||||
| 			byte progress = (byte) Math.Min(args.DownloadedBytes * 100 / args.TotalBytes, 100); | ||||
| 			 | ||||
|  | ||||
| 			if (lastDownloadProgress != progress) { | ||||
| 				lastDownloadProgress = progress; | ||||
| 				context.ReportStatus(new InstanceStatus.Downloading(progress)); | ||||
| 				context.ReportStatus(InstanceStatus.Downloading(progress)); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		var launchResult = await context.Launcher.Launch(context.LaunchServices, OnDownloadProgress, cancellationToken); | ||||
| 		if (launchResult is LaunchResult.CouldNotDownloadMinecraftServer) { | ||||
| 		var launchResult = await context.Launcher.Launch(context.Logger, context.LaunchServices, OnDownloadProgress, cancellationToken); | ||||
| 		if (launchResult is LaunchResult.InvalidJavaRuntime) { | ||||
| 			throw new LaunchFailureException(InstanceLaunchFailReason.JavaRuntimeNotFound, "Session failed to launch, invalid Java runtime."); | ||||
| 		} | ||||
| 		else if (launchResult is LaunchResult.InvalidJvmArguments) { | ||||
| 			throw new LaunchFailureException(InstanceLaunchFailReason.InvalidJvmArguments, "Session failed to launch, invalid JVM arguments."); | ||||
| 		} | ||||
| 		else if (launchResult is LaunchResult.CouldNotDownloadMinecraftServer) { | ||||
| 			throw new LaunchFailureException(InstanceLaunchFailReason.CouldNotDownloadMinecraftServer, "Session failed to launch, could not download Minecraft server."); | ||||
| 		} | ||||
| 		else if (launchResult is LaunchResult.InvalidJavaRuntime) { | ||||
| 			throw new LaunchFailureException(InstanceLaunchFailReason.JavaRuntimeNotFound, "Session failed to launch, invalid Java runtime."); | ||||
| 		else if (launchResult is LaunchResult.CouldNotConfigureMinecraftServer) { | ||||
| 			throw new LaunchFailureException(InstanceLaunchFailReason.CouldNotConfigureMinecraftServer, "Session failed to launch, could not configure Minecraft server."); | ||||
| 		} | ||||
| 		else if (launchResult is LaunchResult.CouldNotStartMinecraftServer) { | ||||
| 			throw new LaunchFailureException(InstanceLaunchFailReason.CouldNotStartMinecraftServer, "Session failed to launch, could not start Minecraft server."); | ||||
| 		} | ||||
|  | ||||
| 		if (launchResult is not LaunchResult.Success launchSuccess) { | ||||
| 			throw new LaunchFailureException(InstanceLaunchFailReason.UnknownError, "Session failed to launch."); | ||||
| 		} | ||||
|  | ||||
| 		context.ReportStatus(InstanceStatus.IsLaunching); | ||||
| 		context.ReportStatus(InstanceStatus.Launching); | ||||
| 		return launchSuccess.Session; | ||||
| 	} | ||||
|  | ||||
| @@ -53,11 +66,10 @@ sealed class InstanceLaunchingState : IInstanceState, IDisposable { | ||||
| 		context.TransitionState(() => { | ||||
| 			if (cancellationTokenSource.IsCancellationRequested) { | ||||
| 				context.PortManager.Release(context.Configuration); | ||||
| 				context.ReportStatus(InstanceStatus.IsNotRunning); | ||||
| 				return new InstanceNotRunningState(); | ||||
| 				return (new InstanceNotRunningState(), InstanceStatus.NotRunning); | ||||
| 			} | ||||
| 			else { | ||||
| 				return new InstanceRunningState(context, task.Result); | ||||
| 				return (new InstanceRunningState(context, task.Result), null); | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
| @@ -65,12 +77,12 @@ sealed class InstanceLaunchingState : IInstanceState, IDisposable { | ||||
| 	private void OnLaunchFailure(Task task) { | ||||
| 		if (task.Exception is { InnerException: LaunchFailureException e }) { | ||||
| 			context.Logger.Error(e.LogMessage); | ||||
| 			context.ReportStatus(new InstanceStatus.Failed(e.Reason)); | ||||
| 			context.ReportStatus(InstanceStatus.Failed(e.Reason)); | ||||
| 		} | ||||
| 		else { | ||||
| 			context.ReportStatus(new InstanceStatus.Failed(InstanceLaunchFailReason.UnknownError)); | ||||
| 			context.ReportStatus(InstanceStatus.Failed(InstanceLaunchFailReason.UnknownError)); | ||||
| 		} | ||||
| 		 | ||||
|  | ||||
| 		context.PortManager.Release(context.Configuration); | ||||
| 		context.TransitionState(new InstanceNotRunningState()); | ||||
| 	} | ||||
| @@ -78,20 +90,20 @@ sealed class InstanceLaunchingState : IInstanceState, IDisposable { | ||||
| 	private sealed class LaunchFailureException : Exception { | ||||
| 		public InstanceLaunchFailReason Reason { get; } | ||||
| 		public string LogMessage { get; } | ||||
| 		 | ||||
|  | ||||
| 		public LaunchFailureException(InstanceLaunchFailReason reason, string logMessage) { | ||||
| 			this.Reason = reason; | ||||
| 			this.LogMessage = logMessage; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public IInstanceState Launch(InstanceContext context) { | ||||
| 		return this; | ||||
| 	public (IInstanceState, LaunchInstanceResult) Launch(InstanceContext context) { | ||||
| 		return (this, LaunchInstanceResult.InstanceAlreadyLaunching); | ||||
| 	} | ||||
|  | ||||
| 	public IInstanceState Stop() { | ||||
| 	public (IInstanceState, StopInstanceResult) Stop(MinecraftStopStrategy stopStrategy) { | ||||
| 		cancellationTokenSource.Cancel(); | ||||
| 		return this; | ||||
| 		return (this, StopInstanceResult.StopInitiated); | ||||
| 	} | ||||
|  | ||||
| 	public Task<bool> SendCommand(string command, CancellationToken cancellationToken) { | ||||
|   | ||||
| @@ -1,9 +1,13 @@ | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Common.Data.Minecraft; | ||||
| using Phantom.Common.Data.Replies; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances.States;  | ||||
|  | ||||
| sealed class InstanceNotRunningState : IInstanceState { | ||||
| 	public IInstanceState Launch(InstanceContext context) { | ||||
| 	public void Initialize() {} | ||||
|  | ||||
| 	public (IInstanceState, LaunchInstanceResult) Launch(InstanceContext context) { | ||||
| 		InstanceLaunchFailReason? failReason = context.PortManager.Reserve(context.Configuration) switch { | ||||
| 			PortManager.Result.ServerPortNotAllowed   => InstanceLaunchFailReason.ServerPortNotAllowed, | ||||
| 			PortManager.Result.ServerPortAlreadyInUse => InstanceLaunchFailReason.ServerPortAlreadyInUse, | ||||
| @@ -13,15 +17,16 @@ sealed class InstanceNotRunningState : IInstanceState { | ||||
| 		}; | ||||
|  | ||||
| 		if (failReason != null) { | ||||
| 			context.ReportStatus(new InstanceStatus.Failed(failReason.Value)); | ||||
| 			return this; | ||||
| 			context.ReportStatus(InstanceStatus.Failed(failReason.Value)); | ||||
| 			return (this, LaunchInstanceResult.LaunchInitiated); | ||||
| 		} | ||||
| 		 | ||||
| 		return new InstanceLaunchingState(context); | ||||
| 		context.ReportStatus(InstanceStatus.Launching); | ||||
| 		return (new InstanceLaunchingState(context), LaunchInstanceResult.LaunchInitiated); | ||||
| 	} | ||||
|  | ||||
| 	public IInstanceState Stop() { | ||||
| 		return this; | ||||
| 	public (IInstanceState, StopInstanceResult) Stop(MinecraftStopStrategy stopStrategy) { | ||||
| 		return (this, StopInstanceResult.InstanceAlreadyStopped); | ||||
| 	} | ||||
|  | ||||
| 	public Task<bool> SendCommand(string command, CancellationToken cancellationToken) { | ||||
|   | ||||
| @@ -1,58 +1,127 @@ | ||||
| using Phantom.Agent.Minecraft.Instance; | ||||
| using Phantom.Agent.Minecraft.Command; | ||||
| using Phantom.Agent.Minecraft.Instance; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Common.Data.Minecraft; | ||||
| using Phantom.Common.Data.Replies; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances.States; | ||||
|  | ||||
| sealed class InstanceRunningState : IInstanceState { | ||||
| 	private readonly InstanceContext context; | ||||
| 	private readonly InstanceSession session; | ||||
| 	private readonly InstanceLogSenderThread logSenderThread; | ||||
| 	private readonly InstanceLogSender logSender; | ||||
| 	private readonly SessionObjects sessionObjects; | ||||
| 	 | ||||
| 	private readonly CancellationTokenSource delayedStopCancellationTokenSource = new (); | ||||
| 	private bool stateOwnsDelayedStopCancellationTokenSource = true; | ||||
| 	private bool isStopping; | ||||
|  | ||||
| 	public InstanceRunningState(InstanceContext context, InstanceSession session) { | ||||
| 		this.context = context; | ||||
| 		this.session = session; | ||||
| 		this.logSenderThread = new InstanceLogSenderThread(context.Configuration.InstanceGuid, context.ShortName); | ||||
| 		this.sessionObjects = new SessionObjects(context, session, logSenderThread); | ||||
|  | ||||
| 		this.session.AddOutputListener(SessionOutput); | ||||
| 		this.session.SessionEnded += SessionEnded; | ||||
| 		this.logSender = new InstanceLogSender(context.LaunchServices.TaskManager, context.Configuration.InstanceGuid, context.ShortName); | ||||
| 		this.sessionObjects = new SessionObjects(this); | ||||
| 	} | ||||
|  | ||||
| 	public void Initialize() { | ||||
| 		session.AddOutputListener(SessionOutput); | ||||
| 		session.SessionEnded += SessionEnded; | ||||
| 		 | ||||
| 		if (session.HasEnded) { | ||||
| 			if (sessionObjects.Dispose()) { | ||||
| 				context.Logger.Warning("Session ended immediately after it was started."); | ||||
| 				context.ReportStatus(new InstanceStatus.Failed(InstanceLaunchFailReason.UnknownError)); | ||||
| 				Task.Run(() => context.TransitionState(new InstanceNotRunningState())); | ||||
| 				context.LaunchServices.TaskManager.Run("Transition state of instance " + context.ShortName + " to not running", () => context.TransitionState(new InstanceNotRunningState(), InstanceStatus.Failed(InstanceLaunchFailReason.UnknownError))); | ||||
| 			} | ||||
| 		} | ||||
| 		else { | ||||
| 			context.ReportStatus(InstanceStatus.IsRunning); | ||||
| 			context.ReportStatus(InstanceStatus.Running); | ||||
| 			context.Logger.Information("Session started."); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private void SessionOutput(object? sender, string e) { | ||||
| 		context.Logger.Verbose("[Server] {Line}", e); | ||||
| 		logSenderThread.Enqueue(e); | ||||
| 		logSender.Enqueue(e); | ||||
| 	} | ||||
|  | ||||
| 	private void SessionEnded(object? sender, EventArgs e) { | ||||
| 		if (sessionObjects.Dispose()) { | ||||
| 		if (!sessionObjects.Dispose()) { | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		if (isStopping) { | ||||
| 			context.Logger.Information("Session ended."); | ||||
| 			context.ReportStatus(InstanceStatus.IsNotRunning); | ||||
| 			context.TransitionState(new InstanceNotRunningState()); | ||||
| 			context.TransitionState(new InstanceNotRunningState(), InstanceStatus.NotRunning); | ||||
| 		} | ||||
| 		else { | ||||
| 			context.Logger.Information("Session ended unexpectedly, restarting..."); | ||||
| 			context.TransitionState(new InstanceLaunchingState(context), InstanceStatus.Restarting); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public IInstanceState Launch(InstanceContext context) { | ||||
| 		return this; | ||||
| 	public (IInstanceState, LaunchInstanceResult) Launch(InstanceContext context) { | ||||
| 		return (this, LaunchInstanceResult.InstanceAlreadyRunning); | ||||
| 	} | ||||
|  | ||||
| 	public IInstanceState Stop() { | ||||
| 	public (IInstanceState, StopInstanceResult) Stop(MinecraftStopStrategy stopStrategy) { | ||||
| 		if (stopStrategy == MinecraftStopStrategy.Instant) { | ||||
| 			CancelDelayedStop(); | ||||
| 			return (PrepareStoppedState(), StopInstanceResult.StopInitiated); | ||||
| 		} | ||||
|  | ||||
| 		if (isStopping) { | ||||
| 			// TODO change delay or something | ||||
| 			return (this, StopInstanceResult.InstanceAlreadyStopping); | ||||
| 		} | ||||
| 		 | ||||
| 		isStopping = true; | ||||
| 		context.LaunchServices.TaskManager.Run("Delayed stop timer for instance " + context.ShortName, () => StopLater(stopStrategy.Seconds)); | ||||
| 		return (this, StopInstanceResult.StopInitiated); | ||||
| 	} | ||||
|  | ||||
| 	private IInstanceState PrepareStoppedState() { | ||||
| 		session.SessionEnded -= SessionEnded; | ||||
| 		return new InstanceStoppingState(context, session, sessionObjects); | ||||
| 	} | ||||
|  | ||||
| 	private void CancelDelayedStop() { | ||||
| 		try { | ||||
| 			delayedStopCancellationTokenSource.Cancel(); | ||||
| 		} catch (ObjectDisposedException) { | ||||
| 			// ignore | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private async Task StopLater(int seconds) { | ||||
| 		var cancellationToken = delayedStopCancellationTokenSource.Token; | ||||
|  | ||||
| 		try { | ||||
| 			stateOwnsDelayedStopCancellationTokenSource = false; | ||||
|  | ||||
| 			int[] stops = { 60, 30, 10, 5, 4, 3, 2, 1, 0 }; | ||||
| 			 | ||||
| 			foreach (var stop in stops) { | ||||
| 				if (seconds > stop) { | ||||
| 					await SendCommand(MinecraftCommand.Say("Server shutting down in " + seconds + (seconds == 1 ? " second." : " seconds.")), cancellationToken); | ||||
| 					await Task.Delay(TimeSpan.FromSeconds(seconds - stop), cancellationToken); | ||||
| 					seconds = stop; | ||||
| 				} | ||||
| 			} | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			context.Logger.Verbose("Cancelled delayed stop."); | ||||
| 			return; | ||||
| 		} catch (ObjectDisposedException) { | ||||
| 			return; | ||||
| 		} catch (Exception e) { | ||||
| 			context.Logger.Warning(e, "Caught exception during delayed stop."); | ||||
| 			return; | ||||
| 		} finally { | ||||
| 			delayedStopCancellationTokenSource.Dispose(); | ||||
| 		} | ||||
|  | ||||
| 		context.TransitionState(PrepareStoppedState()); | ||||
| 	} | ||||
|  | ||||
| 	public async Task<bool> SendCommand(string command, CancellationToken cancellationToken) { | ||||
| 		try { | ||||
| 			context.Logger.Information("Sending command: {Command}", command); | ||||
| @@ -67,15 +136,11 @@ sealed class InstanceRunningState : IInstanceState { | ||||
| 	} | ||||
|  | ||||
| 	public sealed class SessionObjects { | ||||
| 		private readonly InstanceContext context; | ||||
| 		private readonly InstanceSession session; | ||||
| 		private readonly InstanceLogSenderThread logSenderThread; | ||||
| 		private readonly InstanceRunningState state; | ||||
| 		private bool isDisposed; | ||||
|  | ||||
| 		public SessionObjects(InstanceContext context, InstanceSession session, InstanceLogSenderThread logSenderThread) { | ||||
| 			this.context = context; | ||||
| 			this.session = session; | ||||
| 			this.logSenderThread = logSenderThread; | ||||
| 		public SessionObjects(InstanceRunningState state) { | ||||
| 			this.state = state; | ||||
| 		} | ||||
|  | ||||
| 		public bool Dispose() { | ||||
| @@ -87,9 +152,16 @@ sealed class InstanceRunningState : IInstanceState { | ||||
| 				isDisposed = true; | ||||
| 			} | ||||
|  | ||||
| 			logSenderThread.Cancel(); | ||||
| 			session.Dispose(); | ||||
| 			context.PortManager.Release(context.Configuration); | ||||
| 			if (state.stateOwnsDelayedStopCancellationTokenSource) { | ||||
| 				state.delayedStopCancellationTokenSource.Dispose(); | ||||
| 			} | ||||
| 			else { | ||||
| 				state.CancelDelayedStop(); | ||||
| 			} | ||||
| 			 | ||||
| 			state.logSender.Cancel(); | ||||
| 			state.session.Dispose(); | ||||
| 			state.context.PortManager.Release(state.context.Configuration); | ||||
| 			return true; | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -1,5 +1,9 @@ | ||||
| using Phantom.Agent.Minecraft.Instance; | ||||
| using System.Diagnostics; | ||||
| using Phantom.Agent.Minecraft.Command; | ||||
| using Phantom.Agent.Minecraft.Instance; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Common.Data.Minecraft; | ||||
| using Phantom.Common.Data.Replies; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances.States;  | ||||
|  | ||||
| @@ -12,10 +16,12 @@ sealed class InstanceStoppingState : IInstanceState, IDisposable { | ||||
| 		this.sessionObjects = sessionObjects; | ||||
| 		this.session = session; | ||||
| 		this.context = context; | ||||
| 		this.context.Logger.Information("Session stopping."); | ||||
| 		this.context.ReportStatus(InstanceStatus.IsStopping); | ||||
| 		 | ||||
| 		Task.Run(DoStop); | ||||
| 	} | ||||
|  | ||||
| 	public void Initialize() { | ||||
| 		context.Logger.Information("Session stopping."); | ||||
| 		context.ReportStatus(InstanceStatus.Stopping); | ||||
| 		context.LaunchServices.TaskManager.Run("Stop procedure for instance " + context.ShortName, DoStop); | ||||
| 	} | ||||
|  | ||||
| 	private async Task DoStop() { | ||||
| @@ -27,26 +33,27 @@ sealed class InstanceStoppingState : IInstanceState, IDisposable { | ||||
| 			await DoWaitForSessionToEnd(); | ||||
| 		} finally { | ||||
| 			context.Logger.Information("Session stopped."); | ||||
| 			context.ReportStatus(InstanceStatus.IsNotRunning); | ||||
| 			context.TransitionState(new InstanceNotRunningState()); | ||||
| 			context.TransitionState(new InstanceNotRunningState(), InstanceStatus.NotRunning); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private async Task DoSendStopCommand() { | ||||
| 		using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); | ||||
| 		using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); | ||||
| 		try { | ||||
| 			await session.SendCommand("stop", cts.Token); | ||||
| 			await session.SendCommand(MinecraftCommand.Stop, timeout.Token); | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			// ignore | ||||
| 		} catch (ObjectDisposedException e) when (e.ObjectName == typeof(Process).FullName && session.HasEnded) { | ||||
| 			// ignore | ||||
| 		} catch (Exception e) { | ||||
| 			context.Logger.Warning(e, "Caught exception while sending stop command."); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private async Task DoWaitForSessionToEnd() { | ||||
| 		using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(55)); | ||||
| 		using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(55)); | ||||
| 		try { | ||||
| 			await session.WaitForExit(cts.Token); | ||||
| 			await session.WaitForExit(timeout.Token); | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			try { | ||||
| 				context.Logger.Warning("Waiting timed out, killing session..."); | ||||
| @@ -57,12 +64,12 @@ sealed class InstanceStoppingState : IInstanceState, IDisposable { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public IInstanceState Launch(InstanceContext context) { | ||||
| 		return this; | ||||
| 	public (IInstanceState, LaunchInstanceResult) Launch(InstanceContext context) { | ||||
| 		return (this, LaunchInstanceResult.InstanceIsStopping); | ||||
| 	} | ||||
|  | ||||
| 	public IInstanceState Stop() { | ||||
| 		return this; // TODO maybe provide a way to kill? | ||||
| 	public (IInstanceState, StopInstanceResult) Stop(MinecraftStopStrategy stopStrategy) { | ||||
| 		return (this, StopInstanceResult.InstanceAlreadyStopping); // TODO maybe provide a way to kill? | ||||
| 	} | ||||
|  | ||||
| 	public Task<bool> SendCommand(string command, CancellationToken cancellationToken) { | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| using NetMQ.Sockets; | ||||
| using Phantom.Agent.Rpc; | ||||
| using Phantom.Agent.Rpc; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Logging; | ||||
| using Phantom.Common.Messages; | ||||
| using Phantom.Common.Messages.BiDirectional; | ||||
| using Phantom.Common.Messages.ToAgent; | ||||
| using Phantom.Common.Messages.ToServer; | ||||
| using Phantom.Utils.Rpc.Message; | ||||
| using Serilog; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Rpc; | ||||
| @@ -12,32 +13,34 @@ namespace Phantom.Agent.Services.Rpc; | ||||
| public sealed class MessageListener : IMessageToAgentListener { | ||||
| 	private static ILogger Logger { get; } = PhantomLogger.Create<MessageListener>(); | ||||
|  | ||||
| 	private readonly ClientSocket socket; | ||||
| 	private readonly RpcServerConnection connection; | ||||
| 	private readonly AgentServices agent; | ||||
| 	private readonly CancellationTokenSource shutdownTokenSource; | ||||
|  | ||||
| 	public MessageListener(ClientSocket socket, AgentServices agent, CancellationTokenSource shutdownTokenSource) { | ||||
| 		this.socket = socket; | ||||
| 	public MessageListener(RpcServerConnection connection, AgentServices agent, CancellationTokenSource shutdownTokenSource) { | ||||
| 		this.connection = connection; | ||||
| 		this.agent = agent; | ||||
| 		this.shutdownTokenSource = shutdownTokenSource; | ||||
| 	} | ||||
|  | ||||
| 	public async Task HandleRegisterAgentSuccessResult(RegisterAgentSuccessMessage message) { | ||||
| 	public async Task<NoReply> HandleRegisterAgentSuccess(RegisterAgentSuccessMessage message) { | ||||
| 		Logger.Information("Agent authentication successful."); | ||||
|  | ||||
| 		foreach (var instanceInfo in message.InitialInstances) { | ||||
| 			if (await agent.InstanceSessionManager.Configure(instanceInfo) != ConfigureInstanceResult.Success) { | ||||
| 			var result = await agent.InstanceSessionManager.Configure(instanceInfo); | ||||
| 			if (!result.Is(ConfigureInstanceResult.Success)) { | ||||
| 				Logger.Fatal("Unable to configure instance \"{Name}\" (GUID {Guid}), shutting down.", instanceInfo.InstanceName, instanceInfo.InstanceGuid); | ||||
|  | ||||
| 				shutdownTokenSource.Cancel(); | ||||
| 				return; | ||||
| 				return NoReply.Instance; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		await ServerMessaging.SendMessage(new AdvertiseJavaRuntimesMessage(agent.JavaRuntimeRepository.All)); | ||||
| 		await ServerMessaging.Send(new AdvertiseJavaRuntimesMessage(agent.JavaRuntimeRepository.All)); | ||||
| 		return NoReply.Instance; | ||||
| 	} | ||||
|  | ||||
| 	public Task HandleRegisterAgentFailureResult(RegisterAgentFailureMessage message) { | ||||
| 	public Task<NoReply> HandleRegisterAgentFailure(RegisterAgentFailureMessage message) { | ||||
| 		string errorMessage = message.FailureKind switch { | ||||
| 			RegisterAgentFailure.ConnectionAlreadyHasAnAgent => "This connection already has an associated agent.", | ||||
| 			RegisterAgentFailure.InvalidToken                => "Invalid token.", | ||||
| @@ -47,22 +50,27 @@ public sealed class MessageListener : IMessageToAgentListener { | ||||
| 		Logger.Fatal("Agent authentication failed: {Error}", errorMessage); | ||||
| 		Environment.Exit(1); | ||||
|  | ||||
| 		return Task.CompletedTask; | ||||
| 		return Task.FromResult(NoReply.Instance); | ||||
| 	} | ||||
| 	 | ||||
| 	public async Task HandleConfigureInstance(ConfigureInstanceMessage message) { | ||||
| 		await socket.SendSimpleReply(message, await agent.InstanceSessionManager.Configure(message.Configuration)); | ||||
| 	public async Task<InstanceActionResult<ConfigureInstanceResult>> HandleConfigureInstance(ConfigureInstanceMessage message) { | ||||
| 		return await agent.InstanceSessionManager.Configure(message.Configuration); | ||||
| 	} | ||||
|  | ||||
| 	public async Task HandleLaunchInstance(LaunchInstanceMessage message) { | ||||
| 		await socket.SendSimpleReply(message, await agent.InstanceSessionManager.Launch(message.InstanceGuid)); | ||||
| 	public async Task<InstanceActionResult<LaunchInstanceResult>> HandleLaunchInstance(LaunchInstanceMessage message) { | ||||
| 		return await agent.InstanceSessionManager.Launch(message.InstanceGuid); | ||||
| 	} | ||||
|  | ||||
| 	public async Task HandleStopInstance(StopInstanceMessage message) { | ||||
| 		await socket.SendSimpleReply(message, await agent.InstanceSessionManager.Stop(message.InstanceGuid)); | ||||
| 	public async Task<InstanceActionResult<StopInstanceResult>> HandleStopInstance(StopInstanceMessage message) { | ||||
| 		return await agent.InstanceSessionManager.Stop(message.InstanceGuid, message.StopStrategy); | ||||
| 	} | ||||
|  | ||||
| 	public async Task HandleSendCommandToInstance(SendCommandToInstanceMessage message) { | ||||
| 		await socket.SendSimpleReply(message, await agent.InstanceSessionManager.SendCommand(message.InstanceGuid, message.Command)); | ||||
| 	public async Task<InstanceActionResult<SendCommandToInstanceResult>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) { | ||||
| 		return await agent.InstanceSessionManager.SendCommand(message.InstanceGuid, message.Command); | ||||
| 	} | ||||
|  | ||||
| 	public Task<NoReply> HandleReply(ReplyMessage message) { | ||||
| 		connection.Receive(message); | ||||
| 		return Task.FromResult(NoReply.Instance); | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										60
									
								
								Agent/Phantom.Agent/AgentKey.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								Agent/Phantom.Agent/AgentKey.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| using NetMQ; | ||||
| using Phantom.Common.Data.Agent; | ||||
| using Phantom.Common.Logging; | ||||
| using Phantom.Utils.Cryptography; | ||||
| using Phantom.Utils.IO; | ||||
| using Serilog; | ||||
|  | ||||
| namespace Phantom.Agent; | ||||
|  | ||||
| static class AgentKey { | ||||
| 	private static ILogger Logger { get; } = PhantomLogger.Create(nameof(AgentKey)); | ||||
|  | ||||
| 	public static Task<(NetMQCertificate, AgentAuthToken)?> Load(string? agentKeyToken, string? agentKeyFilePath) { | ||||
| 		if (agentKeyFilePath != null) { | ||||
| 			return LoadFromFile(agentKeyFilePath); | ||||
| 		} | ||||
| 		else if (agentKeyToken != null) { | ||||
| 			return Task.FromResult(LoadFromToken(agentKeyToken)); | ||||
| 		} | ||||
| 		else { | ||||
| 			throw new InvalidOperationException(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private static async Task<(NetMQCertificate, AgentAuthToken)?> LoadFromFile(string agentKeyFilePath) { | ||||
| 		if (!File.Exists(agentKeyFilePath)) { | ||||
| 			Logger.Fatal("Missing agent key file: {AgentKeyFilePath}", agentKeyFilePath); | ||||
| 			return null; | ||||
| 		} | ||||
|  | ||||
| 		try { | ||||
| 			Files.RequireMaximumFileSize(agentKeyFilePath, 64); | ||||
| 			return LoadFromBytes(await File.ReadAllBytesAsync(agentKeyFilePath)); | ||||
| 		} catch (IOException e) { | ||||
| 			Logger.Fatal("Error loading agent key from file: {AgentKeyFilePath}", agentKeyFilePath); | ||||
| 			Logger.Fatal(e.Message); | ||||
| 			return null; | ||||
| 		} catch (Exception) { | ||||
| 			Logger.Fatal("File does not contain a valid agent key: {AgentKeyFilePath}", agentKeyFilePath); | ||||
| 			return null; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private static (NetMQCertificate, AgentAuthToken)? LoadFromToken(string agentKey) { | ||||
| 		try { | ||||
| 			return LoadFromBytes(TokenGenerator.DecodeBytes(agentKey)); | ||||
| 		} catch (Exception) { | ||||
| 			Logger.Fatal("Invalid agent key: {AgentKey}", agentKey); | ||||
| 			return null; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private static (NetMQCertificate, AgentAuthToken)? LoadFromBytes(byte[] agentKey) { | ||||
| 		var (publicKey, agentToken) = AgentKeyData.FromBytes(agentKey); | ||||
| 		var serverCertificate = NetMQCertificate.FromPublicKey(publicKey); | ||||
| 		 | ||||
| 		Logger.Information("Loaded agent key."); | ||||
| 		return (serverCertificate, agentToken); | ||||
| 	} | ||||
| } | ||||
| @@ -1,32 +0,0 @@ | ||||
| using NetMQ; | ||||
| using Phantom.Common.Logging; | ||||
| using Phantom.Utils.IO; | ||||
| using Serilog; | ||||
|  | ||||
| namespace Phantom.Agent; | ||||
|  | ||||
| static class CertificateFile { | ||||
| 	private static ILogger Logger { get; } = PhantomLogger.Create(typeof(CertificateFile)); | ||||
|  | ||||
| 	public static async Task<NetMQCertificate?> LoadPublicKey(string publicKeyFilePath) { | ||||
| 		if (!File.Exists(publicKeyFilePath)) { | ||||
| 			Logger.Fatal("Cannot load server certificate, missing key file: {PublicKeyFilePath}", publicKeyFilePath); | ||||
| 			return null; | ||||
| 		} | ||||
|  | ||||
| 		try { | ||||
| 			var publicKey = await LoadPublicKeyFromFile(publicKeyFilePath); | ||||
| 			Logger.Information("Loaded server certificate."); | ||||
| 			return publicKey; | ||||
| 		} catch (Exception e) { | ||||
| 			Logger.Fatal(e, "Error loading server certificate from key file: {PublicKeyFilePath}", publicKeyFilePath); | ||||
| 			return null; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private static async Task<NetMQCertificate> LoadPublicKeyFromFile(string filePath) { | ||||
| 		Files.RequireMaximumFileSize(filePath, 1024); | ||||
| 		byte[] publicKey = await File.ReadAllBytesAsync(filePath); | ||||
| 		return NetMQCertificate.FromPublicKey(publicKey); | ||||
| 	} | ||||
| } | ||||
| @@ -6,7 +6,7 @@ using Serilog; | ||||
| namespace Phantom.Agent;  | ||||
|  | ||||
| static class GuidFile { | ||||
| 	private static ILogger Logger { get; } = PhantomLogger.Create(typeof(GuidFile)); | ||||
| 	private static ILogger Logger { get; } = PhantomLogger.Create(nameof(GuidFile)); | ||||
|  | ||||
| 	private const string GuidFileName = "agent.guid"; | ||||
| 	 | ||||
|   | ||||
| @@ -8,6 +8,7 @@ | ||||
|    | ||||
|   <PropertyGroup> | ||||
|     <OutputType>Exe</OutputType> | ||||
|     <SatelliteResourceLanguages>en</SatelliteResourceLanguages> | ||||
|   </PropertyGroup> | ||||
|    | ||||
|   <ItemGroup> | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using Phantom.Agent; | ||||
| using System.Reflection; | ||||
| using Phantom.Agent; | ||||
| using Phantom.Agent.Rpc; | ||||
| using Phantom.Agent.Services; | ||||
| using Phantom.Agent.Services.Rpc; | ||||
| @@ -7,31 +8,25 @@ using Phantom.Common.Logging; | ||||
| using Phantom.Utils.Rpc; | ||||
| using Phantom.Utils.Runtime; | ||||
|  | ||||
| const int AgentVersion = 1; | ||||
| const int ProtocolVersion = 1; | ||||
|  | ||||
| var cancellationTokenSource = new CancellationTokenSource(); | ||||
| var shutdownCancellationTokenSource = new CancellationTokenSource(); | ||||
| var shutdownCancellationToken = shutdownCancellationTokenSource.Token; | ||||
|  | ||||
| PosixSignals.RegisterCancellation(cancellationTokenSource, static () => { | ||||
| PosixSignals.RegisterCancellation(shutdownCancellationTokenSource, static () => { | ||||
| 	PhantomLogger.Root.InformationHeading("Stopping Phantom Panel agent..."); | ||||
| }); | ||||
|  | ||||
| try { | ||||
| 	var fullVersion = AssemblyAttributes.GetFullVersion(Assembly.GetExecutingAssembly()); | ||||
|  | ||||
| 	PhantomLogger.Root.InformationHeading("Initializing Phantom Panel agent..."); | ||||
| 	PhantomLogger.Root.Information("Agent version: {Version}", fullVersion); | ||||
|  | ||||
| 	var (serverHost, serverPort, javaSearchPath, authToken, authTokenFilePath, agentName, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts) = Variables.LoadOrExit(); | ||||
| 	 | ||||
| 	AgentAuthToken agentAuthToken; | ||||
| 	try { | ||||
| 		agentAuthToken = authTokenFilePath == null ? new AgentAuthToken(authToken) : await AgentAuthToken.ReadFromFile(authTokenFilePath); | ||||
| 	} catch (Exception e) { | ||||
| 		PhantomLogger.Root.Fatal(e, "Error reading auth token."); | ||||
| 		Environment.Exit(1); | ||||
| 		return; | ||||
| 	} | ||||
| 	var (serverHost, serverPort, javaSearchPath, agentKeyToken, agentKeyFilePath, agentName, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts) = Variables.LoadOrExit(); | ||||
|  | ||||
| 	string serverPublicKeyPath = Path.GetFullPath("./secrets/agent.key"); | ||||
| 	var serverCertificate = await CertificateFile.LoadPublicKey(serverPublicKeyPath); | ||||
| 	if (serverCertificate == null) { | ||||
| 	var agentKey = await AgentKey.Load(agentKeyToken, agentKeyFilePath); | ||||
| 	if (agentKey == null) { | ||||
| 		Environment.Exit(1); | ||||
| 	} | ||||
|  | ||||
| @@ -43,21 +38,40 @@ try { | ||||
| 	var agentGuid = await GuidFile.CreateOrLoad(folders.DataFolderPath); | ||||
| 	if (agentGuid == null) { | ||||
| 		Environment.Exit(1); | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	var agentInfo = new AgentInfo(agentGuid.Value, agentName, AgentVersion, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts); | ||||
| 	var (serverCertificate, agentToken) = agentKey.Value; | ||||
| 	var agentInfo = new AgentInfo(agentGuid.Value, agentName, ProtocolVersion, fullVersion, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts); | ||||
| 	var agentServices = new AgentServices(agentInfo, folders); | ||||
|  | ||||
| 	MessageListener MessageListenerFactory(RpcServerConnection connection) { | ||||
| 		return new MessageListener(connection, agentServices, shutdownCancellationTokenSource); | ||||
| 	} | ||||
|  | ||||
| 	PhantomLogger.Root.InformationHeading("Launching Phantom Panel agent..."); | ||||
| 	 | ||||
|  | ||||
| 	await agentServices.Initialize(); | ||||
| 	await RpcLauncher.Launch(new RpcConfiguration(PhantomLogger.Create("Rpc"), serverHost, serverPort, serverCertificate, cancellationTokenSource.Token), agentAuthToken, agentInfo, socket => new MessageListener(socket, agentServices, cancellationTokenSource)); | ||||
| 	await agentServices.Shutdown(); | ||||
|  | ||||
| 	var rpcDisconnectSemaphore = new SemaphoreSlim(0, 1); | ||||
| 	var rpcConfiguration = new RpcConfiguration(PhantomLogger.Create("Rpc"), PhantomLogger.Create<TaskManager>("Rpc"), serverHost, serverPort, serverCertificate); | ||||
| 	var rpcTask = RpcLauncher.Launch(rpcConfiguration, agentToken, agentInfo, MessageListenerFactory, rpcDisconnectSemaphore, shutdownCancellationToken); | ||||
| 	try { | ||||
| 		await rpcTask.WaitAsync(shutdownCancellationToken); | ||||
| 	} finally { | ||||
| 		shutdownCancellationTokenSource.Cancel(); | ||||
| 		await agentServices.Shutdown(); | ||||
|  | ||||
| 		rpcDisconnectSemaphore.Release(); | ||||
| 		await rpcTask; | ||||
| 		rpcDisconnectSemaphore.Dispose(); | ||||
| 	} | ||||
| } catch (OperationCanceledException) { | ||||
| 	// Ignore. | ||||
| } catch (Exception e) { | ||||
| 	PhantomLogger.Root.Fatal(e, "Caught exception in entry point."); | ||||
| } finally { | ||||
| 	cancellationTokenSource.Dispose(); | ||||
| 	shutdownCancellationTokenSource.Dispose(); | ||||
|  | ||||
| 	PhantomLogger.Root.Information("Bye!"); | ||||
| 	PhantomLogger.Dispose(); | ||||
| } | ||||
|   | ||||
| @@ -9,8 +9,8 @@ sealed record Variables( | ||||
| 	string ServerHost, | ||||
| 	ushort ServerPort, | ||||
| 	string JavaSearchPath, | ||||
| 	string? AuthToken, | ||||
| 	string? AuthTokenFilePath, | ||||
| 	string? AgentKeyToken, | ||||
| 	string? AgentKeyFilePath, | ||||
| 	string AgentName, | ||||
| 	ushort MaxInstances, | ||||
| 	RamAllocationUnits MaxMemory, | ||||
| @@ -18,20 +18,20 @@ sealed record Variables( | ||||
| 	AllowedPorts AllowedRconPorts | ||||
| ) { | ||||
| 	private static Variables LoadOrThrow() { | ||||
| 		var (authToken, authTokenFilePath) = EnvironmentVariables.GetEitherString("SERVER_AUTH_TOKEN", "SERVER_AUTH_TOKEN_FILE").OrThrow; | ||||
| 		var javaSearchPath = EnvironmentVariables.GetString("JAVA_SEARCH_PATH").OrGetDefault(GetDefaultJavaSearchPath); | ||||
| 		var (agentKeyToken, agentKeyFilePath) = EnvironmentVariables.GetEitherString("AGENT_KEY", "AGENT_KEY_FILE").Require; | ||||
| 		var javaSearchPath = EnvironmentVariables.GetString("JAVA_SEARCH_PATH").WithDefaultGetter(GetDefaultJavaSearchPath); | ||||
|  | ||||
| 		return new Variables( | ||||
| 			EnvironmentVariables.GetString("SERVER_HOST").OrThrow, | ||||
| 			EnvironmentVariables.GetPortNumber("SERVER_PORT").OrDefault(9401), | ||||
| 			EnvironmentVariables.GetString("SERVER_HOST").Require, | ||||
| 			EnvironmentVariables.GetPortNumber("SERVER_PORT").WithDefault(9401), | ||||
| 			javaSearchPath, | ||||
| 			authToken, | ||||
| 			authTokenFilePath, | ||||
| 			EnvironmentVariables.GetString("AGENT_NAME").OrThrow, | ||||
| 			(ushort) EnvironmentVariables.GetInteger("MAX_INSTANCES", min: 1, max: 10000).OrThrow, | ||||
| 			EnvironmentVariables.GetString("MAX_MEMORY").MapParse(RamAllocationUnits.FromString).OrThrow, | ||||
| 			EnvironmentVariables.GetString("ALLOWED_SERVER_PORTS").MapParse(AllowedPorts.FromString).OrThrow, | ||||
| 			EnvironmentVariables.GetString("ALLOWED_RCON_PORTS").MapParse(AllowedPorts.FromString).OrThrow | ||||
| 			agentKeyToken, | ||||
| 			agentKeyFilePath, | ||||
| 			EnvironmentVariables.GetString("AGENT_NAME").Require, | ||||
| 			(ushort) EnvironmentVariables.GetInteger("MAX_INSTANCES", min: 1, max: 10000).Require, | ||||
| 			EnvironmentVariables.GetString("MAX_MEMORY").MapParse(RamAllocationUnits.FromString).Require, | ||||
| 			EnvironmentVariables.GetString("ALLOWED_SERVER_PORTS").MapParse(AllowedPorts.FromString).Require, | ||||
| 			EnvironmentVariables.GetString("ALLOWED_RCON_PORTS").MapParse(AllowedPorts.FromString).Require | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -11,11 +11,11 @@ | ||||
|   </PropertyGroup> | ||||
|    | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> | ||||
|     <PackageReference Include="NUnit" Version="3.13.3" /> | ||||
|     <PackageReference Include="NUnit3TestAdapter" Version="4.2.1" /> | ||||
|     <PackageReference Include="NUnit.Analyzers" Version="3.3.0" /> | ||||
|     <PackageReference Include="coverlet.collector" Version="3.1.2" /> | ||||
|     <PackageReference Include="Microsoft.NET.Test.Sdk" /> | ||||
|     <PackageReference Include="NUnit" /> | ||||
|     <PackageReference Include="NUnit3TestAdapter" /> | ||||
|     <PackageReference Include="NUnit.Analyzers" /> | ||||
|     <PackageReference Include="coverlet.collector" /> | ||||
|   </ItemGroup> | ||||
|    | ||||
|   <ItemGroup> | ||||
|   | ||||
| @@ -1,56 +1,39 @@ | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using System.Security.Cryptography; | ||||
| using System.Text; | ||||
| using MessagePack; | ||||
| using Phantom.Utils.Cryptography; | ||||
| using Phantom.Utils.IO; | ||||
| using MemoryPack; | ||||
|  | ||||
| namespace Phantom.Common.Data.Agent; | ||||
|  | ||||
| [MessagePackObject] | ||||
| [MemoryPackable] | ||||
| [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] | ||||
| public sealed class AgentAuthToken { | ||||
| 	private const int MinimumTokenLength = 30; | ||||
| 	private const int MaximumTokenLength = 100; | ||||
| public sealed partial class AgentAuthToken { | ||||
| 	internal const int Length = 12; | ||||
|  | ||||
| 	[Key(0)] | ||||
| 	public string Value { get; } | ||||
|  | ||||
| 	[IgnoreMember] | ||||
| 	[MemoryPackOrder(0)] | ||||
| 	[MemoryPackInclude] | ||||
| 	private readonly byte[] bytes; | ||||
|  | ||||
| 	public AgentAuthToken(string? value) { | ||||
| 		if (value == null) { | ||||
| 			throw new ArgumentNullException(nameof(value)); | ||||
| 	internal AgentAuthToken(byte[]? bytes) { | ||||
| 		if (bytes == null) { | ||||
| 			throw new ArgumentNullException(nameof(bytes)); | ||||
| 		} | ||||
|  | ||||
| 		if (value.Length is < MinimumTokenLength or > MaximumTokenLength) { | ||||
| 			throw new ArgumentOutOfRangeException(nameof(value), "Invalid token length: " + value.Length + ". Token length must be between " + MinimumTokenLength + " and " + MaximumTokenLength + "."); | ||||
| 		if (bytes.Length != Length) { | ||||
| 			throw new ArgumentOutOfRangeException(nameof(bytes), "Invalid token length: " + bytes.Length + ". Token length must be exactly " + Length + " bytes."); | ||||
| 		} | ||||
|  | ||||
| 		this.Value = value; | ||||
| 		this.bytes = TokenGenerator.GetBytesOrThrow(value); | ||||
| 		this.bytes = bytes; | ||||
| 	} | ||||
|  | ||||
| 	public bool FixedTimeEquals(AgentAuthToken providedAuthToken) { | ||||
| 		return CryptographicOperations.FixedTimeEquals(bytes, providedAuthToken.bytes); | ||||
| 	} | ||||
|  | ||||
| 	public override string ToString() { | ||||
| 		return Value; | ||||
| 	} | ||||
|  | ||||
| 	public async Task WriteToFile(string filePath) { | ||||
| 		await Files.WriteBytesAsync(filePath, bytes, FileMode.Create, Chmod.URW_GR); | ||||
| 	} | ||||
|  | ||||
| 	public static async Task<AgentAuthToken> ReadFromFile(string filePath) { | ||||
| 		Files.RequireMaximumFileSize(filePath, MaximumTokenLength + 1); | ||||
| 		string contents = await File.ReadAllTextAsync(filePath, Encoding.ASCII); | ||||
| 		return new AgentAuthToken(contents.Trim()); | ||||
| 	internal void WriteTo(Span<byte> span) { | ||||
| 		bytes.CopyTo(span); | ||||
| 	} | ||||
|  | ||||
| 	public static AgentAuthToken Generate() { | ||||
| 		return new AgentAuthToken(TokenGenerator.Create(MinimumTokenLength)); | ||||
| 		return new AgentAuthToken(RandomNumberGenerator.GetBytes(Length)); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,15 @@ | ||||
| using MessagePack; | ||||
| using MemoryPack; | ||||
|  | ||||
| namespace Phantom.Common.Data.Agent; | ||||
|  | ||||
| [MessagePackObject] | ||||
| public sealed record AgentInfo( | ||||
| 	[property: Key(0)] Guid Guid, | ||||
| 	[property: Key(1)] string Name, | ||||
| 	[property: Key(2)] ushort Version, | ||||
| 	[property: Key(3)] ushort MaxInstances, | ||||
| 	[property: Key(4)] RamAllocationUnits MaxMemory, | ||||
| 	[property: Key(5)] AllowedPorts AllowedServerPorts, | ||||
| 	[property: Key(6)] AllowedPorts AllowedRconPorts | ||||
| [MemoryPackable] | ||||
| public sealed partial record AgentInfo( | ||||
| 	[property: MemoryPackOrder(0)] Guid Guid, | ||||
| 	[property: MemoryPackOrder(1)] string Name, | ||||
| 	[property: MemoryPackOrder(2)] ushort ProtocolVersion, | ||||
| 	[property: MemoryPackOrder(3)] string BuildVersion, | ||||
| 	[property: MemoryPackOrder(4)] ushort MaxInstances, | ||||
| 	[property: MemoryPackOrder(5)] RamAllocationUnits MaxMemory, | ||||
| 	[property: MemoryPackOrder(6)] AllowedPorts AllowedServerPorts, | ||||
| 	[property: MemoryPackOrder(7)] AllowedPorts AllowedRconPorts | ||||
| ); | ||||
|   | ||||
							
								
								
									
										18
									
								
								Common/Phantom.Common.Data/Agent/AgentKeyData.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								Common/Phantom.Common.Data/Agent/AgentKeyData.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| namespace Phantom.Common.Data.Agent; | ||||
|  | ||||
| public static class AgentKeyData { | ||||
| 	private const byte TokenLength = AgentAuthToken.Length; | ||||
|  | ||||
| 	public static byte[] ToBytes(byte[] publicKey, AgentAuthToken agentToken) { | ||||
| 		Span<byte> agentKey = stackalloc byte[TokenLength + publicKey.Length]; | ||||
| 		agentToken.WriteTo(agentKey[..TokenLength]); | ||||
| 		publicKey.CopyTo(agentKey[TokenLength..]); | ||||
| 		return agentKey.ToArray(); | ||||
| 	} | ||||
|  | ||||
| 	public static (byte[] PublicKey, AgentAuthToken AgentToken) FromBytes(byte[] agentKey) { | ||||
| 		var token = new AgentAuthToken(agentKey[..TokenLength]); | ||||
| 		var publicKey = agentKey[TokenLength..]; | ||||
| 		return (publicKey, token); | ||||
| 	} | ||||
| } | ||||
| @@ -1,29 +1,28 @@ | ||||
| using System.Collections.Immutable; | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using System.Text; | ||||
| using MessagePack; | ||||
| using MemoryPack; | ||||
|  | ||||
| namespace Phantom.Common.Data; | ||||
|  | ||||
| [MessagePackObject] | ||||
| [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] | ||||
| public sealed class AllowedPorts { | ||||
| 	[Key(0)] | ||||
| 	public ImmutableArray<PortRange> AllDefinitions { get; } | ||||
| [MemoryPackable] | ||||
| public sealed partial class AllowedPorts { | ||||
| 	[MemoryPackOrder(0)] | ||||
| 	[MemoryPackInclude] | ||||
| 	private	readonly ImmutableArray<PortRange> allDefinitions; | ||||
|  | ||||
| 	public AllowedPorts(ImmutableArray<PortRange> allDefinitions) { | ||||
| 	private AllowedPorts(ImmutableArray<PortRange> allDefinitions) { | ||||
| 		// TODO normalize and deduplicate ranges | ||||
| 		this.AllDefinitions = allDefinitions.Sort(static (def1, def2) => def1.FirstPort - def2.FirstPort); | ||||
| 		this.allDefinitions = allDefinitions.Sort(static (def1, def2) => def1.FirstPort - def2.FirstPort); | ||||
| 	} | ||||
|  | ||||
| 	public bool Contains(ushort port) { | ||||
| 		return AllDefinitions.Any(definition => definition.Contains(port)); | ||||
| 		return allDefinitions.Any(definition => definition.Contains(port)); | ||||
| 	} | ||||
|  | ||||
| 	public override string ToString() { | ||||
| 		var builder = new StringBuilder(); | ||||
|  | ||||
| 		foreach (var definition in AllDefinitions) { | ||||
| 		foreach (var definition in allDefinitions) { | ||||
| 			definition.ToString(builder); | ||||
| 			builder.Append(','); | ||||
| 		} | ||||
| @@ -35,53 +34,7 @@ public sealed class AllowedPorts { | ||||
| 		return builder.ToString(); | ||||
| 	} | ||||
|  | ||||
| 	[MessagePackObject] | ||||
| 	public readonly record struct PortRange( | ||||
| 		[property: Key(0)] ushort FirstPort, | ||||
| 		[property: Key(1)] ushort LastPort | ||||
| 	) { | ||||
| 		private PortRange(ushort port) : this(port, port) {} | ||||
|  | ||||
| 		internal bool Contains(ushort port) { | ||||
| 			return port >= FirstPort && port <= LastPort; | ||||
| 		} | ||||
|  | ||||
| 		internal void ToString(StringBuilder builder) { | ||||
| 			builder.Append(FirstPort); | ||||
|  | ||||
| 			if (LastPort != FirstPort) { | ||||
| 				builder.Append('-'); | ||||
| 				builder.Append(LastPort); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		internal static PortRange Parse(ReadOnlySpan<char> definition) { | ||||
| 			int separatorIndex = definition.IndexOf('-'); | ||||
| 			if (separatorIndex == -1) { | ||||
| 				var port = ParsePort(definition.Trim()); | ||||
| 				return new PortRange(port); | ||||
| 			} | ||||
|  | ||||
| 			var firstPort = ParsePort(definition[..separatorIndex].Trim()); | ||||
| 			var lastPort = ParsePort(definition[(separatorIndex + 1)..].Trim()); | ||||
| 			if (lastPort < firstPort) { | ||||
| 				throw new FormatException("Invalid port range '" + firstPort + "-" + lastPort + "'."); | ||||
| 			} | ||||
| 			else { | ||||
| 				return new PortRange(firstPort, lastPort); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		private static ushort ParsePort(ReadOnlySpan<char> port) { | ||||
| 			try { | ||||
| 				return ushort.Parse(port); | ||||
| 			} catch (Exception) { | ||||
| 				throw new FormatException("Invalid port '" + port.ToString() + "'."); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public static AllowedPorts FromString(ReadOnlySpan<char> definitions) { | ||||
| 	private static AllowedPorts FromString(ReadOnlySpan<char> definitions) { | ||||
| 		List<PortRange> parsedDefinitions = new (); | ||||
|  | ||||
| 		while (!definitions.IsEmpty) { | ||||
|   | ||||
							
								
								
									
										67
									
								
								Common/Phantom.Common.Data/Instance/IInstanceStatus.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								Common/Phantom.Common.Data/Instance/IInstanceStatus.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| using MemoryPack; | ||||
|  | ||||
| namespace Phantom.Common.Data.Instance; | ||||
|  | ||||
| [MemoryPackable] | ||||
| [MemoryPackUnion(0, typeof(InstanceIsOffline))] | ||||
| [MemoryPackUnion(1, typeof(InstanceIsInvalid))] | ||||
| [MemoryPackUnion(2, typeof(InstanceIsNotRunning))] | ||||
| [MemoryPackUnion(3, typeof(InstanceIsDownloading))] | ||||
| [MemoryPackUnion(4, typeof(InstanceIsLaunching))] | ||||
| [MemoryPackUnion(5, typeof(InstanceIsRunning))] | ||||
| [MemoryPackUnion(6, typeof(InstanceIsRestarting))] | ||||
| [MemoryPackUnion(7, typeof(InstanceIsStopping))] | ||||
| [MemoryPackUnion(8, typeof(InstanceIsFailed))] | ||||
| public partial interface IInstanceStatus {} | ||||
|  | ||||
| [MemoryPackable] | ||||
| public sealed partial record InstanceIsOffline : IInstanceStatus; | ||||
|  | ||||
| [MemoryPackable] | ||||
| public sealed partial record InstanceIsInvalid([property: MemoryPackOrder(0)] string Reason) : IInstanceStatus; | ||||
|  | ||||
| [MemoryPackable] | ||||
| public sealed partial record InstanceIsNotRunning : IInstanceStatus; | ||||
|  | ||||
| [MemoryPackable] | ||||
| public sealed partial record InstanceIsDownloading([property: MemoryPackOrder(0)] byte Progress) : IInstanceStatus; | ||||
|  | ||||
| [MemoryPackable] | ||||
| public sealed partial record InstanceIsLaunching : IInstanceStatus; | ||||
|  | ||||
| [MemoryPackable] | ||||
| public sealed partial record InstanceIsRunning : IInstanceStatus; | ||||
|  | ||||
| [MemoryPackable] | ||||
| public sealed partial record InstanceIsRestarting : IInstanceStatus; | ||||
|  | ||||
| [MemoryPackable] | ||||
| public sealed partial record InstanceIsStopping : IInstanceStatus; | ||||
|  | ||||
| [MemoryPackable] | ||||
| public sealed partial record InstanceIsFailed([property: MemoryPackOrder(0)] InstanceLaunchFailReason Reason) : IInstanceStatus; | ||||
|  | ||||
| public static class InstanceStatus { | ||||
| 	public static readonly IInstanceStatus Offline = new InstanceIsOffline(); | ||||
| 	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 Restarting = new InstanceIsRestarting(); | ||||
| 	public static readonly IInstanceStatus Stopping = new InstanceIsStopping(); | ||||
| 	 | ||||
| 	public static IInstanceStatus Invalid(string reason) => new InstanceIsInvalid(reason); | ||||
| 	public static IInstanceStatus Downloading(byte progress) => new InstanceIsDownloading(progress); | ||||
| 	public static IInstanceStatus Failed(InstanceLaunchFailReason reason) => new InstanceIsFailed(reason); | ||||
| 	 | ||||
| 	public static bool CanLaunch(this IInstanceStatus status) { | ||||
| 		return status is InstanceIsNotRunning or InstanceIsFailed; | ||||
| 	} | ||||
|  | ||||
| 	public static bool CanStop(this IInstanceStatus status) { | ||||
| 		return status is InstanceIsDownloading or InstanceIsLaunching or InstanceIsRunning; | ||||
| 	} | ||||
|  | ||||
| 	public static bool CanSendCommand(this IInstanceStatus status) { | ||||
| 		return status is InstanceIsRunning; | ||||
| 	} | ||||
| } | ||||
| @@ -1,18 +1,20 @@ | ||||
| using MessagePack; | ||||
| using System.Collections.Immutable; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data.Minecraft; | ||||
|  | ||||
| namespace Phantom.Common.Data.Instance;  | ||||
| namespace Phantom.Common.Data.Instance; | ||||
|  | ||||
| [MessagePackObject] | ||||
| public sealed record InstanceConfiguration( | ||||
| 	[property: Key(0)] Guid AgentGuid, | ||||
| 	[property: Key(1)] Guid InstanceGuid, | ||||
| 	[property: Key(2)] string InstanceName, | ||||
| 	[property: Key(3)] ushort ServerPort, | ||||
| 	[property: Key(4)] ushort RconPort, | ||||
| 	[property: Key(5)] string MinecraftVersion, | ||||
| 	[property: Key(6)] MinecraftServerKind MinecraftServerKind, | ||||
| 	[property: Key(7)] RamAllocationUnits MemoryAllocation, | ||||
| 	[property: Key(8)] Guid JavaRuntimeGuid, | ||||
| 	[property: Key(9)] bool LaunchAutomatically | ||||
| [MemoryPackable] | ||||
| public sealed partial record InstanceConfiguration( | ||||
| 	[property: MemoryPackOrder(0)] Guid AgentGuid, | ||||
| 	[property: MemoryPackOrder(1)] Guid InstanceGuid, | ||||
| 	[property: MemoryPackOrder(2)] string InstanceName, | ||||
| 	[property: MemoryPackOrder(3)] ushort ServerPort, | ||||
| 	[property: MemoryPackOrder(4)] ushort RconPort, | ||||
| 	[property: MemoryPackOrder(5)] string MinecraftVersion, | ||||
| 	[property: MemoryPackOrder(6)] MinecraftServerKind MinecraftServerKind, | ||||
| 	[property: MemoryPackOrder(7)] RamAllocationUnits MemoryAllocation, | ||||
| 	[property: MemoryPackOrder(8)] Guid JavaRuntimeGuid, | ||||
| 	[property: MemoryPackOrder(9)] ImmutableArray<string> JvmArguments, | ||||
| 	[property: MemoryPackOrder(10)] bool LaunchAutomatically | ||||
| ); | ||||
|   | ||||
| @@ -1,25 +1,31 @@ | ||||
| namespace Phantom.Common.Data.Instance; | ||||
|  | ||||
| public enum InstanceLaunchFailReason { | ||||
| 	UnknownError, | ||||
| 	ServerPortNotAllowed, | ||||
| 	ServerPortAlreadyInUse, | ||||
| 	RconPortNotAllowed, | ||||
| 	RconPortAlreadyInUse, | ||||
| 	JavaRuntimeNotFound, | ||||
| 	InvalidJvmArguments, | ||||
| 	CouldNotDownloadMinecraftServer, | ||||
| 	UnknownError | ||||
| 	CouldNotConfigureMinecraftServer, | ||||
| 	CouldNotStartMinecraftServer | ||||
| } | ||||
|  | ||||
| public static class InstanceLaunchFailReasonExtensions { | ||||
| 	public static string ToSentence(this InstanceLaunchFailReason reason) { | ||||
| 		return reason switch { | ||||
| 			InstanceLaunchFailReason.ServerPortNotAllowed            => "Server port not allowed.", | ||||
| 			InstanceLaunchFailReason.ServerPortAlreadyInUse          => "Server port already in use.", | ||||
| 			InstanceLaunchFailReason.RconPortNotAllowed              => "Rcon port not allowed.", | ||||
| 			InstanceLaunchFailReason.RconPortAlreadyInUse            => "Rcon port already in use.", | ||||
| 			InstanceLaunchFailReason.JavaRuntimeNotFound             => "Java runtime not found.", | ||||
| 			InstanceLaunchFailReason.CouldNotDownloadMinecraftServer => "Could not download Minecraft server.", | ||||
| 			_                                                        => "Unknown error." | ||||
| 			InstanceLaunchFailReason.ServerPortNotAllowed             => "Server port not allowed.", | ||||
| 			InstanceLaunchFailReason.ServerPortAlreadyInUse           => "Server port already in use.", | ||||
| 			InstanceLaunchFailReason.RconPortNotAllowed               => "Rcon port not allowed.", | ||||
| 			InstanceLaunchFailReason.RconPortAlreadyInUse             => "Rcon port already in use.", | ||||
| 			InstanceLaunchFailReason.JavaRuntimeNotFound              => "Java runtime not found.", | ||||
| 			InstanceLaunchFailReason.InvalidJvmArguments              => "Invalid JVM arguments.", | ||||
| 			InstanceLaunchFailReason.CouldNotDownloadMinecraftServer  => "Could not download Minecraft server.", | ||||
| 			InstanceLaunchFailReason.CouldNotConfigureMinecraftServer => "Could not configure Minecraft server.", | ||||
| 			InstanceLaunchFailReason.CouldNotStartMinecraftServer     => "Could not start Minecraft server.", | ||||
| 			_                                                         => "Unknown error." | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,63 +0,0 @@ | ||||
| using MessagePack; | ||||
|  | ||||
| namespace Phantom.Common.Data.Instance; | ||||
|  | ||||
| [Union(0, typeof(Offline))] | ||||
| [Union(1, typeof(Invalid))] | ||||
| [Union(2, typeof(NotRunning))] | ||||
| [Union(3, typeof(Downloading))] | ||||
| [Union(4, typeof(Launching))] | ||||
| [Union(5, typeof(Running))] | ||||
| [Union(6, typeof(Stopping))] | ||||
| [Union(7, typeof(Failed))] | ||||
| public abstract record InstanceStatus { | ||||
| 	public static readonly InstanceStatus IsOffline = new Offline(); | ||||
| 	public static readonly InstanceStatus IsNotRunning = new NotRunning(); | ||||
| 	public static readonly InstanceStatus IsLaunching = new Launching(); | ||||
| 	public static readonly InstanceStatus IsRunning = new Running(); | ||||
| 	public static readonly InstanceStatus IsStopping = new Stopping(); | ||||
|  | ||||
| 	[MessagePackObject] | ||||
| 	public sealed record Offline : InstanceStatus; | ||||
|  | ||||
| 	[MessagePackObject] | ||||
| 	public sealed record Invalid( | ||||
| 		[property: Key(0)] string Reason | ||||
| 	) : InstanceStatus; | ||||
|  | ||||
| 	[MessagePackObject] | ||||
| 	public sealed record NotRunning : InstanceStatus; | ||||
|  | ||||
| 	[MessagePackObject] | ||||
| 	public sealed record Downloading( | ||||
| 		[property: Key(0)] byte Progress | ||||
| 	) : InstanceStatus; | ||||
|  | ||||
| 	[MessagePackObject] | ||||
| 	public sealed record Launching : InstanceStatus; | ||||
| 	 | ||||
| 	[MessagePackObject] | ||||
| 	public sealed record Running : InstanceStatus; | ||||
|  | ||||
| 	[MessagePackObject] | ||||
| 	public sealed record Stopping : InstanceStatus; | ||||
|  | ||||
| 	[MessagePackObject] | ||||
| 	public sealed record Failed( | ||||
| 		[property: Key(0)] InstanceLaunchFailReason Reason | ||||
| 	) : InstanceStatus; | ||||
| } | ||||
|  | ||||
| public static class InstanceStatusExtensions { | ||||
| 	public static bool CanLaunch(this InstanceStatus status) { | ||||
| 		return status is InstanceStatus.NotRunning or InstanceStatus.Failed; | ||||
| 	} | ||||
|  | ||||
| 	public static bool CanStop(this InstanceStatus status) { | ||||
| 		return status is InstanceStatus.Downloading or InstanceStatus.Launching or InstanceStatus.Running; | ||||
| 	} | ||||
|  | ||||
| 	public static bool CanSendCommand(this InstanceStatus status) { | ||||
| 		return status is InstanceStatus.Running; | ||||
| 	} | ||||
| } | ||||
| @@ -1,13 +1,13 @@ | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using MessagePack; | ||||
| using MemoryPack; | ||||
|  | ||||
| namespace Phantom.Common.Data.Java; | ||||
|  | ||||
| [MessagePackObject] | ||||
| public sealed record JavaRuntime( | ||||
| 	[property: Key(0)] string MainVersion, | ||||
| 	[property: Key(1)] string FullVersion, | ||||
| 	[property: Key(2)] string DisplayName | ||||
| [MemoryPackable] | ||||
| public sealed partial record JavaRuntime( | ||||
| 	[property: MemoryPackOrder(0)] string MainVersion, | ||||
| 	[property: MemoryPackOrder(1)] string FullVersion, | ||||
| 	[property: MemoryPackOrder(2)] string DisplayName | ||||
| ) : IComparable<JavaRuntime> { | ||||
| 	public int CompareTo(JavaRuntime? other) { | ||||
| 		if (ReferenceEquals(this, other)) { | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| using MessagePack; | ||||
| using MemoryPack; | ||||
|  | ||||
| namespace Phantom.Common.Data.Java;  | ||||
|  | ||||
| [MessagePackObject] | ||||
| public sealed record TaggedJavaRuntime( | ||||
| 	[property: Key(0)] Guid Guid, | ||||
| 	[property: Key(1)] JavaRuntime Runtime | ||||
| [MemoryPackable] | ||||
| public sealed partial record TaggedJavaRuntime( | ||||
| 	[property: MemoryPackOrder(0)] Guid Guid, | ||||
| 	[property: MemoryPackOrder(1)] JavaRuntime Runtime | ||||
| ); | ||||
|   | ||||
| @@ -0,0 +1,10 @@ | ||||
| using MemoryPack; | ||||
|  | ||||
| namespace Phantom.Common.Data.Minecraft; | ||||
|  | ||||
| [MemoryPackable] | ||||
| public readonly partial record struct MinecraftStopStrategy( | ||||
| 	[property: MemoryPackOrder(0)] ushort Seconds | ||||
| ) { | ||||
| 	public static MinecraftStopStrategy Instant => new (0); | ||||
| } | ||||
							
								
								
									
										7
									
								
								Common/Phantom.Common.Data/Minecraft/MinecraftVersion.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								Common/Phantom.Common.Data/Minecraft/MinecraftVersion.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| namespace Phantom.Common.Data.Minecraft;  | ||||
|  | ||||
| public sealed record MinecraftVersion( | ||||
| 	string Id, | ||||
| 	MinecraftVersionType Type, | ||||
| 	string MetadataUrl | ||||
| ); | ||||
							
								
								
									
										38
									
								
								Common/Phantom.Common.Data/Minecraft/MinecraftVersionType.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								Common/Phantom.Common.Data/Minecraft/MinecraftVersionType.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| using System.Collections.Immutable; | ||||
|  | ||||
| namespace Phantom.Common.Data.Minecraft; | ||||
|  | ||||
| public enum MinecraftVersionType : ushort { | ||||
| 	Other = 0, | ||||
| 	Release = 1, | ||||
| 	Snapshot = 2, | ||||
| 	OldBeta = 3, | ||||
| 	OldAlpha = 4 | ||||
| } | ||||
|  | ||||
| public static class MinecraftVersionTypes { | ||||
| 	public static readonly ImmutableArray<MinecraftVersionType> WithServerJars = ImmutableArray.Create( | ||||
| 		MinecraftVersionType.Release, | ||||
| 		MinecraftVersionType.Snapshot | ||||
| 	); | ||||
|  | ||||
| 	public static MinecraftVersionType FromString(string? type) { | ||||
| 		return type switch { | ||||
| 			"release"   => MinecraftVersionType.Release, | ||||
| 			"snapshot"  => MinecraftVersionType.Snapshot, | ||||
| 			"old_beta"  => MinecraftVersionType.OldBeta, | ||||
| 			"old_alpha" => MinecraftVersionType.OldAlpha, | ||||
| 			_           => MinecraftVersionType.Other | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	public static string ToNiceNamePlural(this MinecraftVersionType type) { | ||||
| 		return type switch { | ||||
| 			MinecraftVersionType.Release  => "Releases", | ||||
| 			MinecraftVersionType.Snapshot => "Snapshots", | ||||
| 			MinecraftVersionType.OldBeta  => "Beta", | ||||
| 			MinecraftVersionType.OldAlpha => "Alpha", | ||||
| 			_                             => "Unknown" | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
| @@ -7,7 +7,7 @@ | ||||
|   </PropertyGroup> | ||||
|    | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="MessagePack.Annotations" Version="2.4.35" /> | ||||
|     <PackageReference Include="MemoryPack" /> | ||||
|   </ItemGroup> | ||||
|    | ||||
|   <ItemGroup> | ||||
|   | ||||
							
								
								
									
										48
									
								
								Common/Phantom.Common.Data/PortRange.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								Common/Phantom.Common.Data/PortRange.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| using System.Text; | ||||
| using MemoryPack; | ||||
|  | ||||
| namespace Phantom.Common.Data; | ||||
|  | ||||
| [MemoryPackable] | ||||
| readonly partial record struct PortRange( | ||||
| 	[property: MemoryPackOrder(0)] ushort FirstPort, | ||||
| 	[property: MemoryPackOrder(1)] ushort LastPort | ||||
| ) { | ||||
| 	internal bool Contains(ushort port) { | ||||
| 		return port >= FirstPort && port <= LastPort; | ||||
| 	} | ||||
|  | ||||
| 	internal void ToString(StringBuilder builder) { | ||||
| 		builder.Append(FirstPort); | ||||
|  | ||||
| 		if (LastPort != FirstPort) { | ||||
| 			builder.Append('-'); | ||||
| 			builder.Append(LastPort); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	internal static PortRange Parse(ReadOnlySpan<char> definition) { | ||||
| 		int separatorIndex = definition.IndexOf('-'); | ||||
| 		if (separatorIndex == -1) { | ||||
| 			var port = ParsePort(definition.Trim()); | ||||
| 			return new PortRange(port, port); | ||||
| 		} | ||||
|  | ||||
| 		var firstPort = ParsePort(definition[..separatorIndex].Trim()); | ||||
| 		var lastPort = ParsePort(definition[(separatorIndex + 1)..].Trim()); | ||||
| 		if (lastPort < firstPort) { | ||||
| 			throw new FormatException("Invalid port range '" + firstPort + "-" + lastPort + "'."); | ||||
| 		} | ||||
| 		else { | ||||
| 			return new PortRange(firstPort, lastPort); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private static ushort ParsePort(ReadOnlySpan<char> port) { | ||||
| 		try { | ||||
| 			return ushort.Parse(port); | ||||
| 		} catch (Exception) { | ||||
| 			throw new FormatException("Invalid port '" + port.ToString() + "'."); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,17 +1,17 @@ | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using MessagePack; | ||||
| using MemoryPack; | ||||
|  | ||||
| namespace Phantom.Common.Data; | ||||
|  | ||||
| /// <summary> | ||||
| /// Represents a number of RAM allocation units, using the conversion factor of 256 MB per unit. Supports allocations up to 16 TB minus 256 MB (65535 units). | ||||
| /// </summary> | ||||
| [MessagePackObject] | ||||
| [MemoryPackable] | ||||
| [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] | ||||
| public readonly record struct RamAllocationUnits( | ||||
| 	[property: Key(0)] ushort RawValue | ||||
| public readonly partial record struct RamAllocationUnits( | ||||
| 	[property: MemoryPackOrder(0)] ushort RawValue | ||||
| ) : IComparable<RamAllocationUnits> { | ||||
| 	[IgnoreMember] | ||||
| 	[MemoryPackIgnore] | ||||
| 	public uint InMegabytes => (uint) RawValue * MegabytesPerUnit; | ||||
|  | ||||
| 	public int CompareTo(RamAllocationUnits other) { | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| namespace Phantom.Common.Data.Replies;  | ||||
|  | ||||
| public enum ConfigureInstanceResult { | ||||
| public enum ConfigureInstanceResult : byte { | ||||
| 	Success, | ||||
| 	AgentShuttingDown, | ||||
| 	InstanceLimitExceeded, | ||||
| 	MemoryLimitExceeded | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,8 @@ | ||||
| namespace Phantom.Common.Data.Replies; | ||||
|  | ||||
| public enum InstanceActionGeneralResult : byte { | ||||
| 	None, | ||||
| 	AgentShuttingDown, | ||||
| 	AgentIsNotResponding, | ||||
| 	InstanceDoesNotExist | ||||
| } | ||||
							
								
								
									
										41
									
								
								Common/Phantom.Common.Data/Replies/InstanceActionResult.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								Common/Phantom.Common.Data/Replies/InstanceActionResult.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| using MemoryPack; | ||||
|  | ||||
| namespace Phantom.Common.Data.Replies; | ||||
|  | ||||
| [MemoryPackable] | ||||
| 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.AgentShuttingDown    => "Agent is shutting down.", | ||||
| 			InstanceActionGeneralResult.AgentIsNotResponding => "Agent is not responding.", | ||||
| 			InstanceActionGeneralResult.InstanceDoesNotExist => "Instance does not exist.", | ||||
| 			_                                                => "Unknown result." | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| public static class InstanceActionResult { | ||||
| 	public static InstanceActionResult<T> General<T>(InstanceActionGeneralResult generalResult) { | ||||
| 		return new InstanceActionResult<T>(generalResult, default); | ||||
| 	} | ||||
|  | ||||
| 	public static InstanceActionResult<T> Concrete<T>(T? concreteResult) { | ||||
| 		return new InstanceActionResult<T>(InstanceActionGeneralResult.None, concreteResult); | ||||
| 	} | ||||
|  | ||||
| 	public static InstanceActionResult<T> DidNotReplyIfNull<T>(this InstanceActionResult<T>? result) { | ||||
| 		return result ?? General<T>(InstanceActionGeneralResult.AgentIsNotResponding); | ||||
| 	} | ||||
| } | ||||
| @@ -1,13 +1,10 @@ | ||||
| namespace Phantom.Common.Data.Replies; | ||||
|  | ||||
| public enum LaunchInstanceResult { | ||||
| public enum LaunchInstanceResult : byte { | ||||
| 	LaunchInitiated, | ||||
| 	AgentShuttingDown, | ||||
| 	InstanceDoesNotExist, | ||||
| 	InstanceAlreadyLaunching, | ||||
| 	InstanceAlreadyRunning, | ||||
| 	InstanceIsStopping, | ||||
| 	CommunicationError, | ||||
| 	UnknownError | ||||
| } | ||||
|  | ||||
| @@ -15,12 +12,9 @@ public static class LaunchInstanceResultExtensions { | ||||
| 	public static string ToSentence(this LaunchInstanceResult reason) { | ||||
| 		return reason switch { | ||||
| 			LaunchInstanceResult.LaunchInitiated          => "Launch initiated.", | ||||
| 			LaunchInstanceResult.AgentShuttingDown        => "Agent is shutting down.", | ||||
| 			LaunchInstanceResult.InstanceDoesNotExist     => "Instance does not exist.", | ||||
| 			LaunchInstanceResult.InstanceAlreadyLaunching => "Instance is already launching.", | ||||
| 			LaunchInstanceResult.InstanceAlreadyRunning   => "Instance is already running.", | ||||
| 			LaunchInstanceResult.InstanceIsStopping       => "Instance is stopping.", | ||||
| 			LaunchInstanceResult.CommunicationError       => "Communication error.", | ||||
| 			_                                             => "Unknown error." | ||||
| 		}; | ||||
| 	} | ||||
|   | ||||
| @@ -1,21 +1,15 @@ | ||||
| namespace Phantom.Common.Data.Replies; | ||||
|  | ||||
| public enum SendCommandToInstanceResult { | ||||
| public enum SendCommandToInstanceResult : byte { | ||||
| 	Success, | ||||
| 	InstanceDoesNotExist, | ||||
| 	AgentShuttingDown, | ||||
| 	AgentCommunicationError, | ||||
| 	UnknownError | ||||
| } | ||||
|  | ||||
| public static class SendCommandToInstanceResultExtensions { | ||||
| 	public static string ToSentence(this SendCommandToInstanceResult reason) { | ||||
| 		return reason switch { | ||||
| 			SendCommandToInstanceResult.Success                 => "Command sent.", | ||||
| 			SendCommandToInstanceResult.InstanceDoesNotExist    => "Instance does not exist.", | ||||
| 			SendCommandToInstanceResult.AgentShuttingDown       => "Agent is shutting down.", | ||||
| 			SendCommandToInstanceResult.AgentCommunicationError => "Agent did not reply in time.", | ||||
| 			_                                                   => "Unknown error." | ||||
| 			SendCommandToInstanceResult.Success => "Command sent.", | ||||
| 			_                                   => "Unknown error." | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,12 +1,9 @@ | ||||
| namespace Phantom.Common.Data.Replies; | ||||
|  | ||||
| public enum StopInstanceResult { | ||||
| public enum StopInstanceResult : byte { | ||||
| 	StopInitiated, | ||||
| 	AgentShuttingDown, | ||||
| 	InstanceDoesNotExist, | ||||
| 	InstanceAlreadyStopping, | ||||
| 	InstanceAlreadyStopped, | ||||
| 	CommunicationError, | ||||
| 	UnknownError | ||||
| } | ||||
|  | ||||
| @@ -14,11 +11,8 @@ public static class StopInstanceResultExtensions { | ||||
| 	public static string ToSentence(this StopInstanceResult reason) { | ||||
| 		return reason switch { | ||||
| 			StopInstanceResult.StopInitiated           => "Stopping initiated.", | ||||
| 			StopInstanceResult.AgentShuttingDown       => "Agent is shutting down.", | ||||
| 			StopInstanceResult.InstanceDoesNotExist    => "Instance does not exist.", | ||||
| 			StopInstanceResult.InstanceAlreadyStopping => "Instance is already stopping.", | ||||
| 			StopInstanceResult.InstanceAlreadyStopped  => "Instance is already stopped.", | ||||
| 			StopInstanceResult.CommunicationError      => "Communication error.", | ||||
| 			_                                          => "Unknown error." | ||||
| 		}; | ||||
| 	} | ||||
|   | ||||
| @@ -7,8 +7,8 @@ | ||||
|   </PropertyGroup> | ||||
|    | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Serilog" Version="2.12.0" /> | ||||
|     <PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" /> | ||||
|     <PackageReference Include="Serilog" /> | ||||
|     <PackageReference Include="Serilog.Sinks.Console" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
|   | ||||
| @@ -33,8 +33,8 @@ public static class PhantomLogger { | ||||
| 		return Base.ForContext("Category", name); | ||||
| 	} | ||||
|  | ||||
| 	public static ILogger Create(Type type) { | ||||
| 		return Create(type.Name); | ||||
| 	public static ILogger Create(string name1, string name2) { | ||||
| 		return Create(name1 + ":" + name2); | ||||
| 	} | ||||
|  | ||||
| 	public static ILogger Create<T>() { | ||||
| @@ -42,11 +42,11 @@ public static class PhantomLogger { | ||||
| 	} | ||||
|  | ||||
| 	public static ILogger Create<T>(string name) { | ||||
| 		return Create(typeof(T).Name + ":" + name); | ||||
| 		return Create(typeof(T).Name, name); | ||||
| 	} | ||||
|  | ||||
| 	public static ILogger Create<T1, T2>() { | ||||
| 		return Create(typeof(T1).Name + ":" + typeof(T2).Name); | ||||
| 		return Create(typeof(T1).Name, typeof(T2).Name); | ||||
| 	} | ||||
|  | ||||
| 	public static void Dispose() { | ||||
|   | ||||
							
								
								
									
										18
									
								
								Common/Phantom.Common.Messages/BiDirectional/ReplyMessage.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								Common/Phantom.Common.Messages/BiDirectional/ReplyMessage.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| using MemoryPack; | ||||
| using Phantom.Utils.Rpc.Message; | ||||
|  | ||||
| namespace Phantom.Common.Messages.BiDirectional; | ||||
|  | ||||
| [MemoryPackable] | ||||
| public sealed partial record ReplyMessage( | ||||
| 	[property: MemoryPackOrder(0)] uint SequenceId, | ||||
| 	[property: MemoryPackOrder(1)] byte[] SerializedReply | ||||
| ) : IMessageToServer, IMessageToAgent { | ||||
| 	public Task<NoReply> Accept(IMessageToServerListener listener) { | ||||
| 		return listener.HandleReply(this); | ||||
| 	} | ||||
|  | ||||
| 	public Task<NoReply> Accept(IMessageToAgentListener listener) { | ||||
| 		return listener.HandleReply(this); | ||||
| 	} | ||||
| } | ||||
| @@ -2,4 +2,6 @@ | ||||
|  | ||||
| namespace Phantom.Common.Messages; | ||||
|  | ||||
| public interface IMessageToAgent : IMessage<IMessageToAgentListener> {} | ||||
| public interface IMessageToAgent<TReply> : IMessage<IMessageToAgentListener, TReply> {} | ||||
|  | ||||
| public interface IMessageToAgent : IMessageToAgent<NoReply> {} | ||||
|   | ||||
| @@ -1,12 +1,16 @@ | ||||
| using Phantom.Common.Messages.ToAgent; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Messages.BiDirectional; | ||||
| using Phantom.Common.Messages.ToAgent; | ||||
| using Phantom.Utils.Rpc.Message; | ||||
|  | ||||
| namespace Phantom.Common.Messages; | ||||
|  | ||||
| public interface IMessageToAgentListener { | ||||
| 	Task HandleRegisterAgentSuccessResult(RegisterAgentSuccessMessage message); | ||||
| 	Task HandleRegisterAgentFailureResult(RegisterAgentFailureMessage message); | ||||
| 	Task HandleConfigureInstance(ConfigureInstanceMessage message); | ||||
| 	Task HandleLaunchInstance(LaunchInstanceMessage message); | ||||
| 	Task HandleStopInstance(StopInstanceMessage message); | ||||
| 	Task HandleSendCommandToInstance(SendCommandToInstanceMessage message); | ||||
| 	Task<NoReply> HandleRegisterAgentSuccess(RegisterAgentSuccessMessage message); | ||||
| 	Task<NoReply> HandleRegisterAgentFailure(RegisterAgentFailureMessage message); | ||||
| 	Task<InstanceActionResult<ConfigureInstanceResult>> HandleConfigureInstance(ConfigureInstanceMessage message); | ||||
| 	Task<InstanceActionResult<LaunchInstanceResult>> HandleLaunchInstance(LaunchInstanceMessage message); | ||||
| 	Task<InstanceActionResult<StopInstanceResult>> HandleStopInstance(StopInstanceMessage message); | ||||
| 	Task<InstanceActionResult<SendCommandToInstanceResult>> HandleSendCommandToInstance(SendCommandToInstanceMessage message); | ||||
| 	Task<NoReply> HandleReply(ReplyMessage message); | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| using Phantom.Utils.Rpc.Message; | ||||
|  | ||||
| namespace Phantom.Common.Messages;  | ||||
| namespace Phantom.Common.Messages; | ||||
|  | ||||
| public interface IMessageToServer : IMessage<IMessageToServerListener> {} | ||||
| public interface IMessageToServer<TReply> : IMessage<IMessageToServerListener, TReply> {} | ||||
|  | ||||
| public interface IMessageToServer : IMessageToServer<NoReply> {} | ||||
|   | ||||
| @@ -1,14 +1,16 @@ | ||||
| using Phantom.Common.Messages.ToServer; | ||||
| using Phantom.Common.Messages.BiDirectional; | ||||
| using Phantom.Common.Messages.ToServer; | ||||
| using Phantom.Utils.Rpc.Message; | ||||
|  | ||||
| namespace Phantom.Common.Messages;  | ||||
|  | ||||
| public interface IMessageToServerListener { | ||||
| 	bool IsDisposed { get; } | ||||
| 	Task HandleRegisterAgent(RegisterAgentMessage message); | ||||
| 	Task HandleUnregisterAgent(UnregisterAgentMessage message); | ||||
| 	Task HandleAgentIsAlive(AgentIsAliveMessage message); | ||||
| 	Task HandleAdvertiseJavaRuntimes(AdvertiseJavaRuntimesMessage message); | ||||
| 	Task HandleReportInstanceStatus(ReportInstanceStatusMessage message); | ||||
| 	Task HandleInstanceOutput(InstanceOutputMessage message); | ||||
| 	Task HandleSimpleReply(SimpleReplyMessage message); | ||||
| 	Task<NoReply> HandleRegisterAgent(RegisterAgentMessage message); | ||||
| 	Task<NoReply> HandleUnregisterAgent(UnregisterAgentMessage message); | ||||
| 	Task<NoReply> HandleAgentIsAlive(AgentIsAliveMessage message); | ||||
| 	Task<NoReply> HandleAdvertiseJavaRuntimes(AdvertiseJavaRuntimesMessage message); | ||||
| 	Task<NoReply> HandleReportInstanceStatus(ReportInstanceStatusMessage message); | ||||
| 	Task<NoReply> HandleInstanceOutput(InstanceOutputMessage message); | ||||
| 	Task<NoReply> HandleReply(ReplyMessage message); | ||||
| } | ||||
|   | ||||
| @@ -1,5 +0,0 @@ | ||||
| namespace Phantom.Common.Messages; | ||||
|  | ||||
| public interface IMessageWithReply { | ||||
| 	public uint SequenceId { get; } | ||||
| } | ||||
| @@ -1,4 +1,6 @@ | ||||
| using Phantom.Common.Logging; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Logging; | ||||
| using Phantom.Common.Messages.BiDirectional; | ||||
| using Phantom.Common.Messages.ToAgent; | ||||
| using Phantom.Common.Messages.ToServer; | ||||
| using Phantom.Utils.Rpc.Message; | ||||
| @@ -6,16 +8,17 @@ using Phantom.Utils.Rpc.Message; | ||||
| namespace Phantom.Common.Messages;  | ||||
|  | ||||
| public static class MessageRegistries { | ||||
| 	public static MessageRegistry<IMessageToAgentListener, IMessageToAgent> ToAgent { get; } = new (PhantomLogger.Create("MessageRegistry:ToAgent")); | ||||
| 	public static MessageRegistry<IMessageToServerListener, IMessageToServer> ToServer { get; } = new (PhantomLogger.Create("MessageRegistry:ToServer")); | ||||
| 	public static MessageRegistry<IMessageToAgentListener> ToAgent { get; } = new (PhantomLogger.Create("MessageRegistry:ToAgent")); | ||||
| 	public static MessageRegistry<IMessageToServerListener> ToServer { get; } = new (PhantomLogger.Create("MessageRegistry:ToServer")); | ||||
|  | ||||
| 	static MessageRegistries() { | ||||
| 		ToAgent.Add<RegisterAgentSuccessMessage>(0); | ||||
| 		ToAgent.Add<RegisterAgentFailureMessage>(1); | ||||
| 		ToAgent.Add<ConfigureInstanceMessage>(2); | ||||
| 		ToAgent.Add<LaunchInstanceMessage>(3); | ||||
| 		ToAgent.Add<StopInstanceMessage>(4); | ||||
| 		ToAgent.Add<SendCommandToInstanceMessage>(5); | ||||
| 		ToAgent.Add<ConfigureInstanceMessage, InstanceActionResult<ConfigureInstanceResult>>(2); | ||||
| 		ToAgent.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(3); | ||||
| 		ToAgent.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(4); | ||||
| 		ToAgent.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(5); | ||||
| 		ToAgent.Add<ReplyMessage>(127); | ||||
| 		 | ||||
| 		ToServer.Add<RegisterAgentMessage>(0); | ||||
| 		ToServer.Add<UnregisterAgentMessage>(1); | ||||
| @@ -23,6 +26,6 @@ public static class MessageRegistries { | ||||
| 		ToServer.Add<AdvertiseJavaRuntimesMessage>(3); | ||||
| 		ToServer.Add<ReportInstanceStatusMessage>(4); | ||||
| 		ToServer.Add<InstanceOutputMessage>(5); | ||||
| 		ToServer.Add<SimpleReplyMessage>(127); | ||||
| 		ToServer.Add<ReplyMessage>(127); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
| using MessagePack; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Common.Data.Replies; | ||||
|  | ||||
| namespace Phantom.Common.Messages.ToAgent; | ||||
|  | ||||
| [MessagePackObject] | ||||
| public sealed record ConfigureInstanceMessage( | ||||
| 	[property: Key(0)] uint SequenceId, | ||||
| 	[property: Key(1)] InstanceConfiguration Configuration | ||||
| ) : IMessageToAgent, IMessageWithReply { | ||||
| 	public Task Accept(IMessageToAgentListener listener) { | ||||
| [MemoryPackable] | ||||
| public sealed partial record ConfigureInstanceMessage( | ||||
| 	[property: MemoryPackOrder(0)] InstanceConfiguration Configuration | ||||
| ) : IMessageToAgent<InstanceActionResult<ConfigureInstanceResult>> { | ||||
| 	public Task<InstanceActionResult<ConfigureInstanceResult>> Accept(IMessageToAgentListener listener) { | ||||
| 		return listener.HandleConfigureInstance(this); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,13 +1,13 @@ | ||||
| using MessagePack; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data.Replies; | ||||
|  | ||||
| namespace Phantom.Common.Messages.ToAgent;  | ||||
|  | ||||
| [MessagePackObject] | ||||
| public sealed record LaunchInstanceMessage( | ||||
| 	[property: Key(0)] uint SequenceId, | ||||
| 	[property: Key(1)] Guid InstanceGuid | ||||
| ) : IMessageToAgent, IMessageWithReply { | ||||
| 	public Task Accept(IMessageToAgentListener listener) { | ||||
| [MemoryPackable] | ||||
| public sealed partial record LaunchInstanceMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid InstanceGuid | ||||
| ) : IMessageToAgent<InstanceActionResult<LaunchInstanceResult>> { | ||||
| 	public Task<InstanceActionResult<LaunchInstanceResult>> Accept(IMessageToAgentListener listener) { | ||||
| 		return listener.HandleLaunchInstance(this); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,13 +1,14 @@ | ||||
| using MessagePack; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Utils.Rpc.Message; | ||||
|  | ||||
| namespace Phantom.Common.Messages.ToAgent; | ||||
|  | ||||
| [MessagePackObject] | ||||
| public sealed record RegisterAgentFailureMessage( | ||||
| 	[property: Key(0)] RegisterAgentFailure FailureKind | ||||
| [MemoryPackable] | ||||
| public sealed partial record RegisterAgentFailureMessage( | ||||
| 	[property: MemoryPackOrder(0)] RegisterAgentFailure FailureKind | ||||
| ) : IMessageToAgent { | ||||
| 	public Task Accept(IMessageToAgentListener listener) { | ||||
| 		return listener.HandleRegisterAgentFailureResult(this); | ||||
| 	public Task<NoReply> Accept(IMessageToAgentListener listener) { | ||||
| 		return listener.HandleRegisterAgentFailure(this); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,15 @@ | ||||
| using System.Collections.Immutable; | ||||
| using MessagePack; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Utils.Rpc.Message; | ||||
|  | ||||
| namespace Phantom.Common.Messages.ToAgent; | ||||
|  | ||||
| [MessagePackObject] | ||||
| public sealed record RegisterAgentSuccessMessage( | ||||
| 	[property: Key(0)] ImmutableArray<InstanceConfiguration> InitialInstances | ||||
| [MemoryPackable] | ||||
| public sealed partial record RegisterAgentSuccessMessage( | ||||
| 	[property: MemoryPackOrder(0)] ImmutableArray<InstanceConfiguration> InitialInstances | ||||
| ) : IMessageToAgent { | ||||
| 	public Task Accept(IMessageToAgentListener listener) { | ||||
| 		return listener.HandleRegisterAgentSuccessResult(this); | ||||
| 	public Task<NoReply> Accept(IMessageToAgentListener listener) { | ||||
| 		return listener.HandleRegisterAgentSuccess(this); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
| using MessagePack; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data.Replies; | ||||
|  | ||||
| namespace Phantom.Common.Messages.ToAgent;  | ||||
|  | ||||
| [MessagePackObject] | ||||
| public sealed record SendCommandToInstanceMessage( | ||||
| 	[property: Key(0)] uint SequenceId, | ||||
| 	[property: Key(1)] Guid InstanceGuid, | ||||
| 	[property: Key(2)] string Command | ||||
| ) : IMessageToAgent, IMessageWithReply { | ||||
| 	public Task Accept(IMessageToAgentListener listener) { | ||||
| [MemoryPackable] | ||||
| public sealed partial record SendCommandToInstanceMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid InstanceGuid, | ||||
| 	[property: MemoryPackOrder(1)] string Command | ||||
| ) : IMessageToAgent<InstanceActionResult<SendCommandToInstanceResult>> { | ||||
| 	public Task<InstanceActionResult<SendCommandToInstanceResult>> Accept(IMessageToAgentListener listener) { | ||||
| 		return listener.HandleSendCommandToInstance(this); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,13 +1,15 @@ | ||||
| using MessagePack; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data.Minecraft; | ||||
| using Phantom.Common.Data.Replies; | ||||
|  | ||||
| namespace Phantom.Common.Messages.ToAgent;  | ||||
|  | ||||
| [MessagePackObject] | ||||
| public sealed record StopInstanceMessage( | ||||
| 	[property: Key(0)] uint SequenceId, | ||||
| 	[property: Key(1)] Guid InstanceGuid | ||||
| ) : IMessageToAgent, IMessageWithReply { | ||||
| 	public Task Accept(IMessageToAgentListener listener) { | ||||
| [MemoryPackable] | ||||
| public sealed partial record StopInstanceMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid InstanceGuid, | ||||
| 	[property: MemoryPackOrder(1)] MinecraftStopStrategy StopStrategy | ||||
| ) : IMessageToAgent<InstanceActionResult<StopInstanceResult>> { | ||||
| 	public Task<InstanceActionResult<StopInstanceResult>> Accept(IMessageToAgentListener listener) { | ||||
| 		return listener.HandleStopInstance(this); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,15 @@ | ||||
| using System.Collections.Immutable; | ||||
| using MessagePack; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data.Java; | ||||
| using Phantom.Utils.Rpc.Message; | ||||
|  | ||||
| namespace Phantom.Common.Messages.ToServer;  | ||||
|  | ||||
| [MessagePackObject] | ||||
| public sealed record AdvertiseJavaRuntimesMessage( | ||||
| 	[property: Key(0)] ImmutableArray<TaggedJavaRuntime> Runtimes | ||||
| [MemoryPackable] | ||||
| public sealed partial record AdvertiseJavaRuntimesMessage( | ||||
| 	[property: MemoryPackOrder(0)] ImmutableArray<TaggedJavaRuntime> Runtimes | ||||
| ) : IMessageToServer { | ||||
| 	public Task Accept(IMessageToServerListener listener) { | ||||
| 	public Task<NoReply> Accept(IMessageToServerListener listener) { | ||||
| 		return listener.HandleAdvertiseJavaRuntimes(this); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| using MessagePack; | ||||
| using MemoryPack; | ||||
| using Phantom.Utils.Rpc.Message; | ||||
|  | ||||
| namespace Phantom.Common.Messages.ToServer;  | ||||
|  | ||||
| [MessagePackObject] | ||||
| public sealed record AgentIsAliveMessage : IMessageToServer { | ||||
| 	public Task Accept(IMessageToServerListener listener) { | ||||
| [MemoryPackable] | ||||
| public sealed partial record AgentIsAliveMessage : IMessageToServer { | ||||
| 	public Task<NoReply> Accept(IMessageToServerListener listener) { | ||||
| 		return listener.HandleAgentIsAlive(this); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,15 @@ | ||||
| using System.Collections.Immutable; | ||||
| using MessagePack; | ||||
| using MemoryPack; | ||||
| using Phantom.Utils.Rpc.Message; | ||||
|  | ||||
| namespace Phantom.Common.Messages.ToServer; | ||||
|  | ||||
| [MessagePackObject] | ||||
| public sealed record InstanceOutputMessage( | ||||
| 	[property: Key(0)] Guid InstanceGuid, | ||||
| 	[property: Key(1)] ImmutableArray<string> Lines | ||||
| [MemoryPackable] | ||||
| public sealed partial record InstanceOutputMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid InstanceGuid, | ||||
| 	[property: MemoryPackOrder(1)] ImmutableArray<string> Lines | ||||
| ) : IMessageToServer { | ||||
| 	public Task Accept(IMessageToServerListener listener) { | ||||
| 	public Task<NoReply> Accept(IMessageToServerListener listener) { | ||||
| 		return listener.HandleInstanceOutput(this); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,15 @@ | ||||
| using MessagePack; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data.Agent; | ||||
| using Phantom.Utils.Rpc.Message; | ||||
|  | ||||
| namespace Phantom.Common.Messages.ToServer; | ||||
|  | ||||
| [MessagePackObject] | ||||
| public sealed record RegisterAgentMessage( | ||||
| 	[property: Key(0)] AgentAuthToken AuthToken, | ||||
| 	[property: Key(1)] AgentInfo AgentInfo | ||||
| [MemoryPackable] | ||||
| public sealed partial record RegisterAgentMessage( | ||||
| 	[property: MemoryPackOrder(0)] AgentAuthToken AuthToken, | ||||
| 	[property: MemoryPackOrder(1)] AgentInfo AgentInfo | ||||
| ) : IMessageToServer { | ||||
| 	public Task Accept(IMessageToServerListener listener) { | ||||
| 	public Task<NoReply> Accept(IMessageToServerListener listener) { | ||||
| 		return listener.HandleRegisterAgent(this); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,15 @@ | ||||
| using MessagePack; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Utils.Rpc.Message; | ||||
|  | ||||
| namespace Phantom.Common.Messages.ToServer; | ||||
|  | ||||
| [MessagePackObject] | ||||
| public sealed record ReportInstanceStatusMessage( | ||||
| 	[property: Key(0)] Guid InstanceGuid, | ||||
| 	[property: Key(1)] InstanceStatus InstanceStatus | ||||
| [MemoryPackable] | ||||
| public sealed partial record ReportInstanceStatusMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid InstanceGuid, | ||||
| 	[property: MemoryPackOrder(1)] IInstanceStatus InstanceStatus | ||||
| ) : IMessageToServer { | ||||
| 	public Task Accept(IMessageToServerListener listener) { | ||||
| 	public Task<NoReply> Accept(IMessageToServerListener listener) { | ||||
| 		return listener.HandleReportInstanceStatus(this); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,22 +0,0 @@ | ||||
| using System.Runtime.CompilerServices; | ||||
| using MessagePack; | ||||
|  | ||||
| namespace Phantom.Common.Messages.ToServer; | ||||
|  | ||||
| [MessagePackObject] | ||||
| public sealed record SimpleReplyMessage( | ||||
| 	[property: Key(0)] uint SequenceId, | ||||
| 	[property: Key(1)] int EnumValue | ||||
| ) : IMessageToServer { | ||||
| 	public static SimpleReplyMessage FromEnum<TEnum>(uint sequenceId, TEnum enumValue) where TEnum : Enum { | ||||
| 		if (Unsafe.SizeOf<TEnum>() != Unsafe.SizeOf<int>()) { | ||||
| 			throw new ArgumentException("Enum type " + typeof(TEnum).Name + " is not compatible with int.", nameof(TEnum)); | ||||
| 		} | ||||
|  | ||||
| 		return new SimpleReplyMessage(sequenceId, Unsafe.As<TEnum, int>(ref enumValue)); | ||||
| 	} | ||||
|  | ||||
| 	public Task Accept(IMessageToServerListener listener) { | ||||
| 		return listener.HandleSimpleReply(this); | ||||
| 	} | ||||
| } | ||||
| @@ -1,12 +1,13 @@ | ||||
| using MessagePack; | ||||
| using MemoryPack; | ||||
| using Phantom.Utils.Rpc.Message; | ||||
|  | ||||
| namespace Phantom.Common.Messages.ToServer; | ||||
|  | ||||
| [MessagePackObject] | ||||
| public sealed record UnregisterAgentMessage( | ||||
| 	[property: Key(0)] Guid AgentGuid | ||||
| [MemoryPackable] | ||||
| public sealed partial record UnregisterAgentMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid AgentGuid | ||||
| ) : IMessageToServer { | ||||
| 	public Task Accept(IMessageToServerListener listener) { | ||||
| 	public Task<NoReply> Accept(IMessageToServerListener listener) { | ||||
| 		return listener.HandleUnregisterAgent(this); | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										49
									
								
								Common/Phantom.Common.Minecraft/JvmArgumentsHelper.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								Common/Phantom.Common.Minecraft/JvmArgumentsHelper.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| using System.Collections.Immutable; | ||||
|  | ||||
| namespace Phantom.Common.Minecraft; | ||||
|  | ||||
| public static class JvmArgumentsHelper { | ||||
| 	public static ImmutableArray<string> Split(string arguments) { | ||||
| 		return arguments.Split('\n', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToImmutableArray(); | ||||
| 	} | ||||
|  | ||||
| 	public static ValidationError? Validate(string arguments) { | ||||
| 		return Validate(Split(arguments)); | ||||
| 	} | ||||
| 	 | ||||
| 	public static ValidationError? Validate(ImmutableArray<string> arguments) { | ||||
| 		if (!arguments.All(static argument => argument.StartsWith('-'))) { | ||||
| 			return ValidationError.InvalidFormat; | ||||
| 		} | ||||
|  | ||||
| 		// TODO not perfect, but good enough | ||||
| 		if (arguments.Any(static argument => argument.Contains("-Xmx"))) { | ||||
| 			return ValidationError.XmxNotAllowed; | ||||
| 		} | ||||
| 		 | ||||
| 		if (arguments.Any(static argument => argument.Contains("-Xms"))) { | ||||
| 			return ValidationError.XmsNotAllowed; | ||||
| 		} | ||||
| 		 | ||||
| 		return null; | ||||
| 	} | ||||
|  | ||||
| 	public static string Join(ImmutableArray<string> arguments) { | ||||
| 		return string.Join('\n', arguments); | ||||
| 	} | ||||
|  | ||||
| 	public enum ValidationError { | ||||
| 		InvalidFormat, | ||||
| 		XmxNotAllowed, | ||||
| 		XmsNotAllowed | ||||
| 	} | ||||
|  | ||||
| 	public static string ToSentence(this ValidationError? result) { | ||||
| 		return result switch { | ||||
| 			ValidationError.InvalidFormat => "Invalid format.", | ||||
| 			ValidationError.XmxNotAllowed => "The -Xmx argument must not be specified manually.", | ||||
| 			ValidationError.XmsNotAllowed => "The -Xms argument must not be specified manually.", | ||||
| 			_                             => throw new ArgumentOutOfRangeException(nameof(result), result, null) | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
| @@ -1,9 +1,9 @@ | ||||
| using Phantom.Utils.Cryptography; | ||||
| using Phantom.Utils.IO; | ||||
| 
 | ||||
| namespace Phantom.Agent.Minecraft.Server;  | ||||
| namespace Phantom.Common.Minecraft;  | ||||
| 
 | ||||
| sealed record ServerExecutableInfo( | ||||
| public sealed record MinecraftServerExecutableInfo( | ||||
| 	string DownloadUrl, | ||||
| 	Sha1String Hash, | ||||
| 	FileSize Size | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user