1
0
mirror of https://github.com/chylex/Minecraft-Phantom-Panel.git synced 2025-09-15 15:32:10 +02:00

1 Commits

Author SHA1 Message Date
fad7b35e04 Add Agent shutdown button to web 2022-10-07 17:24:14 +02:00
264 changed files with 1903 additions and 8889 deletions

View File

@@ -1,9 +0,0 @@
# Ignore hidden files
.*
# Include .git for build version information
!.git
# Not needed for building
AddMigration.*
*.DotSettings.user

View File

@@ -5,13 +5,13 @@
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent1" /> <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent1" />
<option name="PASS_PARENT_ENVS" value="1" /> <option name="PASS_PARENT_ENVS" value="1" />
<envs> <envs>
<env name="AGENT_KEY" value="JXBQQYG5T267RQS75MXWBTCJZY5CKTCCGQY22MCZPHSQQSJYCHH2NG2TCNXQY6TBSXM9NQDRS2CMX" />
<env name="AGENT_NAME" value="Agent 1" /> <env name="AGENT_NAME" value="Agent 1" />
<env name="ALLOWED_RCON_PORTS" value="25575,27000,27001" /> <env name="ALLOWED_RCON_PORTS" value="25575,27000,27001" />
<env name="ALLOWED_SERVER_PORTS" value="25565,26000,26001" /> <env name="ALLOWED_SERVER_PORTS" value="25565,26000,26001" />
<env name="JAVA_SEARCH_PATH" value="~/.jdks" /> <env name="JAVA_SEARCH_PATH" value="~/.jdks" />
<env name="MAX_INSTANCES" value="3" /> <env name="MAX_INSTANCES" value="3" />
<env name="MAX_MEMORY" value="12G" /> <env name="MAX_MEMORY" value="12G" />
<env name="SERVER_AUTH_TOKEN_FILE" value="./secrets/agent.token" />
<env name="SERVER_HOST" value="localhost" /> <env name="SERVER_HOST" value="localhost" />
</envs> </envs>
<option name="USE_EXTERNAL_CONSOLE" value="0" /> <option name="USE_EXTERNAL_CONSOLE" value="0" />

View File

@@ -5,13 +5,13 @@
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent2" /> <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent2" />
<option name="PASS_PARENT_ENVS" value="1" /> <option name="PASS_PARENT_ENVS" value="1" />
<envs> <envs>
<env name="AGENT_KEY" value="JXBQQYG5T267RQS75MXWBTCJZY5CKTCCGQY22MCZPHSQQSJYCHH2NG2TCNXQY6TBSXM9NQDRS2CMX" />
<env name="AGENT_NAME" value="Agent 2" /> <env name="AGENT_NAME" value="Agent 2" />
<env name="ALLOWED_RCON_PORTS" value="27002-27006" /> <env name="ALLOWED_RCON_PORTS" value="27002-27006" />
<env name="ALLOWED_SERVER_PORTS" value="26002-26006" /> <env name="ALLOWED_SERVER_PORTS" value="26002-26006" />
<env name="JAVA_SEARCH_PATH" value="~/.jdks" /> <env name="JAVA_SEARCH_PATH" value="~/.jdks" />
<env name="MAX_INSTANCES" value="5" /> <env name="MAX_INSTANCES" value="5" />
<env name="MAX_MEMORY" value="10G" /> <env name="MAX_MEMORY" value="10G" />
<env name="SERVER_AUTH_TOKEN_FILE" value="./secrets/agent.token" />
<env name="SERVER_HOST" value="localhost" /> <env name="SERVER_HOST" value="localhost" />
</envs> </envs>
<option name="USE_EXTERNAL_CONSOLE" value="0" /> <option name="USE_EXTERNAL_CONSOLE" value="0" />

View File

@@ -5,13 +5,13 @@
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent3" /> <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent3" />
<option name="PASS_PARENT_ENVS" value="1" /> <option name="PASS_PARENT_ENVS" value="1" />
<envs> <envs>
<env name="AGENT_KEY" value="JXBQQYG5T267RQS75MXWBTCJZY5CKTCCGQY22MCZPHSQQSJYCHH2NG2TCNXQY6TBSXM9NQDRS2CMX" />
<env name="AGENT_NAME" value="Agent 3" /> <env name="AGENT_NAME" value="Agent 3" />
<env name="ALLOWED_RCON_PORTS" value="27007" /> <env name="ALLOWED_RCON_PORTS" value="27007" />
<env name="ALLOWED_SERVER_PORTS" value="26007" /> <env name="ALLOWED_SERVER_PORTS" value="26007" />
<env name="JAVA_SEARCH_PATH" value="~/.jdks" /> <env name="JAVA_SEARCH_PATH" value="~/.jdks" />
<env name="MAX_INSTANCES" value="1" /> <env name="MAX_INSTANCES" value="1" />
<env name="MAX_MEMORY" value="2560M" /> <env name="MAX_MEMORY" value="2560M" />
<env name="SERVER_AUTH_TOKEN_FILE" value="./secrets/agent.token" />
<env name="SERVER_HOST" value="localhost" /> <env name="SERVER_HOST" value="localhost" />
</envs> </envs>
<option name="USE_EXTERNAL_CONSOLE" value="0" /> <option name="USE_EXTERNAL_CONSOLE" value="0" />

2
.workdir/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/Agent*/data
/Agent*/temp

View File

@@ -1,2 +0,0 @@
*
!.gitignore

View File

@@ -0,0 +1 @@
tLkn<EFBFBD>Z<EFBFBD><EFBFBD><><D18B>|~2<><32>><3E><>р<EFBFBD><D180>O*<2A> p

View File

@@ -0,0 +1 @@
TH7Z2MJKMR975N6HFBKQP9WQFMWWN5

View File

@@ -1,2 +0,0 @@
*
!.gitignore

View File

@@ -0,0 +1 @@
tLkn<EFBFBD>Z<EFBFBD><EFBFBD><><D18B>|~2<><32>><3E><>р<EFBFBD><D180>O*<2A> p

View File

@@ -0,0 +1 @@
TH7Z2MJKMR975N6HFBKQP9WQFMWWN5

View File

@@ -1,2 +0,0 @@
*
!.gitignore

View File

@@ -0,0 +1 @@
tLkn<EFBFBD>Z<EFBFBD><EFBFBD><><D18B>|~2<><32>><3E><>р<EFBFBD><D180>O*<2A> p

View File

@@ -0,0 +1 @@
TH7Z2MJKMR975N6HFBKQP9WQFMWWN5

View File

@@ -1 +0,0 @@
/keys

Binary file not shown.

View File

@@ -0,0 +1 @@
TH7Z2MJKMR975N6HFBKQP9WQFMWWN5

View File

@@ -1 +1 @@
<EFBFBD>Z<EFBFBD>t<>MPI<49>GMZ<4D><5A><EFBFBD><EFBFBD>kN<6B>VF1X<><58>p +<2B><><EFBFBD><EFBFBD><<3C>f:<3A>bJ"e<18>׸ބ<D7B8><1F><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>

View File

@@ -1,9 +0,0 @@
namespace Phantom.Agent.Minecraft.Command;
public static class MinecraftCommand {
public const string Stop = "stop";
public static string Say(string message) {
return "say " + message;
}
}

View File

@@ -1,5 +1,4 @@
using System.Collections.Immutable; using Phantom.Agent.Minecraft.Java;
using Phantom.Agent.Minecraft.Java;
using Phantom.Agent.Minecraft.Properties; using Phantom.Agent.Minecraft.Properties;
namespace Phantom.Agent.Minecraft.Instance; namespace Phantom.Agent.Minecraft.Instance;
@@ -7,7 +6,6 @@ namespace Phantom.Agent.Minecraft.Instance;
public sealed record InstanceProperties( public sealed record InstanceProperties(
Guid JavaRuntimeGuid, Guid JavaRuntimeGuid,
JvmProperties JvmProperties, JvmProperties JvmProperties,
ImmutableArray<string> JvmArguments,
string InstanceFolder, string InstanceFolder,
string ServerVersion, string ServerVersion,
ServerProperties ServerProperties ServerProperties ServerProperties

View File

@@ -7,7 +7,7 @@ using Serilog;
namespace Phantom.Agent.Minecraft.Java; namespace Phantom.Agent.Minecraft.Java;
public sealed class JavaRuntimeDiscovery { public sealed class JavaRuntimeDiscovery {
private static readonly ILogger Logger = PhantomLogger.Create(nameof(JavaRuntimeDiscovery)); private static readonly ILogger Logger = PhantomLogger.Create(typeof(JavaRuntimeDiscovery));
public static string? GetSystemSearchPath() { public static string? GetSystemSearchPath() {
const string LinuxJavaPath = "/usr/lib/jvm"; const string LinuxJavaPath = "/usr/lib/jvm";
@@ -38,38 +38,28 @@ public sealed class JavaRuntimeDiscovery {
AttributesToSkip = FileAttributes.Hidden | FileAttributes.ReparsePoint | FileAttributes.System AttributesToSkip = FileAttributes.Hidden | FileAttributes.ReparsePoint | FileAttributes.System
}).Order()) { }).Order()) {
var javaExecutablePath = Paths.NormalizeSlashes(Path.Combine(binFolderPath, javaExecutableName)); var javaExecutablePath = Paths.NormalizeSlashes(Path.Combine(binFolderPath, javaExecutableName));
if (File.Exists(javaExecutablePath)) {
FileAttributes javaExecutableAttributes; Logger.Information("Found candidate Java executable: {JavaExecutablePath}", javaExecutablePath);
try {
javaExecutableAttributes = File.GetAttributes(javaExecutablePath); JavaRuntime? foundRuntime;
} catch (Exception) { try {
continue; 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);
} }
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);
} }
} }

View File

@@ -1,31 +1,25 @@
using System.Collections.Immutable; using System.Collections.ObjectModel;
using System.Collections.ObjectModel;
namespace Phantom.Agent.Minecraft.Java; namespace Phantom.Agent.Minecraft.Java;
sealed class JvmArgumentBuilder { sealed class JvmArgumentBuilder {
private readonly JvmProperties basicProperties; private readonly JvmProperties basicProperties;
private readonly List<string> customArguments = new (); private readonly List<string> customProperties = new ();
public JvmArgumentBuilder(JvmProperties basicProperties, ImmutableArray<string> customArguments) { public JvmArgumentBuilder(JvmProperties basicProperties) {
this.basicProperties = basicProperties; this.basicProperties = basicProperties;
foreach (var jvmArgument in customArguments) {
this.customArguments.Add(jvmArgument);
}
} }
public void AddProperty(string key, string value) { public void AddProperty(string key, string value) {
customArguments.Add("-D" + key + "=\"" + value + "\""); // TODO test quoting? customProperties.Add("-D" + key + "=\"" + value + "\""); // TODO test quoting?
} }
public void Build(Collection<string> target) { public void Build(Collection<string> target) {
foreach (var property in customArguments) {
target.Add(property);
}
target.Add("-Xms" + basicProperties.InitialHeapMegabytes + "M"); target.Add("-Xms" + basicProperties.InitialHeapMegabytes + "M");
target.Add("-Xmx" + basicProperties.MaximumHeapMegabytes + "M"); target.Add("-Xmx" + basicProperties.MaximumHeapMegabytes + "M");
target.Add("-Xrs");
foreach (var property in customProperties) {
target.Add(property);
}
} }
} }

View File

@@ -4,8 +4,6 @@ using Kajabity.Tools.Java;
using Phantom.Agent.Minecraft.Instance; using Phantom.Agent.Minecraft.Instance;
using Phantom.Agent.Minecraft.Java; using Phantom.Agent.Minecraft.Java;
using Phantom.Agent.Minecraft.Server; using Phantom.Agent.Minecraft.Server;
using Phantom.Common.Minecraft;
using Serilog;
namespace Phantom.Agent.Minecraft.Launcher; namespace Phantom.Agent.Minecraft.Launcher;
@@ -16,15 +14,11 @@ public abstract class BaseLauncher {
this.instanceProperties = instanceProperties; this.instanceProperties = instanceProperties;
} }
public async Task<LaunchResult> Launch(ILogger logger, LaunchServices services, EventHandler<DownloadProgressEventArgs> downloadProgressEventHandler, CancellationToken cancellationToken) { public async Task<LaunchResult> Launch(LaunchServices services, EventHandler<DownloadProgressEventArgs> downloadProgressEventHandler, CancellationToken cancellationToken) {
if (!services.JavaRuntimeRepository.TryGetByGuid(instanceProperties.JavaRuntimeGuid, out var javaRuntimeExecutable)) { if (!services.JavaRuntimeRepository.TryGetByGuid(instanceProperties.JavaRuntimeGuid, out var javaRuntimeExecutable)) {
return new LaunchResult.InvalidJavaRuntime(); return new LaunchResult.InvalidJavaRuntime();
} }
if (JvmArgumentsHelper.Validate(instanceProperties.JvmArguments) != null) {
return new LaunchResult.InvalidJvmArguments();
}
var vanillaServerJarPath = await services.ServerExecutables.DownloadAndGetPath(instanceProperties.ServerVersion, downloadProgressEventHandler, cancellationToken); var vanillaServerJarPath = await services.ServerExecutables.DownloadAndGetPath(instanceProperties.ServerVersion, downloadProgressEventHandler, cancellationToken);
if (vanillaServerJarPath == null) { if (vanillaServerJarPath == null) {
return new LaunchResult.CouldNotDownloadMinecraftServer(); return new LaunchResult.CouldNotDownloadMinecraftServer();
@@ -39,8 +33,8 @@ public abstract class BaseLauncher {
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = false CreateNoWindow = false
}; };
var jvmArguments = new JvmArgumentBuilder(instanceProperties.JvmProperties, instanceProperties.JvmArguments); var jvmArguments = new JvmArgumentBuilder(instanceProperties.JvmProperties);
CustomizeJvmArguments(jvmArguments); CustomizeJvmArguments(jvmArguments);
var serverJarPath = await PrepareServerJar(vanillaServerJarPath, instanceProperties.InstanceFolder, cancellationToken); var serverJarPath = await PrepareServerJar(vanillaServerJarPath, instanceProperties.InstanceFolder, cancellationToken);
@@ -53,29 +47,12 @@ public abstract class BaseLauncher {
var process = new Process { StartInfo = startInfo }; var process = new Process { StartInfo = startInfo };
var session = new InstanceSession(process); var session = new InstanceSession(process);
try { await AcceptEula(instanceProperties);
await AcceptEula(instanceProperties); await UpdateServerProperties(instanceProperties);
await UpdateServerProperties(instanceProperties);
} catch (Exception e) {
logger.Error(e, "Caught exception while configuring the server.");
return new LaunchResult.CouldNotConfigureMinecraftServer();
}
try { process.Start();
process.Start(); process.BeginOutputReadLine();
process.BeginOutputReadLine(); process.BeginErrorReadLine();
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); return new LaunchResult.Success(session);
} }
@@ -95,18 +72,16 @@ public abstract class BaseLauncher {
var serverPropertiesFilePath = Path.Combine(instanceProperties.InstanceFolder, "server.properties"); var serverPropertiesFilePath = Path.Combine(instanceProperties.InstanceFolder, "server.properties");
var serverPropertiesData = new JavaProperties(); var serverPropertiesData = new JavaProperties();
await using var fileStream = new FileStream(serverPropertiesFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
try { try {
serverPropertiesData.Load(fileStream); await using var readStream = new FileStream(serverPropertiesFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
} catch (ParseException e) { serverPropertiesData.Load(readStream);
throw new Exception("Could not parse server.properties file: " + serverPropertiesFilePath, e); } catch (FileNotFoundException) {
// ignore
} }
instanceProperties.ServerProperties.SetTo(serverPropertiesData); instanceProperties.ServerProperties.SetTo(serverPropertiesData);
fileStream.Seek(0L, SeekOrigin.Begin); await using var writeStream = new FileStream(serverPropertiesFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None);
fileStream.SetLength(0L); serverPropertiesData.Store(writeStream, true);
serverPropertiesData.Store(fileStream, true);
} }
} }

View File

@@ -8,12 +8,6 @@ public abstract record LaunchResult {
public sealed record Success(InstanceSession Session) : LaunchResult; public sealed record Success(InstanceSession Session) : LaunchResult;
public sealed record InvalidJavaRuntime : LaunchResult; public sealed record InvalidJavaRuntime : LaunchResult;
public sealed record InvalidJvmArguments : LaunchResult;
public sealed record CouldNotDownloadMinecraftServer : LaunchResult; public sealed record CouldNotDownloadMinecraftServer : LaunchResult;
public sealed record CouldNotConfigureMinecraftServer : LaunchResult;
public sealed record CouldNotStartMinecraftServer : LaunchResult;
} }

View File

@@ -1,7 +1,6 @@
using Phantom.Agent.Minecraft.Java; using Phantom.Agent.Minecraft.Java;
using Phantom.Agent.Minecraft.Server; using Phantom.Agent.Minecraft.Server;
using Phantom.Utils.Runtime;
namespace Phantom.Agent.Minecraft.Launcher; namespace Phantom.Agent.Minecraft.Launcher;
public sealed record LaunchServices(TaskManager TaskManager, MinecraftServerExecutables ServerExecutables, JavaRuntimeRepository JavaRuntimeRepository); public sealed record LaunchServices(MinecraftServerExecutables ServerExecutables, JavaRuntimeRepository JavaRuntimeRepository);

View File

@@ -7,17 +7,15 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Kajabity.Tools.Java" /> <PackageReference Include="Kajabity.Tools.Java" Version="0.3.7879.40798" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Common\Phantom.Common.Data\Phantom.Common.Data.csproj" /> <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.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.Collections\Phantom.Utils.Collections.csproj" />
<ProjectReference Include="..\..\Utils\Phantom.Utils.Cryptography\Phantom.Utils.Cryptography.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.IO\Phantom.Utils.IO.csproj" />
<ProjectReference Include="..\..\Utils\Phantom.Utils.Runtime\Phantom.Utils.Runtime.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,9 +1,9 @@
using System.Security.Cryptography; using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text.Json;
using Phantom.Common.Logging; using Phantom.Common.Logging;
using Phantom.Common.Minecraft;
using Phantom.Utils.Cryptography; using Phantom.Utils.Cryptography;
using Phantom.Utils.IO; using Phantom.Utils.IO;
using Phantom.Utils.Runtime;
using Serilog; using Serilog;
namespace Phantom.Agent.Minecraft.Server; namespace Phantom.Agent.Minecraft.Server;
@@ -11,8 +11,8 @@ namespace Phantom.Agent.Minecraft.Server;
sealed class MinecraftServerExecutableDownloader { sealed class MinecraftServerExecutableDownloader {
private static readonly ILogger Logger = PhantomLogger.Create<MinecraftServerExecutableDownloader>(); private static readonly ILogger Logger = PhantomLogger.Create<MinecraftServerExecutableDownloader>();
private readonly MinecraftVersions minecraftVersions; private const string VersionManifestUrl = "https://launchermeta.mojang.com/mc/game/version_manifest.json";
public Task<string?> Task { get; } public Task<string?> Task { get; }
public event EventHandler<DownloadProgressEventArgs>? DownloadProgress; public event EventHandler<DownloadProgressEventArgs>? DownloadProgress;
public event EventHandler? Completed; public event EventHandler? Completed;
@@ -20,9 +20,7 @@ sealed class MinecraftServerExecutableDownloader {
private readonly CancellationTokenSource cancellationTokenSource = new (); private readonly CancellationTokenSource cancellationTokenSource = new ();
private int listeners = 0; private int listeners = 0;
public MinecraftServerExecutableDownloader(MinecraftVersions minecraftVersions, string version, string filePath, MinecraftServerExecutableDownloadListener listener) { public MinecraftServerExecutableDownloader(string version, string filePath, MinecraftServerExecutableDownloadListener listener) {
this.minecraftVersions = minecraftVersions;
Register(listener); Register(listener);
Task = DownloadAndGetPath(version, filePath); Task = DownloadAndGetPath(version, filePath);
Task.ContinueWith(OnCompleted, TaskScheduler.Default); Task.ContinueWith(OnCompleted, TaskScheduler.Default);
@@ -75,18 +73,21 @@ sealed class MinecraftServerExecutableDownloader {
private async Task<string?> DownloadAndGetPath(string version, string filePath) { private async Task<string?> DownloadAndGetPath(string version, string filePath) {
Logger.Information("Downloading server version {Version}...", version); Logger.Information("Downloading server version {Version}...", version);
HttpClient http = new HttpClient();
string tmpFilePath = filePath + ".tmp"; string tmpFilePath = filePath + ".tmp";
var cancellationToken = cancellationTokenSource.Token; var cancellationToken = cancellationTokenSource.Token;
try { try {
var serverExecutableInfo = await minecraftVersions.GetServerExecutableInfo(version, cancellationToken); Logger.Information("Fetching version manifest from: {Url}", VersionManifestUrl);
if (serverExecutableInfo == null) { var versionManifest = await FetchVersionManifest(http, cancellationToken);
return null; 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);
Logger.Information("Downloading server executable from: {Url} ({Size})", serverExecutableInfo.DownloadUrl, serverExecutableInfo.Size.ToHumanReadable(decimalPlaces: 1)); Logger.Information("Downloading server executable from: {Url} ({Size})", serverExecutableInfo.DownloadUrl, serverExecutableInfo.Size.ToHumanReadable(decimalPlaces: 1));
try { try {
using var http = new HttpClient();
await FetchServerExecutableFile(http, new DownloadProgressCallback(this), serverExecutableInfo, tmpFilePath, cancellationToken); await FetchServerExecutableFile(http, new DownloadProgressCallback(this), serverExecutableInfo, tmpFilePath, cancellationToken);
} catch (Exception) { } catch (Exception) {
TryDeleteExecutableAfterFailure(tmpFilePath); TryDeleteExecutableAfterFailure(tmpFilePath);
@@ -110,7 +111,31 @@ sealed class MinecraftServerExecutableDownloader {
} }
} }
private static async Task FetchServerExecutableFile(HttpClient http, DownloadProgressCallback progressCallback, MinecraftServerExecutableInfo info, string filePath, CancellationToken cancellationToken) { 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) {
Sha1String downloadedFileHash; Sha1String downloadedFileHash;
try { try {
@@ -145,6 +170,83 @@ 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 sealed class MinecraftServerDownloadStreamCopier : IDisposable {
private readonly StreamCopier streamCopier = new (); private readonly StreamCopier streamCopier = new ();
private readonly IncrementalHash sha1 = IncrementalHash.CreateHash(HashAlgorithmName.SHA1); private readonly IncrementalHash sha1 = IncrementalHash.CreateHash(HashAlgorithmName.SHA1);
@@ -176,4 +278,10 @@ sealed class MinecraftServerExecutableDownloader {
streamCopier.Dispose(); streamCopier.Dispose();
} }
} }
private sealed class StopProcedureException : Exception {
public static StopProcedureException Instance { get; } = new ();
private StopProcedureException() {}
}
} }

View File

@@ -1,19 +1,15 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Phantom.Common.Logging; using Phantom.Common.Logging;
using Phantom.Common.Minecraft;
using Phantom.Utils.IO;
using Serilog; using Serilog;
namespace Phantom.Agent.Minecraft.Server; namespace Phantom.Agent.Minecraft.Server;
public sealed partial class MinecraftServerExecutables : IDisposable { public sealed class MinecraftServerExecutables {
private static readonly ILogger Logger = PhantomLogger.Create<MinecraftServerExecutables>(); private static readonly ILogger Logger = PhantomLogger.Create<MinecraftServerExecutables>();
[GeneratedRegex(@"[^a-zA-Z0-9_\-\.]", RegexOptions.Compiled)] private static readonly Regex VersionFolderSanitizeRegex = new (@"[^a-zA-Z0-9_\-\.]", RegexOptions.Compiled);
private static partial Regex VersionFolderSanitizeRegex();
private readonly string basePath; private readonly string basePath;
private readonly MinecraftVersions minecraftVersions = new ();
private readonly Dictionary<string, MinecraftServerExecutableDownloader> runningDownloadersByVersion = new (); private readonly Dictionary<string, MinecraftServerExecutableDownloader> runningDownloadersByVersion = new ();
public MinecraftServerExecutables(string basePath) { public MinecraftServerExecutables(string basePath) {
@@ -21,7 +17,7 @@ public sealed partial class MinecraftServerExecutables : IDisposable {
} }
internal async Task<string?> DownloadAndGetPath(string version, EventHandler<DownloadProgressEventArgs> progressEventHandler, CancellationToken cancellationToken) { 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"); string serverExecutableFilePath = Path.Combine(serverExecutableFolderPath, "server.jar");
if (File.Exists(serverExecutableFilePath)) { if (File.Exists(serverExecutableFilePath)) {
@@ -29,7 +25,7 @@ public sealed partial class MinecraftServerExecutables : IDisposable {
} }
try { try {
Directories.Create(serverExecutableFolderPath, Chmod.URWX_GRX); Directory.CreateDirectory(serverExecutableFolderPath);
} catch (Exception e) { } catch (Exception e) {
Logger.Error(e, "Unable to create folder for server executable: {ServerExecutableFolderPath}", serverExecutableFolderPath); Logger.Error(e, "Unable to create folder for server executable: {ServerExecutableFolderPath}", serverExecutableFolderPath);
return null; return null;
@@ -44,21 +40,17 @@ public sealed partial class MinecraftServerExecutables : IDisposable {
downloader.Register(listener); downloader.Register(listener);
} }
else { else {
downloader = new MinecraftServerExecutableDownloader(minecraftVersions, version, serverExecutableFilePath, listener); downloader = new MinecraftServerExecutableDownloader(version, serverExecutableFilePath, listener);
downloader.Completed += (_, _) => { downloader.Completed += (_, _) => {
lock (this) { lock (this) {
runningDownloadersByVersion.Remove(version); runningDownloadersByVersion.Remove(version);
} }
}; };
runningDownloadersByVersion[version] = downloader; runningDownloadersByVersion[version] = downloader;
} }
} }
return await downloader.Task.WaitAsync(cancellationToken); return await downloader.Task.WaitAsync(cancellationToken);
} }
public void Dispose() {
minecraftVersions.Dispose();
}
} }

View File

@@ -1,9 +1,9 @@
using Phantom.Utils.Cryptography; using Phantom.Utils.Cryptography;
using Phantom.Utils.IO; using Phantom.Utils.IO;
namespace Phantom.Common.Minecraft; namespace Phantom.Agent.Minecraft.Server;
public sealed record MinecraftServerExecutableInfo( sealed record ServerExecutableInfo(
string DownloadUrl, string DownloadUrl,
Sha1String Hash, Sha1String Hash,
FileSize Size FileSize Size

View File

@@ -1,40 +0,0 @@
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();
}
}

View File

@@ -6,6 +6,10 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="Serilog" Version="2.12.0" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Common\Phantom.Common.Messages\Phantom.Common.Messages.csproj" /> <ProjectReference Include="..\..\Common\Phantom.Common.Messages\Phantom.Common.Messages.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -0,0 +1,19 @@
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));
}
}

View File

@@ -2,45 +2,36 @@
using NetMQ.Sockets; using NetMQ.Sockets;
using Phantom.Common.Data.Agent; using Phantom.Common.Data.Agent;
using Phantom.Common.Messages; using Phantom.Common.Messages;
using Phantom.Common.Messages.BiDirectional;
using Phantom.Common.Messages.ToServer; using Phantom.Common.Messages.ToServer;
using Phantom.Utils.Rpc; using Phantom.Utils.Rpc;
using Phantom.Utils.Rpc.Message;
using Phantom.Utils.Runtime;
using Serilog;
using Serilog.Events; using Serilog.Events;
namespace Phantom.Agent.Rpc; namespace Phantom.Agent.Rpc;
public sealed class RpcLauncher : RpcRuntime<ClientSocket> { public sealed class RpcLauncher : RpcRuntime<ClientSocket> {
public static async Task Launch(RpcConfiguration config, AgentAuthToken authToken, AgentInfo agentInfo, Func<RpcServerConnection, IMessageToAgentListener> listenerFactory, SemaphoreSlim disconnectSemaphore, CancellationToken receiveCancellationToken) { public static async Task Launch(RpcConfiguration config, AgentAuthToken authToken, AgentInfo agentInfo, Func<ClientSocket, IMessageToAgentListener> listenerFactory) {
var socket = new ClientSocket(); var socket = new ClientSocket();
var options = socket.Options; var options = socket.Options;
options.CurveServerCertificate = config.ServerCertificate; options.CurveServerCertificate = config.ServerCertificate;
options.CurveCertificate = new NetMQCertificate(); options.CurveCertificate = new NetMQCertificate();
options.HelloMessage = MessageRegistries.ToServer.Write(new RegisterAgentMessage(authToken, agentInfo)).ToArray(); options.HelloMessage = MessageRegistries.ToServer.Write(new RegisterAgentMessage(authToken, agentInfo)).ToArray();
await new RpcLauncher(config, socket, agentInfo.Guid, listenerFactory, disconnectSemaphore, receiveCancellationToken).Launch(); await new RpcLauncher(config, socket, agentInfo.Guid, listenerFactory).Launch();
} }
private readonly RpcConfiguration config; private readonly RpcConfiguration config;
private readonly Guid agentGuid; private readonly Guid agentGuid;
private readonly Func<RpcServerConnection, IMessageToAgentListener> messageListenerFactory; private readonly Func<ClientSocket, IMessageToAgentListener> messageListenerFactory;
private readonly SemaphoreSlim disconnectSemaphore; private RpcLauncher(RpcConfiguration config, ClientSocket socket, Guid agentGuid, Func<ClientSocket, IMessageToAgentListener> messageListenerFactory) : base(socket, config.CancellationToken) {
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.config = config;
this.agentGuid = agentGuid; this.agentGuid = agentGuid;
this.messageListenerFactory = messageListenerFactory; this.messageListenerFactory = messageListenerFactory;
this.disconnectSemaphore = disconnectSemaphore;
this.receiveCancellationToken = receiveCancellationToken;
} }
protected override void Connect(ClientSocket socket) { protected override void Connect(ClientSocket socket) {
var logger = config.RuntimeLogger; var logger = config.Logger;
var url = config.TcpUrl; var url = config.TcpUrl;
logger.Information("Starting ZeroMQ client and connecting to {Url}...", url); logger.Information("Starting ZeroMQ client and connecting to {Url}...", url);
@@ -48,60 +39,35 @@ public sealed class RpcLauncher : RpcRuntime<ClientSocket> {
logger.Information("ZeroMQ client ready."); logger.Information("ZeroMQ client ready.");
} }
protected override void Run(ClientSocket socket, MessageReplyTracker replyTracker, TaskManager taskManager) { protected override async Task Run(ClientSocket socket, CancellationToken cancellationToken) {
var connection = new RpcServerConnection(socket, replyTracker); var logger = config.Logger;
ServerMessaging.SetCurrentConnection(connection);
var logger = config.RuntimeLogger; var listener = messageListenerFactory(socket);
var handler = new MessageToAgentHandler(messageListenerFactory(connection), logger, taskManager, receiveCancellationToken);
var keepAliveLoop = new KeepAliveLoop(connection); ServerMessaging.SetCurrentSocket(socket, cancellationToken);
try { // TODO optimize msg
while (!receiveCancellationToken.IsCancellationRequested) { await foreach (var bytes in socket.ReceiveBytesAsyncEnumerable(cancellationToken)) {
var data = socket.Receive(receiveCancellationToken); if (logger.IsEnabled(LogEventLevel.Verbose)) {
if (bytes.Length > 0 && MessageRegistries.ToAgent.TryGetType(bytes, out var type)) {
LogMessageType(logger, data); logger.Verbose("Received {MessageType} ({Bytes} B) from server.", type.Name, bytes.Length);
}
if (data.Length > 0) { else {
MessageRegistries.ToAgent.Handle(data, handler); logger.Verbose("Received {Bytes} B message from server.", bytes.Length);
} }
} }
} catch (OperationCanceledException) {
// Ignore.
} finally {
logger.Verbose("ZeroMQ client stopped receiving messages.");
disconnectSemaphore.Wait(CancellationToken.None); if (bytes.Length > 0) {
keepAliveLoop.Cancel(); MessageRegistries.ToAgent.Handle(bytes, listener, cancellationToken);
}
} }
} }
private static void LogMessageType(ILogger logger, ReadOnlyMemory<byte> data) { protected override async Task Disconnect(ClientSocket socket) {
if (!logger.IsEnabled(LogEventLevel.Verbose)) { var timeoutTask = Task.Delay(TimeSpan.FromSeconds(5));
return; 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.");
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));
} }
} }
} }

View File

@@ -1,41 +0,0 @@
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 waitForReplyCancellationToken) 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, waitForReplyCancellationToken);
}
public void Receive(ReplyMessage message) {
replyTracker.ReceiveReply(message.SequenceId, message.SerializedReply);
}
}

View File

@@ -1,34 +1,55 @@
using Phantom.Common.Logging; using NetMQ.Sockets;
using Phantom.Common.Logging;
using Phantom.Common.Messages; using Phantom.Common.Messages;
using Phantom.Common.Messages.ToServer;
using Serilog; using Serilog;
namespace Phantom.Agent.Rpc; namespace Phantom.Agent.Rpc;
public static class ServerMessaging { public static class ServerMessaging {
private static readonly ILogger Logger = PhantomLogger.Create(nameof(ServerMessaging)); private static readonly ILogger Logger = PhantomLogger.Create(typeof(ServerMessaging));
private static RpcServerConnection? CurrentConnection { get; set; } private static readonly TimeSpan KeepAliveInterval = TimeSpan.FromSeconds(10);
private static RpcServerConnection CurrentConnectionOrThrow => CurrentConnection ?? throw new InvalidOperationException("Server connection not ready.");
private static readonly object SetCurrentConnectionLock = new ();
internal static void SetCurrentConnection(RpcServerConnection connection) { private static ClientSocket? CurrentSocket { get; set; }
lock (SetCurrentConnectionLock) { private static readonly object SetCurrentSocketLock = new ();
if (CurrentConnection != null) {
throw new InvalidOperationException("Server connection can only be set once."); internal static void SetCurrentSocket(ClientSocket socket, CancellationToken cancellationToken) {
} Logger.Information("Server socket ready.");
CurrentConnection = connection;
}
Logger.Information("Server connection ready."); 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);
}
} }
public static Task Send<TMessage>(TMessage message) where TMessage : IMessageToServer { public static async Task SendMessage<TMessage>(TMessage message) where TMessage : IMessageToServer {
return CurrentConnectionOrThrow.Send(message); var currentSocket = CurrentSocket ?? throw new InvalidOperationException("Server socket not ready.");
await currentSocket.SendMessage(message);
} }
public static Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken) where TMessage : IMessageToServer<TReply> where TReply : class { private static async Task SendKeepAliveLoop(CancellationToken cancellationToken) {
return CurrentConnectionOrThrow.Send<TMessage, TReply>(message, waitForReplyTime, waitForReplyCancellationToken); 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.");
}
} }
} }

View File

@@ -1,26 +1,19 @@
using Phantom.Agent.Minecraft.Java; using Phantom.Agent.Minecraft.Java;
using Phantom.Agent.Services.Instances; using Phantom.Agent.Services.Instances;
using Phantom.Common.Data.Agent; using Phantom.Common.Data.Agent;
using Phantom.Common.Logging;
using Phantom.Utils.Runtime;
using Serilog;
namespace Phantom.Agent.Services; namespace Phantom.Agent.Services;
public sealed class AgentServices { public sealed class AgentServices {
private static readonly ILogger Logger = PhantomLogger.Create<AgentServices>();
private AgentFolders AgentFolders { get; } private AgentFolders AgentFolders { get; }
private TaskManager TaskManager { get; }
internal JavaRuntimeRepository JavaRuntimeRepository { get; } internal JavaRuntimeRepository JavaRuntimeRepository { get; }
internal InstanceSessionManager InstanceSessionManager { get; } internal InstanceSessionManager InstanceSessionManager { get; }
public AgentServices(AgentInfo agentInfo, AgentFolders agentFolders) { public AgentServices(AgentInfo agentInfo, AgentFolders agentFolders) {
this.AgentFolders = agentFolders; this.AgentFolders = agentFolders;
this.TaskManager = new TaskManager(PhantomLogger.Create<TaskManager, AgentServices>());
this.JavaRuntimeRepository = new JavaRuntimeRepository(); this.JavaRuntimeRepository = new JavaRuntimeRepository();
this.InstanceSessionManager = new InstanceSessionManager(agentInfo, agentFolders, JavaRuntimeRepository, TaskManager); this.InstanceSessionManager = new InstanceSessionManager(agentInfo, agentFolders, JavaRuntimeRepository);
} }
public async Task Initialize() { public async Task Initialize() {
@@ -30,13 +23,6 @@ public sealed class AgentServices {
} }
public async Task Shutdown() { public async Task Shutdown() {
Logger.Information("Stopping services...");
await InstanceSessionManager.StopAll(); await InstanceSessionManager.StopAll();
InstanceSessionManager.Dispose();
await TaskManager.Stop();
Logger.Information("Services stopped.");
} }
} }

View File

@@ -2,7 +2,6 @@
using Phantom.Agent.Rpc; using Phantom.Agent.Rpc;
using Phantom.Agent.Services.Instances.States; using Phantom.Agent.Services.Instances.States;
using Phantom.Common.Data.Instance; using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies; using Phantom.Common.Data.Replies;
using Phantom.Common.Logging; using Phantom.Common.Logging;
using Phantom.Common.Messages.ToServer; using Phantom.Common.Messages.ToServer;
@@ -33,7 +32,7 @@ sealed class Instance : IDisposable {
private readonly LaunchServices launchServices; private readonly LaunchServices launchServices;
private readonly PortManager portManager; private readonly PortManager portManager;
private IInstanceStatus currentStatus; private InstanceStatus currentStatus;
private IInstanceState currentState; private IInstanceState currentState;
private readonly SemaphoreSlim stateTransitioningActionSemaphore = new (1, 1); private readonly SemaphoreSlim stateTransitioningActionSemaphore = new (1, 1);
@@ -47,31 +46,24 @@ sealed class Instance : IDisposable {
this.launchServices = launchServices; this.launchServices = launchServices;
this.portManager = portManager; this.portManager = portManager;
this.currentState = new InstanceNotRunningState(); this.currentState = new InstanceNotRunningState();
this.currentStatus = InstanceStatus.NotRunning; this.currentStatus = InstanceStatus.IsNotRunning;
} }
private async Task ReportLastStatus() { private async Task ReportLastStatus() {
await ServerMessaging.Send(new ReportInstanceStatusMessage(Configuration.InstanceGuid, currentStatus)); await ServerMessaging.SendMessage(new ReportInstanceStatusMessage(Configuration.InstanceGuid, currentStatus));
} }
private void TransitionState(IInstanceState newState) { private bool TransitionState(IInstanceState newState) {
if (currentState == newState) { if (currentState == newState) {
return; return false;
} }
if (currentState is IDisposable disposable) { if (currentState is IDisposable disposable) {
disposable.Dispose(); disposable.Dispose();
} }
logger.Verbose("Transitioning instance state to: {NewState}", newState.GetType().Name);
currentState = newState; currentState = newState;
currentState.Initialize(); return true;
}
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) { public async Task Reconfigure(InstanceConfiguration configuration, BaseLauncher launcher, CancellationToken cancellationToken) {
@@ -85,32 +77,44 @@ sealed class Instance : IDisposable {
} }
} }
public async Task<LaunchInstanceResult> Launch(CancellationToken shutdownCancellationToken) { public async Task<LaunchInstanceResult> Launch(CancellationToken cancellationToken) {
await stateTransitioningActionSemaphore.WaitAsync(shutdownCancellationToken); await stateTransitioningActionSemaphore.WaitAsync(cancellationToken);
try { try {
return TransitionStateAndReturn(currentState.Launch(new InstanceContextImpl(this, shutdownCancellationToken))); if (TransitionState(currentState.Launch(new InstanceContextImpl(this)))) {
} catch (Exception e) { return LaunchInstanceResult.LaunchInitiated;
logger.Error(e, "Caught exception while launching instance."); }
return LaunchInstanceResult.UnknownError;
return currentState switch {
InstanceLaunchingState => LaunchInstanceResult.InstanceAlreadyLaunching,
InstanceRunningState => LaunchInstanceResult.InstanceAlreadyRunning,
InstanceStoppingState => LaunchInstanceResult.InstanceIsStopping,
_ => LaunchInstanceResult.UnknownError
};
} finally { } finally {
stateTransitioningActionSemaphore.Release(); stateTransitioningActionSemaphore.Release();
} }
} }
public async Task<StopInstanceResult> Stop(MinecraftStopStrategy stopStrategy) { public async Task<StopInstanceResult> Stop() {
await stateTransitioningActionSemaphore.WaitAsync(); await stateTransitioningActionSemaphore.WaitAsync();
try { try {
return TransitionStateAndReturn(currentState.Stop(stopStrategy)); if (TransitionState(currentState.Stop())) {
} catch (Exception e) { return StopInstanceResult.StopInitiated;
logger.Error(e, "Caught exception while stopping instance."); }
return StopInstanceResult.UnknownError;
return currentState switch {
InstanceNotRunningState => StopInstanceResult.InstanceAlreadyStopped,
InstanceLaunchingState => StopInstanceResult.StopInitiated,
InstanceStoppingState => StopInstanceResult.InstanceAlreadyStopping,
_ => StopInstanceResult.UnknownError
};
} finally { } finally {
stateTransitioningActionSemaphore.Release(); stateTransitioningActionSemaphore.Release();
} }
} }
public async Task StopAndWait(TimeSpan waitTime) { public async Task StopAndWait(TimeSpan waitTime) {
await Stop(MinecraftStopStrategy.Instant); await Stop();
using var waitTokenSource = new CancellationTokenSource(waitTime); using var waitTokenSource = new CancellationTokenSource(waitTime);
var waitToken = waitTokenSource.Token; var waitToken = waitTokenSource.Token;
@@ -126,13 +130,10 @@ sealed class Instance : IDisposable {
private sealed class InstanceContextImpl : InstanceContext { private sealed class InstanceContextImpl : InstanceContext {
private readonly Instance instance; private readonly Instance instance;
private readonly CancellationToken shutdownCancellationToken;
private int statusUpdateCounter; private int statusUpdateCounter;
public InstanceContextImpl(Instance instance, CancellationToken shutdownCancellationToken) : base(instance.Configuration, instance.Launcher) { public InstanceContextImpl(Instance instance) : base(instance.Configuration, instance.Launcher) {
this.instance = instance; this.instance = instance;
this.shutdownCancellationToken = shutdownCancellationToken;
} }
public override LaunchServices LaunchServices => instance.launchServices; public override LaunchServices LaunchServices => instance.launchServices;
@@ -140,33 +141,21 @@ sealed class Instance : IDisposable {
public override ILogger Logger => instance.logger; public override ILogger Logger => instance.logger;
public override string ShortName => instance.shortName; public override string ShortName => instance.shortName;
public override void ReportStatus(IInstanceStatus newStatus) { public override void ReportStatus(InstanceStatus newStatus) {
int myStatusUpdateCounter = Interlocked.Increment(ref statusUpdateCounter); int myStatusUpdateCounter = Interlocked.Increment(ref statusUpdateCounter);
instance.launchServices.TaskManager.Run("Report status of instance " + instance.shortName + " as " + newStatus.GetType().Name, async () => { Task.Run(async () => {
if (myStatusUpdateCounter == statusUpdateCounter) { if (myStatusUpdateCounter == statusUpdateCounter) {
instance.currentStatus = newStatus; instance.currentStatus = newStatus;
await ServerMessaging.Send(new ReportInstanceStatusMessage(Configuration.InstanceGuid, newStatus)); await ServerMessaging.SendMessage(new ReportInstanceStatusMessage(Configuration.InstanceGuid, newStatus));
} }
}); });
} }
public override void TransitionState(Func<(IInstanceState, IInstanceStatus?)> newStateAndStatus) { public override void TransitionState(Func<IInstanceState> newState) {
instance.stateTransitioningActionSemaphore.Wait(CancellationToken.None); instance.stateTransitioningActionSemaphore.Wait();
try { try {
var (state, status) = newStateAndStatus(); instance.TransitionState(newState());
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 { } finally {
instance.stateTransitioningActionSemaphore.Release(); instance.stateTransitioningActionSemaphore.Release();
} }
@@ -174,10 +163,10 @@ sealed class Instance : IDisposable {
} }
public void Dispose() { public void Dispose() {
stateTransitioningActionSemaphore.Dispose();
if (currentState is IDisposable disposable) { if (currentState is IDisposable disposable) {
disposable.Dispose(); disposable.Dispose();
} }
stateTransitioningActionSemaphore.Dispose();
} }
} }

View File

@@ -19,10 +19,10 @@ abstract class InstanceContext {
Launcher = launcher; Launcher = launcher;
} }
public abstract void ReportStatus(IInstanceStatus newStatus); public abstract void ReportStatus(InstanceStatus newStatus);
public abstract void TransitionState(Func<(IInstanceState, IInstanceStatus?)> newStateAndStatus); public abstract void TransitionState(Func<IInstanceState> newState);
public void TransitionState(IInstanceState newState, IInstanceStatus? newStatus = null) { public void TransitionState(IInstanceState newState) {
TransitionState(() => (newState, newStatus)); TransitionState(() => newState);
} }
} }

View File

@@ -1,96 +0,0 @@
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.
}
}
}

View File

@@ -0,0 +1,84 @@
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();
}
}

View File

@@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis; using Phantom.Agent.Minecraft.Instance;
using Phantom.Agent.Minecraft.Instance;
using Phantom.Agent.Minecraft.Java; using Phantom.Agent.Minecraft.Java;
using Phantom.Agent.Minecraft.Launcher; using Phantom.Agent.Minecraft.Launcher;
using Phantom.Agent.Minecraft.Launcher.Types; using Phantom.Agent.Minecraft.Launcher.Types;
@@ -8,11 +7,8 @@ using Phantom.Agent.Minecraft.Server;
using Phantom.Common.Data; using Phantom.Common.Data;
using Phantom.Common.Data.Agent; using Phantom.Common.Data.Agent;
using Phantom.Common.Data.Instance; using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies; using Phantom.Common.Data.Replies;
using Phantom.Common.Logging; using Phantom.Common.Logging;
using Phantom.Utils.IO;
using Phantom.Utils.Runtime;
using Serilog; using Serilog;
namespace Phantom.Agent.Services.Instances; namespace Phantom.Agent.Services.Instances;
@@ -23,7 +19,6 @@ sealed class InstanceSessionManager : IDisposable {
private readonly AgentInfo agentInfo; private readonly AgentInfo agentInfo;
private readonly string basePath; private readonly string basePath;
private readonly MinecraftServerExecutables minecraftServerExecutables;
private readonly LaunchServices launchServices; private readonly LaunchServices launchServices;
private readonly PortManager portManager; private readonly PortManager portManager;
private readonly Dictionary<Guid, Instance> instances = new (); private readonly Dictionary<Guid, Instance> instances = new ();
@@ -32,53 +27,32 @@ sealed class InstanceSessionManager : IDisposable {
private readonly CancellationToken shutdownCancellationToken; private readonly CancellationToken shutdownCancellationToken;
private readonly SemaphoreSlim semaphore = new (1, 1); private readonly SemaphoreSlim semaphore = new (1, 1);
public InstanceSessionManager(AgentInfo agentInfo, AgentFolders agentFolders, JavaRuntimeRepository javaRuntimeRepository, TaskManager taskManager) { public InstanceSessionManager(AgentInfo agentInfo, AgentFolders agentFolders, JavaRuntimeRepository javaRuntimeRepository) {
this.agentInfo = agentInfo; this.agentInfo = agentInfo;
this.basePath = agentFolders.InstancesFolderPath; this.basePath = agentFolders.InstancesFolderPath;
this.minecraftServerExecutables = new MinecraftServerExecutables(agentFolders.ServerExecutableFolderPath); this.launchServices = new LaunchServices(new MinecraftServerExecutables(agentFolders.ServerExecutableFolderPath), javaRuntimeRepository);
this.launchServices = new LaunchServices(taskManager, minecraftServerExecutables, javaRuntimeRepository);
this.portManager = new PortManager(agentInfo.AllowedServerPorts, agentInfo.AllowedRconPorts); this.portManager = new PortManager(agentInfo.AllowedServerPorts, agentInfo.AllowedRconPorts);
this.shutdownCancellationToken = shutdownCancellationTokenSource.Token; this.shutdownCancellationToken = shutdownCancellationTokenSource.Token;
} }
private async Task<InstanceActionResult<T>> AcquireSemaphoreAndRun<T>(Func<Task<InstanceActionResult<T>>> func) { public async Task<ConfigureInstanceResult> Configure(InstanceConfiguration configuration) {
try { try {
await semaphore.WaitAsync(shutdownCancellationToken); await semaphore.WaitAsync(shutdownCancellationToken);
try {
return await func();
} finally {
semaphore.Release();
}
} catch (OperationCanceledException) { } catch (OperationCanceledException) {
return InstanceActionResult.General<T>(InstanceActionGeneralResult.AgentShuttingDown); return ConfigureInstanceResult.AgentShuttingDown;
} }
}
[SuppressMessage("ReSharper", "ConvertIfStatementToReturnStatement")] var instanceGuid = configuration.InstanceGuid;
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);
}
});
}
public async Task<InstanceActionResult<ConfigureInstanceResult>> Configure(InstanceConfiguration configuration) { try {
return await AcquireSemaphoreAndRun(async () => {
var instanceGuid = configuration.InstanceGuid;
var otherInstances = instances.Values.Where(inst => inst.Configuration.InstanceGuid != instanceGuid).ToArray(); var otherInstances = instances.Values.Where(inst => inst.Configuration.InstanceGuid != instanceGuid).ToArray();
if (otherInstances.Length + 1 > agentInfo.MaxInstances) { if (otherInstances.Length + 1 > agentInfo.MaxInstances) {
return InstanceActionResult.Concrete(ConfigureInstanceResult.InstanceLimitExceeded); return ConfigureInstanceResult.InstanceLimitExceeded;
} }
var availableMemory = agentInfo.MaxMemory - otherInstances.Aggregate(RamAllocationUnits.Zero, static (total, instance) => total + instance.Configuration.MemoryAllocation); var availableMemory = agentInfo.MaxMemory - otherInstances.Aggregate(RamAllocationUnits.Zero, static (total, instance) => total + instance.Configuration.MemoryAllocation);
if (availableMemory < configuration.MemoryAllocation) { if (availableMemory < configuration.MemoryAllocation) {
return InstanceActionResult.Concrete(ConfigureInstanceResult.MemoryLimitExceeded); return ConfigureInstanceResult.MemoryLimitExceeded;
} }
var heapMegabytes = configuration.MemoryAllocation.InMegabytes; var heapMegabytes = configuration.MemoryAllocation.InMegabytes;
@@ -88,12 +62,11 @@ sealed class InstanceSessionManager : IDisposable {
); );
var instanceFolder = Path.Combine(basePath, instanceGuid.ToString()); var instanceFolder = Path.Combine(basePath, instanceGuid.ToString());
Directories.Create(instanceFolder, Chmod.URWX_GRX); Directory.CreateDirectory(instanceFolder);
var properties = new InstanceProperties( var properties = new InstanceProperties(
configuration.JavaRuntimeGuid, configuration.JavaRuntimeGuid,
jvmProperties, jvmProperties,
configuration.JvmArguments,
instanceFolder, instanceFolder,
configuration.MinecraftVersion, configuration.MinecraftVersion,
new ServerProperties(configuration.ServerPort, configuration.RconPort) new ServerProperties(configuration.ServerPort, configuration.RconPort)
@@ -114,48 +87,86 @@ sealed class InstanceSessionManager : IDisposable {
await instance.Launch(shutdownCancellationToken); await instance.Launch(shutdownCancellationToken);
} }
return InstanceActionResult.Concrete(ConfigureInstanceResult.Success); return ConfigureInstanceResult.Success;
}); } finally {
semaphore.Release();
}
} }
public Task<InstanceActionResult<LaunchInstanceResult>> Launch(Guid instanceGuid) { public async Task<LaunchInstanceResult> Launch(Guid instanceGuid) {
return AcquireSemaphoreAndRunWithInstance(instanceGuid, instance => instance.Launch(shutdownCancellationToken)); 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<StopInstanceResult>> Stop(Guid instanceGuid, MinecraftStopStrategy stopStrategy) { public async Task<StopInstanceResult> Stop(Guid instanceGuid) {
return AcquireSemaphoreAndRunWithInstance(instanceGuid, instance => instance.Stop(stopStrategy)); 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<SendCommandToInstanceResult>> SendCommand(Guid instanceGuid, string command) { public async Task<SendCommandToInstanceResult> SendCommand(Guid instanceGuid, string command) {
return AcquireSemaphoreAndRunWithInstance(instanceGuid, async instance => await instance.SendCommand(command, shutdownCancellationToken) ? SendCommandToInstanceResult.Success : SendCommandToInstanceResult.UnknownError); 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 async Task StopAll() { public async Task StopAll() {
shutdownCancellationTokenSource.Cancel(); shutdownCancellationTokenSource.Cancel();
Logger.Information("Stopping all instances...");
await semaphore.WaitAsync(CancellationToken.None); await semaphore.WaitAsync(CancellationToken.None);
try { try {
await Task.WhenAll(instances.Values.Select(static instance => instance.StopAndWait(TimeSpan.FromSeconds(30)))); await Task.WhenAll(instances.Values.Select(static instance => instance.StopAndWait(TimeSpan.FromSeconds(30))));
DisposeAllInstances(); instances.Clear();
} finally { } finally {
semaphore.Release(); semaphore.Release();
} }
} }
public void Dispose() { public void Dispose() {
DisposeAllInstances();
minecraftServerExecutables.Dispose();
shutdownCancellationTokenSource.Dispose(); shutdownCancellationTokenSource.Dispose();
semaphore.Dispose(); semaphore.Dispose();
} }
private void DisposeAllInstances() {
foreach (var (_, instance) in instances) {
instance.Dispose();
}
instances.Clear();
}
} }

View File

@@ -14,28 +14,17 @@ sealed class PortManager {
} }
public Result Reserve(InstanceConfiguration configuration) { 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) { lock (usedPorts) {
if (usedPorts.Contains(serverPort)) { if (usedPorts.Contains(configuration.ServerPort)) {
return Result.ServerPortAlreadyInUse; return Result.ServerPortAlreadyInUse;
} }
if (usedPorts.Contains(rconPort)) { if (usedPorts.Contains(configuration.RconPort)) {
return Result.RconPortAlreadyInUse; return Result.RconPortAlreadyInUse;
} }
usedPorts.Add(serverPort); usedPorts.Add(configuration.ServerPort);
usedPorts.Add(rconPort); usedPorts.Add(configuration.RconPort);
} }
return Result.Success; return Result.Success;
@@ -53,6 +42,6 @@ sealed class PortManager {
ServerPortNotAllowed, ServerPortNotAllowed,
ServerPortAlreadyInUse, ServerPortAlreadyInUse,
RconPortNotAllowed, RconPortNotAllowed,
RconPortAlreadyInUse RconPortAlreadyInUse,
} }
} }

View File

@@ -1,11 +1,7 @@
using Phantom.Common.Data.Minecraft; namespace Phantom.Agent.Services.Instances.States;
using Phantom.Common.Data.Replies;
namespace Phantom.Agent.Services.Instances.States;
interface IInstanceState { interface IInstanceState {
void Initialize(); IInstanceState Launch(InstanceContext context);
(IInstanceState, LaunchInstanceResult) Launch(InstanceContext context); IInstanceState Stop();
(IInstanceState, StopInstanceResult) Stop(MinecraftStopStrategy stopStrategy);
Task<bool> SendCommand(string command, CancellationToken cancellationToken); Task<bool> SendCommand(string command, CancellationToken cancellationToken);
} }

View File

@@ -2,8 +2,6 @@
using Phantom.Agent.Minecraft.Launcher; using Phantom.Agent.Minecraft.Launcher;
using Phantom.Agent.Minecraft.Server; using Phantom.Agent.Minecraft.Server;
using Phantom.Common.Data.Instance; using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies;
namespace Phantom.Agent.Services.Instances.States; namespace Phantom.Agent.Services.Instances.States;
@@ -14,12 +12,10 @@ sealed class InstanceLaunchingState : IInstanceState, IDisposable {
public InstanceLaunchingState(InstanceContext context) { public InstanceLaunchingState(InstanceContext context) {
this.context = context; this.context = context;
} this.context.Logger.Information("Session starting...");
this.context.ReportStatus(InstanceStatus.IsLaunching);
public void Initialize() {
context.Logger.Information("Session starting..."); var launchTask = Task.Run(DoLaunch);
var launchTask = context.LaunchServices.TaskManager.Run("Launch procedure for instance " + context.ShortName, DoLaunch);
launchTask.ContinueWith(OnLaunchSuccess, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default); launchTask.ContinueWith(OnLaunchSuccess, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default);
launchTask.ContinueWith(OnLaunchFailure, CancellationToken.None, TaskContinuationOptions.NotOnRanToCompletion, TaskScheduler.Default); launchTask.ContinueWith(OnLaunchFailure, CancellationToken.None, TaskContinuationOptions.NotOnRanToCompletion, TaskScheduler.Default);
} }
@@ -27,38 +23,29 @@ sealed class InstanceLaunchingState : IInstanceState, IDisposable {
private async Task<InstanceSession> DoLaunch() { private async Task<InstanceSession> DoLaunch() {
var cancellationToken = cancellationTokenSource.Token; var cancellationToken = cancellationTokenSource.Token;
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
void OnDownloadProgress(object? sender, DownloadProgressEventArgs args) { void OnDownloadProgress(object? sender, DownloadProgressEventArgs args) {
byte progress = (byte) Math.Min(args.DownloadedBytes * 100 / args.TotalBytes, 100); byte progress = (byte) Math.Min(args.DownloadedBytes * 100 / args.TotalBytes, 100);
if (lastDownloadProgress != progress) { if (lastDownloadProgress != progress) {
lastDownloadProgress = progress; lastDownloadProgress = progress;
context.ReportStatus(InstanceStatus.Downloading(progress)); context.ReportStatus(new InstanceStatus.Downloading(progress));
} }
} }
var launchResult = await context.Launcher.Launch(context.Logger, context.LaunchServices, OnDownloadProgress, cancellationToken); var launchResult = await context.Launcher.Launch(context.LaunchServices, OnDownloadProgress, cancellationToken);
if (launchResult is LaunchResult.InvalidJavaRuntime) { if (launchResult is LaunchResult.CouldNotDownloadMinecraftServer) {
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."); throw new LaunchFailureException(InstanceLaunchFailReason.CouldNotDownloadMinecraftServer, "Session failed to launch, could not download Minecraft server.");
} }
else if (launchResult is LaunchResult.CouldNotConfigureMinecraftServer) { else if (launchResult is LaunchResult.InvalidJavaRuntime) {
throw new LaunchFailureException(InstanceLaunchFailReason.CouldNotConfigureMinecraftServer, "Session failed to launch, could not configure Minecraft server."); throw new LaunchFailureException(InstanceLaunchFailReason.JavaRuntimeNotFound, "Session failed to launch, invalid Java runtime.");
}
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) { if (launchResult is not LaunchResult.Success launchSuccess) {
throw new LaunchFailureException(InstanceLaunchFailReason.UnknownError, "Session failed to launch."); throw new LaunchFailureException(InstanceLaunchFailReason.UnknownError, "Session failed to launch.");
} }
context.ReportStatus(InstanceStatus.Launching); context.ReportStatus(InstanceStatus.IsLaunching);
return launchSuccess.Session; return launchSuccess.Session;
} }
@@ -66,10 +53,11 @@ sealed class InstanceLaunchingState : IInstanceState, IDisposable {
context.TransitionState(() => { context.TransitionState(() => {
if (cancellationTokenSource.IsCancellationRequested) { if (cancellationTokenSource.IsCancellationRequested) {
context.PortManager.Release(context.Configuration); context.PortManager.Release(context.Configuration);
return (new InstanceNotRunningState(), InstanceStatus.NotRunning); context.ReportStatus(InstanceStatus.IsNotRunning);
return new InstanceNotRunningState();
} }
else { else {
return (new InstanceRunningState(context, task.Result), null); return new InstanceRunningState(context, task.Result);
} }
}); });
} }
@@ -77,12 +65,12 @@ sealed class InstanceLaunchingState : IInstanceState, IDisposable {
private void OnLaunchFailure(Task task) { private void OnLaunchFailure(Task task) {
if (task.Exception is { InnerException: LaunchFailureException e }) { if (task.Exception is { InnerException: LaunchFailureException e }) {
context.Logger.Error(e.LogMessage); context.Logger.Error(e.LogMessage);
context.ReportStatus(InstanceStatus.Failed(e.Reason)); context.ReportStatus(new InstanceStatus.Failed(e.Reason));
} }
else { else {
context.ReportStatus(InstanceStatus.Failed(InstanceLaunchFailReason.UnknownError)); context.ReportStatus(new InstanceStatus.Failed(InstanceLaunchFailReason.UnknownError));
} }
context.PortManager.Release(context.Configuration); context.PortManager.Release(context.Configuration);
context.TransitionState(new InstanceNotRunningState()); context.TransitionState(new InstanceNotRunningState());
} }
@@ -90,20 +78,20 @@ sealed class InstanceLaunchingState : IInstanceState, IDisposable {
private sealed class LaunchFailureException : Exception { private sealed class LaunchFailureException : Exception {
public InstanceLaunchFailReason Reason { get; } public InstanceLaunchFailReason Reason { get; }
public string LogMessage { get; } public string LogMessage { get; }
public LaunchFailureException(InstanceLaunchFailReason reason, string logMessage) { public LaunchFailureException(InstanceLaunchFailReason reason, string logMessage) {
this.Reason = reason; this.Reason = reason;
this.LogMessage = logMessage; this.LogMessage = logMessage;
} }
} }
public (IInstanceState, LaunchInstanceResult) Launch(InstanceContext context) { public IInstanceState Launch(InstanceContext context) {
return (this, LaunchInstanceResult.InstanceAlreadyLaunching); return this;
} }
public (IInstanceState, StopInstanceResult) Stop(MinecraftStopStrategy stopStrategy) { public IInstanceState Stop() {
cancellationTokenSource.Cancel(); cancellationTokenSource.Cancel();
return (this, StopInstanceResult.StopInitiated); return this;
} }
public Task<bool> SendCommand(string command, CancellationToken cancellationToken) { public Task<bool> SendCommand(string command, CancellationToken cancellationToken) {

View File

@@ -1,13 +1,9 @@
using Phantom.Common.Data.Instance; using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies;
namespace Phantom.Agent.Services.Instances.States; namespace Phantom.Agent.Services.Instances.States;
sealed class InstanceNotRunningState : IInstanceState { sealed class InstanceNotRunningState : IInstanceState {
public void Initialize() {} public IInstanceState Launch(InstanceContext context) {
public (IInstanceState, LaunchInstanceResult) Launch(InstanceContext context) {
InstanceLaunchFailReason? failReason = context.PortManager.Reserve(context.Configuration) switch { InstanceLaunchFailReason? failReason = context.PortManager.Reserve(context.Configuration) switch {
PortManager.Result.ServerPortNotAllowed => InstanceLaunchFailReason.ServerPortNotAllowed, PortManager.Result.ServerPortNotAllowed => InstanceLaunchFailReason.ServerPortNotAllowed,
PortManager.Result.ServerPortAlreadyInUse => InstanceLaunchFailReason.ServerPortAlreadyInUse, PortManager.Result.ServerPortAlreadyInUse => InstanceLaunchFailReason.ServerPortAlreadyInUse,
@@ -17,16 +13,15 @@ sealed class InstanceNotRunningState : IInstanceState {
}; };
if (failReason != null) { if (failReason != null) {
context.ReportStatus(InstanceStatus.Failed(failReason.Value)); context.ReportStatus(new InstanceStatus.Failed(failReason.Value));
return (this, LaunchInstanceResult.LaunchInitiated); return this;
} }
context.ReportStatus(InstanceStatus.Launching); return new InstanceLaunchingState(context);
return (new InstanceLaunchingState(context), LaunchInstanceResult.LaunchInitiated);
} }
public (IInstanceState, StopInstanceResult) Stop(MinecraftStopStrategy stopStrategy) { public IInstanceState Stop() {
return (this, StopInstanceResult.InstanceAlreadyStopped); return this;
} }
public Task<bool> SendCommand(string command, CancellationToken cancellationToken) { public Task<bool> SendCommand(string command, CancellationToken cancellationToken) {

View File

@@ -1,127 +1,58 @@
using Phantom.Agent.Minecraft.Command; using Phantom.Agent.Minecraft.Instance;
using Phantom.Agent.Minecraft.Instance;
using Phantom.Common.Data.Instance; using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies;
namespace Phantom.Agent.Services.Instances.States; namespace Phantom.Agent.Services.Instances.States;
sealed class InstanceRunningState : IInstanceState { sealed class InstanceRunningState : IInstanceState {
private readonly InstanceContext context; private readonly InstanceContext context;
private readonly InstanceSession session; private readonly InstanceSession session;
private readonly InstanceLogSender logSender; private readonly InstanceLogSenderThread logSenderThread;
private readonly SessionObjects sessionObjects; private readonly SessionObjects sessionObjects;
private readonly CancellationTokenSource delayedStopCancellationTokenSource = new ();
private bool stateOwnsDelayedStopCancellationTokenSource = true;
private bool isStopping;
public InstanceRunningState(InstanceContext context, InstanceSession session) { public InstanceRunningState(InstanceContext context, InstanceSession session) {
this.context = context; this.context = context;
this.session = session; this.session = session;
this.logSender = new InstanceLogSender(context.LaunchServices.TaskManager, context.Configuration.InstanceGuid, context.ShortName); this.logSenderThread = new InstanceLogSenderThread(context.Configuration.InstanceGuid, context.ShortName);
this.sessionObjects = new SessionObjects(this); this.sessionObjects = new SessionObjects(context, session, logSenderThread);
}
this.session.AddOutputListener(SessionOutput);
this.session.SessionEnded += SessionEnded;
public void Initialize() {
session.AddOutputListener(SessionOutput);
session.SessionEnded += SessionEnded;
if (session.HasEnded) { if (session.HasEnded) {
if (sessionObjects.Dispose()) { if (sessionObjects.Dispose()) {
context.Logger.Warning("Session ended immediately after it was started."); context.Logger.Warning("Session ended immediately after it was started.");
context.LaunchServices.TaskManager.Run("Transition state of instance " + context.ShortName + " to not running", () => context.TransitionState(new InstanceNotRunningState(), InstanceStatus.Failed(InstanceLaunchFailReason.UnknownError))); context.ReportStatus(new InstanceStatus.Failed(InstanceLaunchFailReason.UnknownError));
Task.Run(() => context.TransitionState(new InstanceNotRunningState()));
} }
} }
else { else {
context.ReportStatus(InstanceStatus.Running); context.ReportStatus(InstanceStatus.IsRunning);
context.Logger.Information("Session started."); context.Logger.Information("Session started.");
} }
} }
private void SessionOutput(object? sender, string e) { private void SessionOutput(object? sender, string e) {
context.Logger.Verbose("[Server] {Line}", e); context.Logger.Verbose("[Server] {Line}", e);
logSender.Enqueue(e); logSenderThread.Enqueue(e);
} }
private void SessionEnded(object? sender, EventArgs e) { private void SessionEnded(object? sender, EventArgs e) {
if (!sessionObjects.Dispose()) { if (sessionObjects.Dispose()) {
return;
}
if (isStopping) {
context.Logger.Information("Session ended."); context.Logger.Information("Session ended.");
context.TransitionState(new InstanceNotRunningState(), InstanceStatus.NotRunning); context.ReportStatus(InstanceStatus.IsNotRunning);
} context.TransitionState(new InstanceNotRunningState());
else {
context.Logger.Information("Session ended unexpectedly, restarting...");
context.TransitionState(new InstanceLaunchingState(context), InstanceStatus.Restarting);
} }
} }
public (IInstanceState, LaunchInstanceResult) Launch(InstanceContext context) { public IInstanceState Launch(InstanceContext context) {
return (this, LaunchInstanceResult.InstanceAlreadyRunning); return this;
} }
public (IInstanceState, StopInstanceResult) Stop(MinecraftStopStrategy stopStrategy) { public IInstanceState Stop() {
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; session.SessionEnded -= SessionEnded;
return new InstanceStoppingState(context, session, sessionObjects); 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) { public async Task<bool> SendCommand(string command, CancellationToken cancellationToken) {
try { try {
context.Logger.Information("Sending command: {Command}", command); context.Logger.Information("Sending command: {Command}", command);
@@ -136,11 +67,15 @@ sealed class InstanceRunningState : IInstanceState {
} }
public sealed class SessionObjects { public sealed class SessionObjects {
private readonly InstanceRunningState state; private readonly InstanceContext context;
private readonly InstanceSession session;
private readonly InstanceLogSenderThread logSenderThread;
private bool isDisposed; private bool isDisposed;
public SessionObjects(InstanceRunningState state) { public SessionObjects(InstanceContext context, InstanceSession session, InstanceLogSenderThread logSenderThread) {
this.state = state; this.context = context;
this.session = session;
this.logSenderThread = logSenderThread;
} }
public bool Dispose() { public bool Dispose() {
@@ -152,16 +87,9 @@ sealed class InstanceRunningState : IInstanceState {
isDisposed = true; isDisposed = true;
} }
if (state.stateOwnsDelayedStopCancellationTokenSource) { logSenderThread.Cancel();
state.delayedStopCancellationTokenSource.Dispose(); session.Dispose();
} context.PortManager.Release(context.Configuration);
else {
state.CancelDelayedStop();
}
state.logSender.Cancel();
state.session.Dispose();
state.context.PortManager.Release(state.context.Configuration);
return true; return true;
} }
} }

View File

@@ -1,9 +1,5 @@
using System.Diagnostics; using Phantom.Agent.Minecraft.Instance;
using Phantom.Agent.Minecraft.Command;
using Phantom.Agent.Minecraft.Instance;
using Phantom.Common.Data.Instance; using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies;
namespace Phantom.Agent.Services.Instances.States; namespace Phantom.Agent.Services.Instances.States;
@@ -16,12 +12,10 @@ sealed class InstanceStoppingState : IInstanceState, IDisposable {
this.sessionObjects = sessionObjects; this.sessionObjects = sessionObjects;
this.session = session; this.session = session;
this.context = context; this.context = context;
} this.context.Logger.Information("Session stopping.");
this.context.ReportStatus(InstanceStatus.IsStopping);
public void Initialize() {
context.Logger.Information("Session stopping."); Task.Run(DoStop);
context.ReportStatus(InstanceStatus.Stopping);
context.LaunchServices.TaskManager.Run("Stop procedure for instance " + context.ShortName, DoStop);
} }
private async Task DoStop() { private async Task DoStop() {
@@ -33,27 +27,26 @@ sealed class InstanceStoppingState : IInstanceState, IDisposable {
await DoWaitForSessionToEnd(); await DoWaitForSessionToEnd();
} finally { } finally {
context.Logger.Information("Session stopped."); context.Logger.Information("Session stopped.");
context.TransitionState(new InstanceNotRunningState(), InstanceStatus.NotRunning); context.ReportStatus(InstanceStatus.IsNotRunning);
context.TransitionState(new InstanceNotRunningState());
} }
} }
private async Task DoSendStopCommand() { private async Task DoSendStopCommand() {
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try { try {
await session.SendCommand(MinecraftCommand.Stop, timeout.Token); await session.SendCommand("stop", cts.Token);
} catch (OperationCanceledException) { } catch (OperationCanceledException) {
// ignore // ignore
} catch (ObjectDisposedException e) when (e.ObjectName == typeof(Process).FullName && session.HasEnded) {
// ignore
} catch (Exception e) { } catch (Exception e) {
context.Logger.Warning(e, "Caught exception while sending stop command."); context.Logger.Warning(e, "Caught exception while sending stop command.");
} }
} }
private async Task DoWaitForSessionToEnd() { private async Task DoWaitForSessionToEnd() {
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(55)); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(55));
try { try {
await session.WaitForExit(timeout.Token); await session.WaitForExit(cts.Token);
} catch (OperationCanceledException) { } catch (OperationCanceledException) {
try { try {
context.Logger.Warning("Waiting timed out, killing session..."); context.Logger.Warning("Waiting timed out, killing session...");
@@ -64,12 +57,12 @@ sealed class InstanceStoppingState : IInstanceState, IDisposable {
} }
} }
public (IInstanceState, LaunchInstanceResult) Launch(InstanceContext context) { public IInstanceState Launch(InstanceContext context) {
return (this, LaunchInstanceResult.InstanceIsStopping); return this;
} }
public (IInstanceState, StopInstanceResult) Stop(MinecraftStopStrategy stopStrategy) { public IInstanceState Stop() {
return (this, StopInstanceResult.InstanceAlreadyStopping); // TODO maybe provide a way to kill? return this; // TODO maybe provide a way to kill?
} }
public Task<bool> SendCommand(string command, CancellationToken cancellationToken) { public Task<bool> SendCommand(string command, CancellationToken cancellationToken) {

View File

@@ -1,11 +1,9 @@
using Phantom.Agent.Rpc; using NetMQ.Sockets;
using Phantom.Agent.Rpc;
using Phantom.Common.Data.Replies; using Phantom.Common.Data.Replies;
using Phantom.Common.Logging;
using Phantom.Common.Messages; using Phantom.Common.Messages;
using Phantom.Common.Messages.BiDirectional;
using Phantom.Common.Messages.ToAgent; using Phantom.Common.Messages.ToAgent;
using Phantom.Common.Messages.ToServer; using Phantom.Common.Messages.ToServer;
using Phantom.Utils.Rpc.Message;
using Serilog; using Serilog;
namespace Phantom.Agent.Services.Rpc; namespace Phantom.Agent.Services.Rpc;
@@ -13,34 +11,32 @@ namespace Phantom.Agent.Services.Rpc;
public sealed class MessageListener : IMessageToAgentListener { public sealed class MessageListener : IMessageToAgentListener {
private static ILogger Logger { get; } = PhantomLogger.Create<MessageListener>(); private static ILogger Logger { get; } = PhantomLogger.Create<MessageListener>();
private readonly RpcServerConnection connection; private readonly ClientSocket socket;
private readonly AgentServices agent; private readonly AgentServices agent;
private readonly CancellationTokenSource shutdownTokenSource; private readonly CancellationTokenSource shutdownTokenSource;
public MessageListener(RpcServerConnection connection, AgentServices agent, CancellationTokenSource shutdownTokenSource) { public MessageListener(ClientSocket socket, AgentServices agent, CancellationTokenSource shutdownTokenSource) {
this.connection = connection; this.socket = socket;
this.agent = agent; this.agent = agent;
this.shutdownTokenSource = shutdownTokenSource; this.shutdownTokenSource = shutdownTokenSource;
} }
public async Task<NoReply> HandleRegisterAgentSuccess(RegisterAgentSuccessMessage message) { public async Task HandleRegisterAgentSuccessResult(RegisterAgentSuccessMessage message) {
Logger.Information("Agent authentication successful."); Logger.Information("Agent authentication successful.");
foreach (var instanceInfo in message.InitialInstances) { foreach (var instanceInfo in message.InitialInstances) {
var result = await agent.InstanceSessionManager.Configure(instanceInfo); if (await agent.InstanceSessionManager.Configure(instanceInfo) != ConfigureInstanceResult.Success) {
if (!result.Is(ConfigureInstanceResult.Success)) {
Logger.Fatal("Unable to configure instance \"{Name}\" (GUID {Guid}), shutting down.", instanceInfo.InstanceName, instanceInfo.InstanceGuid); Logger.Fatal("Unable to configure instance \"{Name}\" (GUID {Guid}), shutting down.", instanceInfo.InstanceName, instanceInfo.InstanceGuid);
shutdownTokenSource.Cancel(); shutdownTokenSource.Cancel();
return NoReply.Instance; return;
} }
} }
await ServerMessaging.Send(new AdvertiseJavaRuntimesMessage(agent.JavaRuntimeRepository.All)); await ServerMessaging.SendMessage(new AdvertiseJavaRuntimesMessage(agent.JavaRuntimeRepository.All));
return NoReply.Instance;
} }
public Task<NoReply> HandleRegisterAgentFailure(RegisterAgentFailureMessage message) { public Task HandleRegisterAgentFailureResult(RegisterAgentFailureMessage message) {
string errorMessage = message.FailureKind switch { string errorMessage = message.FailureKind switch {
RegisterAgentFailure.ConnectionAlreadyHasAnAgent => "This connection already has an associated agent.", RegisterAgentFailure.ConnectionAlreadyHasAnAgent => "This connection already has an associated agent.",
RegisterAgentFailure.InvalidToken => "Invalid token.", RegisterAgentFailure.InvalidToken => "Invalid token.",
@@ -50,27 +46,27 @@ public sealed class MessageListener : IMessageToAgentListener {
Logger.Fatal("Agent authentication failed: {Error}", errorMessage); Logger.Fatal("Agent authentication failed: {Error}", errorMessage);
Environment.Exit(1); Environment.Exit(1);
return Task.FromResult(NoReply.Instance); return Task.CompletedTask;
} }
public async Task<InstanceActionResult<ConfigureInstanceResult>> HandleConfigureInstance(ConfigureInstanceMessage message) { public Task HandleShutdownAgent(ShutdownAgentMessage message) {
return await agent.InstanceSessionManager.Configure(message.Configuration); shutdownTokenSource.Cancel();
return Task.CompletedTask;
} }
public async Task<InstanceActionResult<LaunchInstanceResult>> HandleLaunchInstance(LaunchInstanceMessage message) { public async Task HandleConfigureInstance(ConfigureInstanceMessage message) {
return await agent.InstanceSessionManager.Launch(message.InstanceGuid); await socket.SendSimpleReply(message, await agent.InstanceSessionManager.Configure(message.Configuration));
} }
public async Task<InstanceActionResult<StopInstanceResult>> HandleStopInstance(StopInstanceMessage message) { public async Task HandleLaunchInstance(LaunchInstanceMessage message) {
return await agent.InstanceSessionManager.Stop(message.InstanceGuid, message.StopStrategy); await socket.SendSimpleReply(message, await agent.InstanceSessionManager.Launch(message.InstanceGuid));
} }
public async Task<InstanceActionResult<SendCommandToInstanceResult>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) { public async Task HandleStopInstance(StopInstanceMessage message) {
return await agent.InstanceSessionManager.SendCommand(message.InstanceGuid, message.Command); await socket.SendSimpleReply(message, await agent.InstanceSessionManager.Stop(message.InstanceGuid));
} }
public Task<NoReply> HandleReply(ReplyMessage message) { public async Task HandleSendCommandToInstance(SendCommandToInstanceMessage message) {
connection.Receive(message); await socket.SendSimpleReply(message, await agent.InstanceSessionManager.SendCommand(message.InstanceGuid, message.Command));
return Task.FromResult(NoReply.Instance);
} }
} }

View File

@@ -1,60 +0,0 @@
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);
}
}

View File

@@ -0,0 +1,32 @@
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);
}
}

View File

@@ -6,7 +6,7 @@ using Serilog;
namespace Phantom.Agent; namespace Phantom.Agent;
static class GuidFile { static class GuidFile {
private static ILogger Logger { get; } = PhantomLogger.Create(nameof(GuidFile)); private static ILogger Logger { get; } = PhantomLogger.Create(typeof(GuidFile));
private const string GuidFileName = "agent.guid"; private const string GuidFileName = "agent.guid";

View File

@@ -8,7 +8,6 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,5 +1,4 @@
using System.Reflection; using Phantom.Agent;
using Phantom.Agent;
using Phantom.Agent.Rpc; using Phantom.Agent.Rpc;
using Phantom.Agent.Services; using Phantom.Agent.Services;
using Phantom.Agent.Services.Rpc; using Phantom.Agent.Services.Rpc;
@@ -8,25 +7,31 @@ using Phantom.Common.Logging;
using Phantom.Utils.Rpc; using Phantom.Utils.Rpc;
using Phantom.Utils.Runtime; using Phantom.Utils.Runtime;
const int ProtocolVersion = 1; const int AgentVersion = 1;
var shutdownCancellationTokenSource = new CancellationTokenSource(); var cancellationTokenSource = new CancellationTokenSource();
var shutdownCancellationToken = shutdownCancellationTokenSource.Token;
PosixSignals.RegisterCancellation(shutdownCancellationTokenSource, static () => { PosixSignals.RegisterCancellation(cancellationTokenSource, static () => {
PhantomLogger.Root.InformationHeading("Stopping Phantom Panel agent..."); PhantomLogger.Root.InformationHeading("Stopping Phantom Panel agent...");
}); });
try { try {
var fullVersion = AssemblyAttributes.GetFullVersion(Assembly.GetExecutingAssembly());
PhantomLogger.Root.InformationHeading("Initializing Phantom Panel agent..."); PhantomLogger.Root.InformationHeading("Initializing Phantom Panel agent...");
PhantomLogger.Root.Information("Agent version: {Version}", fullVersion);
var (serverHost, serverPort, javaSearchPath, agentKeyToken, agentKeyFilePath, agentName, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts) = Variables.LoadOrExit(); 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 agentKey = await AgentKey.Load(agentKeyToken, agentKeyFilePath); string serverPublicKeyPath = Path.GetFullPath("./secrets/agent.key");
if (agentKey == null) { var serverCertificate = await CertificateFile.LoadPublicKey(serverPublicKeyPath);
if (serverCertificate == null) {
Environment.Exit(1); Environment.Exit(1);
} }
@@ -38,40 +43,21 @@ try {
var agentGuid = await GuidFile.CreateOrLoad(folders.DataFolderPath); var agentGuid = await GuidFile.CreateOrLoad(folders.DataFolderPath);
if (agentGuid == null) { if (agentGuid == null) {
Environment.Exit(1); Environment.Exit(1);
return;
} }
var (serverCertificate, agentToken) = agentKey.Value; var agentInfo = new AgentInfo(agentGuid.Value, agentName, AgentVersion, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts);
var agentInfo = new AgentInfo(agentGuid.Value, agentName, ProtocolVersion, fullVersion, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts);
var agentServices = new AgentServices(agentInfo, folders); var agentServices = new AgentServices(agentInfo, folders);
MessageListener MessageListenerFactory(RpcServerConnection connection) {
return new MessageListener(connection, agentServices, shutdownCancellationTokenSource);
}
PhantomLogger.Root.InformationHeading("Launching Phantom Panel agent..."); PhantomLogger.Root.InformationHeading("Launching Phantom Panel agent...");
await agentServices.Initialize(); await agentServices.Initialize();
await RpcLauncher.Launch(new RpcConfiguration(PhantomLogger.Create("Rpc"), serverHost, serverPort, serverCertificate, cancellationTokenSource.Token), agentAuthToken, agentInfo, socket => new MessageListener(socket, agentServices, cancellationTokenSource));
var rpcDisconnectSemaphore = new SemaphoreSlim(0, 1); await agentServices.Shutdown();
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) { } catch (OperationCanceledException) {
// Ignore. // Ignore.
} catch (Exception e) {
PhantomLogger.Root.Fatal(e, "Caught exception in entry point.");
} finally { } finally {
shutdownCancellationTokenSource.Dispose(); cancellationTokenSource.Dispose();
PhantomLogger.Root.Information("Bye!"); PhantomLogger.Root.Information("Bye!");
PhantomLogger.Dispose(); PhantomLogger.Dispose();
} }

View File

@@ -9,8 +9,8 @@ sealed record Variables(
string ServerHost, string ServerHost,
ushort ServerPort, ushort ServerPort,
string JavaSearchPath, string JavaSearchPath,
string? AgentKeyToken, string? AuthToken,
string? AgentKeyFilePath, string? AuthTokenFilePath,
string AgentName, string AgentName,
ushort MaxInstances, ushort MaxInstances,
RamAllocationUnits MaxMemory, RamAllocationUnits MaxMemory,
@@ -18,20 +18,20 @@ sealed record Variables(
AllowedPorts AllowedRconPorts AllowedPorts AllowedRconPorts
) { ) {
private static Variables LoadOrThrow() { private static Variables LoadOrThrow() {
var (agentKeyToken, agentKeyFilePath) = EnvironmentVariables.GetEitherString("AGENT_KEY", "AGENT_KEY_FILE").Require; var (authToken, authTokenFilePath) = EnvironmentVariables.GetEitherString("SERVER_AUTH_TOKEN", "SERVER_AUTH_TOKEN_FILE").OrThrow;
var javaSearchPath = EnvironmentVariables.GetString("JAVA_SEARCH_PATH").WithDefaultGetter(GetDefaultJavaSearchPath); var javaSearchPath = EnvironmentVariables.GetString("JAVA_SEARCH_PATH").OrGetDefault(GetDefaultJavaSearchPath);
return new Variables( return new Variables(
EnvironmentVariables.GetString("SERVER_HOST").Require, EnvironmentVariables.GetString("SERVER_HOST").OrThrow,
EnvironmentVariables.GetPortNumber("SERVER_PORT").WithDefault(9401), EnvironmentVariables.GetPortNumber("SERVER_PORT").OrDefault(9401),
javaSearchPath, javaSearchPath,
agentKeyToken, authToken,
agentKeyFilePath, authTokenFilePath,
EnvironmentVariables.GetString("AGENT_NAME").Require, EnvironmentVariables.GetString("AGENT_NAME").OrThrow,
(ushort) EnvironmentVariables.GetInteger("MAX_INSTANCES", min: 1, max: 10000).Require, (ushort) EnvironmentVariables.GetInteger("MAX_INSTANCES", min: 1, max: 10000).OrThrow,
EnvironmentVariables.GetString("MAX_MEMORY").MapParse(RamAllocationUnits.FromString).Require, EnvironmentVariables.GetString("MAX_MEMORY").MapParse(RamAllocationUnits.FromString).OrThrow,
EnvironmentVariables.GetString("ALLOWED_SERVER_PORTS").MapParse(AllowedPorts.FromString).Require, EnvironmentVariables.GetString("ALLOWED_SERVER_PORTS").MapParse(AllowedPorts.FromString).OrThrow,
EnvironmentVariables.GetString("ALLOWED_RCON_PORTS").MapParse(AllowedPorts.FromString).Require EnvironmentVariables.GetString("ALLOWED_RCON_PORTS").MapParse(AllowedPorts.FromString).OrThrow
); );
} }

View File

@@ -11,11 +11,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="NUnit" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" /> <PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="NUnit.Analyzers" /> <PackageReference Include="NUnit.Analyzers" Version="3.3.0" />
<PackageReference Include="coverlet.collector" /> <PackageReference Include="coverlet.collector" Version="3.1.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,39 +1,56 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography; using System.Security.Cryptography;
using MemoryPack; using System.Text;
using MessagePack;
using Phantom.Utils.Cryptography;
using Phantom.Utils.IO;
namespace Phantom.Common.Data.Agent; namespace Phantom.Common.Data.Agent;
[MemoryPackable] [MessagePackObject]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public sealed partial class AgentAuthToken { public sealed class AgentAuthToken {
internal const int Length = 12; private const int MinimumTokenLength = 30;
private const int MaximumTokenLength = 100;
[MemoryPackOrder(0)] [Key(0)]
[MemoryPackInclude] public string Value { get; }
[IgnoreMember]
private readonly byte[] bytes; private readonly byte[] bytes;
internal AgentAuthToken(byte[]? bytes) { public AgentAuthToken(string? value) {
if (bytes == null) { if (value == null) {
throw new ArgumentNullException(nameof(bytes)); throw new ArgumentNullException(nameof(value));
} }
if (bytes.Length != Length) { if (value.Length is < MinimumTokenLength or > MaximumTokenLength) {
throw new ArgumentOutOfRangeException(nameof(bytes), "Invalid token length: " + bytes.Length + ". Token length must be exactly " + Length + " bytes."); throw new ArgumentOutOfRangeException(nameof(value), "Invalid token length: " + value.Length + ". Token length must be between " + MinimumTokenLength + " and " + MaximumTokenLength + ".");
} }
this.bytes = bytes; this.Value = value;
this.bytes = TokenGenerator.GetBytesOrThrow(value);
} }
public bool FixedTimeEquals(AgentAuthToken providedAuthToken) { public bool FixedTimeEquals(AgentAuthToken providedAuthToken) {
return CryptographicOperations.FixedTimeEquals(bytes, providedAuthToken.bytes); return CryptographicOperations.FixedTimeEquals(bytes, providedAuthToken.bytes);
} }
internal void WriteTo(Span<byte> span) { public override string ToString() {
bytes.CopyTo(span); 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());
} }
public static AgentAuthToken Generate() { public static AgentAuthToken Generate() {
return new AgentAuthToken(RandomNumberGenerator.GetBytes(Length)); return new AgentAuthToken(TokenGenerator.Create(MinimumTokenLength));
} }
} }

View File

@@ -1,15 +1,14 @@
using MemoryPack; using MessagePack;
namespace Phantom.Common.Data.Agent; namespace Phantom.Common.Data.Agent;
[MemoryPackable] [MessagePackObject]
public sealed partial record AgentInfo( public sealed record AgentInfo(
[property: MemoryPackOrder(0)] Guid Guid, [property: Key(0)] Guid Guid,
[property: MemoryPackOrder(1)] string Name, [property: Key(1)] string Name,
[property: MemoryPackOrder(2)] ushort ProtocolVersion, [property: Key(2)] ushort Version,
[property: MemoryPackOrder(3)] string BuildVersion, [property: Key(3)] ushort MaxInstances,
[property: MemoryPackOrder(4)] ushort MaxInstances, [property: Key(4)] RamAllocationUnits MaxMemory,
[property: MemoryPackOrder(5)] RamAllocationUnits MaxMemory, [property: Key(5)] AllowedPorts AllowedServerPorts,
[property: MemoryPackOrder(6)] AllowedPorts AllowedServerPorts, [property: Key(6)] AllowedPorts AllowedRconPorts
[property: MemoryPackOrder(7)] AllowedPorts AllowedRconPorts
); );

View File

@@ -1,18 +0,0 @@
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);
}
}

View File

@@ -1,28 +1,29 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Text; using System.Text;
using MemoryPack; using MessagePack;
namespace Phantom.Common.Data; namespace Phantom.Common.Data;
[MemoryPackable] [MessagePackObject]
public sealed partial class AllowedPorts { [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
[MemoryPackOrder(0)] public sealed class AllowedPorts {
[MemoryPackInclude] [Key(0)]
private readonly ImmutableArray<PortRange> allDefinitions; public ImmutableArray<PortRange> AllDefinitions { get; }
private AllowedPorts(ImmutableArray<PortRange> allDefinitions) { public AllowedPorts(ImmutableArray<PortRange> allDefinitions) {
// TODO normalize and deduplicate ranges // 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) { public bool Contains(ushort port) {
return allDefinitions.Any(definition => definition.Contains(port)); return AllDefinitions.Any(definition => definition.Contains(port));
} }
public override string ToString() { public override string ToString() {
var builder = new StringBuilder(); var builder = new StringBuilder();
foreach (var definition in allDefinitions) { foreach (var definition in AllDefinitions) {
definition.ToString(builder); definition.ToString(builder);
builder.Append(','); builder.Append(',');
} }
@@ -34,7 +35,53 @@ public sealed partial class AllowedPorts {
return builder.ToString(); return builder.ToString();
} }
private static AllowedPorts FromString(ReadOnlySpan<char> definitions) { [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) {
List<PortRange> parsedDefinitions = new (); List<PortRange> parsedDefinitions = new ();
while (!definitions.IsEmpty) { while (!definitions.IsEmpty) {

View File

@@ -1,67 +0,0 @@
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;
}
}

View File

@@ -1,20 +1,18 @@
using System.Collections.Immutable; using MessagePack;
using MemoryPack;
using Phantom.Common.Data.Minecraft; using Phantom.Common.Data.Minecraft;
namespace Phantom.Common.Data.Instance; namespace Phantom.Common.Data.Instance;
[MemoryPackable] [MessagePackObject]
public sealed partial record InstanceConfiguration( public sealed record InstanceConfiguration(
[property: MemoryPackOrder(0)] Guid AgentGuid, [property: Key(0)] Guid AgentGuid,
[property: MemoryPackOrder(1)] Guid InstanceGuid, [property: Key(1)] Guid InstanceGuid,
[property: MemoryPackOrder(2)] string InstanceName, [property: Key(2)] string InstanceName,
[property: MemoryPackOrder(3)] ushort ServerPort, [property: Key(3)] ushort ServerPort,
[property: MemoryPackOrder(4)] ushort RconPort, [property: Key(4)] ushort RconPort,
[property: MemoryPackOrder(5)] string MinecraftVersion, [property: Key(5)] string MinecraftVersion,
[property: MemoryPackOrder(6)] MinecraftServerKind MinecraftServerKind, [property: Key(6)] MinecraftServerKind MinecraftServerKind,
[property: MemoryPackOrder(7)] RamAllocationUnits MemoryAllocation, [property: Key(7)] RamAllocationUnits MemoryAllocation,
[property: MemoryPackOrder(8)] Guid JavaRuntimeGuid, [property: Key(8)] Guid JavaRuntimeGuid,
[property: MemoryPackOrder(9)] ImmutableArray<string> JvmArguments, [property: Key(9)] bool LaunchAutomatically
[property: MemoryPackOrder(10)] bool LaunchAutomatically
); );

View File

@@ -1,31 +1,25 @@
namespace Phantom.Common.Data.Instance; namespace Phantom.Common.Data.Instance;
public enum InstanceLaunchFailReason : byte { public enum InstanceLaunchFailReason {
UnknownError,
ServerPortNotAllowed, ServerPortNotAllowed,
ServerPortAlreadyInUse, ServerPortAlreadyInUse,
RconPortNotAllowed, RconPortNotAllowed,
RconPortAlreadyInUse, RconPortAlreadyInUse,
JavaRuntimeNotFound, JavaRuntimeNotFound,
InvalidJvmArguments,
CouldNotDownloadMinecraftServer, CouldNotDownloadMinecraftServer,
CouldNotConfigureMinecraftServer, UnknownError
CouldNotStartMinecraftServer
} }
public static class InstanceLaunchFailReasonExtensions { public static class InstanceLaunchFailReasonExtensions {
public static string ToSentence(this InstanceLaunchFailReason reason) { public static string ToSentence(this InstanceLaunchFailReason reason) {
return reason switch { return reason switch {
InstanceLaunchFailReason.ServerPortNotAllowed => "Server port not allowed.", InstanceLaunchFailReason.ServerPortNotAllowed => "Server port not allowed.",
InstanceLaunchFailReason.ServerPortAlreadyInUse => "Server port already in use.", InstanceLaunchFailReason.ServerPortAlreadyInUse => "Server port already in use.",
InstanceLaunchFailReason.RconPortNotAllowed => "Rcon port not allowed.", InstanceLaunchFailReason.RconPortNotAllowed => "Rcon port not allowed.",
InstanceLaunchFailReason.RconPortAlreadyInUse => "Rcon port already in use.", InstanceLaunchFailReason.RconPortAlreadyInUse => "Rcon port already in use.",
InstanceLaunchFailReason.JavaRuntimeNotFound => "Java runtime not found.", InstanceLaunchFailReason.JavaRuntimeNotFound => "Java runtime not found.",
InstanceLaunchFailReason.InvalidJvmArguments => "Invalid JVM arguments.", InstanceLaunchFailReason.CouldNotDownloadMinecraftServer => "Could not download Minecraft server.",
InstanceLaunchFailReason.CouldNotDownloadMinecraftServer => "Could not download Minecraft server.", _ => "Unknown error."
InstanceLaunchFailReason.CouldNotConfigureMinecraftServer => "Could not configure Minecraft server.",
InstanceLaunchFailReason.CouldNotStartMinecraftServer => "Could not start Minecraft server.",
_ => "Unknown error."
}; };
} }
} }

View File

@@ -0,0 +1,63 @@
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;
}
}

View File

@@ -1,13 +1,13 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using MemoryPack; using MessagePack;
namespace Phantom.Common.Data.Java; namespace Phantom.Common.Data.Java;
[MemoryPackable] [MessagePackObject]
public sealed partial record JavaRuntime( public sealed record JavaRuntime(
[property: MemoryPackOrder(0)] string MainVersion, [property: Key(0)] string MainVersion,
[property: MemoryPackOrder(1)] string FullVersion, [property: Key(1)] string FullVersion,
[property: MemoryPackOrder(2)] string DisplayName [property: Key(2)] string DisplayName
) : IComparable<JavaRuntime> { ) : IComparable<JavaRuntime> {
public int CompareTo(JavaRuntime? other) { public int CompareTo(JavaRuntime? other) {
if (ReferenceEquals(this, other)) { if (ReferenceEquals(this, other)) {

View File

@@ -1,9 +1,9 @@
using MemoryPack; using MessagePack;
namespace Phantom.Common.Data.Java; namespace Phantom.Common.Data.Java;
[MemoryPackable] [MessagePackObject]
public sealed partial record TaggedJavaRuntime( public sealed record TaggedJavaRuntime(
[property: MemoryPackOrder(0)] Guid Guid, [property: Key(0)] Guid Guid,
[property: MemoryPackOrder(1)] JavaRuntime Runtime [property: Key(1)] JavaRuntime Runtime
); );

View File

@@ -1,10 +0,0 @@
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);
}

View File

@@ -1,7 +0,0 @@
namespace Phantom.Common.Data.Minecraft;
public sealed record MinecraftVersion(
string Id,
MinecraftVersionType Type,
string MetadataUrl
);

View File

@@ -1,38 +0,0 @@
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"
};
}
}

View File

@@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MemoryPack" /> <PackageReference Include="MessagePack.Annotations" Version="2.4.35" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,48 +0,0 @@
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() + "'.");
}
}
}

View File

@@ -1,17 +1,17 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using MemoryPack; using MessagePack;
namespace Phantom.Common.Data; namespace Phantom.Common.Data;
/// <summary> /// <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). /// 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> /// </summary>
[MemoryPackable] [MessagePackObject]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public readonly partial record struct RamAllocationUnits( public readonly record struct RamAllocationUnits(
[property: MemoryPackOrder(0)] ushort RawValue [property: Key(0)] ushort RawValue
) : IComparable<RamAllocationUnits> { ) : IComparable<RamAllocationUnits> {
[MemoryPackIgnore] [IgnoreMember]
public uint InMegabytes => (uint) RawValue * MegabytesPerUnit; public uint InMegabytes => (uint) RawValue * MegabytesPerUnit;
public int CompareTo(RamAllocationUnits other) { public int CompareTo(RamAllocationUnits other) {

View File

@@ -1,7 +1,8 @@
namespace Phantom.Common.Data.Replies; namespace Phantom.Common.Data.Replies;
public enum ConfigureInstanceResult : byte { public enum ConfigureInstanceResult {
Success, Success,
AgentShuttingDown,
InstanceLimitExceeded, InstanceLimitExceeded,
MemoryLimitExceeded MemoryLimitExceeded
} }

View File

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

View File

@@ -1,41 +0,0 @@
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);
}
}

View File

@@ -1,20 +1,26 @@
namespace Phantom.Common.Data.Replies; namespace Phantom.Common.Data.Replies;
public enum LaunchInstanceResult : byte { public enum LaunchInstanceResult {
UnknownError,
LaunchInitiated, LaunchInitiated,
AgentShuttingDown,
InstanceDoesNotExist,
InstanceAlreadyLaunching, InstanceAlreadyLaunching,
InstanceAlreadyRunning, InstanceAlreadyRunning,
InstanceIsStopping InstanceIsStopping,
CommunicationError,
UnknownError
} }
public static class LaunchInstanceResultExtensions { public static class LaunchInstanceResultExtensions {
public static string ToSentence(this LaunchInstanceResult reason) { public static string ToSentence(this LaunchInstanceResult reason) {
return reason switch { return reason switch {
LaunchInstanceResult.LaunchInitiated => "Launch initiated.", LaunchInstanceResult.LaunchInitiated => "Launch initiated.",
LaunchInstanceResult.AgentShuttingDown => "Agent is shutting down.",
LaunchInstanceResult.InstanceDoesNotExist => "Instance does not exist.",
LaunchInstanceResult.InstanceAlreadyLaunching => "Instance is already launching.", LaunchInstanceResult.InstanceAlreadyLaunching => "Instance is already launching.",
LaunchInstanceResult.InstanceAlreadyRunning => "Instance is already running.", LaunchInstanceResult.InstanceAlreadyRunning => "Instance is already running.",
LaunchInstanceResult.InstanceIsStopping => "Instance is stopping.", LaunchInstanceResult.InstanceIsStopping => "Instance is stopping.",
LaunchInstanceResult.CommunicationError => "Communication error.",
_ => "Unknown error." _ => "Unknown error."
}; };
} }

View File

@@ -1,15 +1,21 @@
namespace Phantom.Common.Data.Replies; namespace Phantom.Common.Data.Replies;
public enum SendCommandToInstanceResult : byte { public enum SendCommandToInstanceResult {
UnknownError, Success,
Success InstanceDoesNotExist,
AgentShuttingDown,
AgentCommunicationError,
UnknownError
} }
public static class SendCommandToInstanceResultExtensions { public static class SendCommandToInstanceResultExtensions {
public static string ToSentence(this SendCommandToInstanceResult reason) { public static string ToSentence(this SendCommandToInstanceResult reason) {
return reason switch { return reason switch {
SendCommandToInstanceResult.Success => "Command sent.", SendCommandToInstanceResult.Success => "Command sent.",
_ => "Unknown error." SendCommandToInstanceResult.InstanceDoesNotExist => "Instance does not exist.",
SendCommandToInstanceResult.AgentShuttingDown => "Agent is shutting down.",
SendCommandToInstanceResult.AgentCommunicationError => "Agent did not reply in time.",
_ => "Unknown error."
}; };
} }
} }

View File

@@ -1,18 +1,24 @@
namespace Phantom.Common.Data.Replies; namespace Phantom.Common.Data.Replies;
public enum StopInstanceResult : byte { public enum StopInstanceResult {
UnknownError,
StopInitiated, StopInitiated,
AgentShuttingDown,
InstanceDoesNotExist,
InstanceAlreadyStopping, InstanceAlreadyStopping,
InstanceAlreadyStopped InstanceAlreadyStopped,
CommunicationError,
UnknownError
} }
public static class StopInstanceResultExtensions { public static class StopInstanceResultExtensions {
public static string ToSentence(this StopInstanceResult reason) { public static string ToSentence(this StopInstanceResult reason) {
return reason switch { return reason switch {
StopInstanceResult.StopInitiated => "Stopping initiated.", StopInstanceResult.StopInitiated => "Stopping initiated.",
StopInstanceResult.AgentShuttingDown => "Agent is shutting down.",
StopInstanceResult.InstanceDoesNotExist => "Instance does not exist.",
StopInstanceResult.InstanceAlreadyStopping => "Instance is already stopping.", StopInstanceResult.InstanceAlreadyStopping => "Instance is already stopping.",
StopInstanceResult.InstanceAlreadyStopped => "Instance is already stopped.", StopInstanceResult.InstanceAlreadyStopped => "Instance is already stopped.",
StopInstanceResult.CommunicationError => "Communication error.",
_ => "Unknown error." _ => "Unknown error."
}; };
} }

View File

@@ -7,8 +7,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog" /> <PackageReference Include="Serilog" Version="2.12.0" />
<PackageReference Include="Serilog.Sinks.Console" /> <PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -33,8 +33,8 @@ public static class PhantomLogger {
return Base.ForContext("Category", name); return Base.ForContext("Category", name);
} }
public static ILogger Create(string name1, string name2) { public static ILogger Create(Type type) {
return Create(name1 + ":" + name2); return Create(type.Name);
} }
public static ILogger Create<T>() { public static ILogger Create<T>() {
@@ -42,11 +42,11 @@ public static class PhantomLogger {
} }
public static ILogger Create<T>(string name) { 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>() { 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() { public static void Dispose() {

View File

@@ -1,18 +0,0 @@
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);
}
}

View File

@@ -2,6 +2,4 @@
namespace Phantom.Common.Messages; namespace Phantom.Common.Messages;
public interface IMessageToAgent<TReply> : IMessage<IMessageToAgentListener, TReply> {} public interface IMessageToAgent : IMessage<IMessageToAgentListener> {}
public interface IMessageToAgent : IMessageToAgent<NoReply> {}

View File

@@ -1,16 +1,13 @@
using Phantom.Common.Data.Replies; using Phantom.Common.Messages.ToAgent;
using Phantom.Common.Messages.BiDirectional;
using Phantom.Common.Messages.ToAgent;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages; namespace Phantom.Common.Messages;
public interface IMessageToAgentListener { public interface IMessageToAgentListener {
Task<NoReply> HandleRegisterAgentSuccess(RegisterAgentSuccessMessage message); Task HandleRegisterAgentSuccessResult(RegisterAgentSuccessMessage message);
Task<NoReply> HandleRegisterAgentFailure(RegisterAgentFailureMessage message); Task HandleRegisterAgentFailureResult(RegisterAgentFailureMessage message);
Task<InstanceActionResult<ConfigureInstanceResult>> HandleConfigureInstance(ConfigureInstanceMessage message); Task HandleConfigureInstance(ConfigureInstanceMessage message);
Task<InstanceActionResult<LaunchInstanceResult>> HandleLaunchInstance(LaunchInstanceMessage message); Task HandleLaunchInstance(LaunchInstanceMessage message);
Task<InstanceActionResult<StopInstanceResult>> HandleStopInstance(StopInstanceMessage message); Task HandleStopInstance(StopInstanceMessage message);
Task<InstanceActionResult<SendCommandToInstanceResult>> HandleSendCommandToInstance(SendCommandToInstanceMessage message); Task HandleSendCommandToInstance(SendCommandToInstanceMessage message);
Task<NoReply> HandleReply(ReplyMessage message); Task HandleShutdownAgent(ShutdownAgentMessage message);
} }

View File

@@ -1,7 +1,5 @@
using Phantom.Utils.Rpc.Message; using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages; namespace Phantom.Common.Messages;
public interface IMessageToServer<TReply> : IMessage<IMessageToServerListener, TReply> {} public interface IMessageToServer : IMessage<IMessageToServerListener> {}
public interface IMessageToServer : IMessageToServer<NoReply> {}

View File

@@ -1,16 +1,14 @@
using Phantom.Common.Messages.BiDirectional; using Phantom.Common.Messages.ToServer;
using Phantom.Common.Messages.ToServer;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages; namespace Phantom.Common.Messages;
public interface IMessageToServerListener { public interface IMessageToServerListener {
bool IsDisposed { get; } bool IsDisposed { get; }
Task<NoReply> HandleRegisterAgent(RegisterAgentMessage message); Task HandleRegisterAgent(RegisterAgentMessage message);
Task<NoReply> HandleUnregisterAgent(UnregisterAgentMessage message); Task HandleUnregisterAgent(UnregisterAgentMessage message);
Task<NoReply> HandleAgentIsAlive(AgentIsAliveMessage message); Task HandleAgentIsAlive(AgentIsAliveMessage message);
Task<NoReply> HandleAdvertiseJavaRuntimes(AdvertiseJavaRuntimesMessage message); Task HandleAdvertiseJavaRuntimes(AdvertiseJavaRuntimesMessage message);
Task<NoReply> HandleReportInstanceStatus(ReportInstanceStatusMessage message); Task HandleReportInstanceStatus(ReportInstanceStatusMessage message);
Task<NoReply> HandleInstanceOutput(InstanceOutputMessage message); Task HandleInstanceOutput(InstanceOutputMessage message);
Task<NoReply> HandleReply(ReplyMessage message); Task HandleSimpleReply(SimpleReplyMessage message);
} }

View File

@@ -0,0 +1,5 @@
namespace Phantom.Common.Messages;
public interface IMessageWithReply {
public uint SequenceId { get; }
}

View File

@@ -1,6 +1,4 @@
using Phantom.Common.Data.Replies; using Phantom.Common.Logging;
using Phantom.Common.Logging;
using Phantom.Common.Messages.BiDirectional;
using Phantom.Common.Messages.ToAgent; using Phantom.Common.Messages.ToAgent;
using Phantom.Common.Messages.ToServer; using Phantom.Common.Messages.ToServer;
using Phantom.Utils.Rpc.Message; using Phantom.Utils.Rpc.Message;
@@ -8,17 +6,17 @@ using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages; namespace Phantom.Common.Messages;
public static class MessageRegistries { public static class MessageRegistries {
public static MessageRegistry<IMessageToAgentListener> ToAgent { get; } = new (PhantomLogger.Create("MessageRegistry:ToAgent")); public static MessageRegistry<IMessageToAgentListener, IMessageToAgent> ToAgent { get; } = new (PhantomLogger.Create("MessageRegistry:ToAgent"));
public static MessageRegistry<IMessageToServerListener> ToServer { get; } = new (PhantomLogger.Create("MessageRegistry:ToServer")); public static MessageRegistry<IMessageToServerListener, IMessageToServer> ToServer { get; } = new (PhantomLogger.Create("MessageRegistry:ToServer"));
static MessageRegistries() { static MessageRegistries() {
ToAgent.Add<RegisterAgentSuccessMessage>(0); ToAgent.Add<RegisterAgentSuccessMessage>(0);
ToAgent.Add<RegisterAgentFailureMessage>(1); ToAgent.Add<RegisterAgentFailureMessage>(1);
ToAgent.Add<ConfigureInstanceMessage, InstanceActionResult<ConfigureInstanceResult>>(2); ToAgent.Add<ConfigureInstanceMessage>(2);
ToAgent.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(3); ToAgent.Add<LaunchInstanceMessage>(3);
ToAgent.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(4); ToAgent.Add<StopInstanceMessage>(4);
ToAgent.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(5); ToAgent.Add<SendCommandToInstanceMessage>(5);
ToAgent.Add<ReplyMessage>(127); ToAgent.Add<ShutdownAgentMessage>(6);
ToServer.Add<RegisterAgentMessage>(0); ToServer.Add<RegisterAgentMessage>(0);
ToServer.Add<UnregisterAgentMessage>(1); ToServer.Add<UnregisterAgentMessage>(1);
@@ -26,6 +24,6 @@ public static class MessageRegistries {
ToServer.Add<AdvertiseJavaRuntimesMessage>(3); ToServer.Add<AdvertiseJavaRuntimesMessage>(3);
ToServer.Add<ReportInstanceStatusMessage>(4); ToServer.Add<ReportInstanceStatusMessage>(4);
ToServer.Add<InstanceOutputMessage>(5); ToServer.Add<InstanceOutputMessage>(5);
ToServer.Add<ReplyMessage>(127); ToServer.Add<SimpleReplyMessage>(127);
} }
} }

View File

@@ -1,14 +1,14 @@
using MemoryPack; using MessagePack;
using Phantom.Common.Data.Instance; using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Replies;
namespace Phantom.Common.Messages.ToAgent; namespace Phantom.Common.Messages.ToAgent;
[MemoryPackable] [MessagePackObject]
public sealed partial record ConfigureInstanceMessage( public sealed record ConfigureInstanceMessage(
[property: MemoryPackOrder(0)] InstanceConfiguration Configuration [property: Key(0)] uint SequenceId,
) : IMessageToAgent<InstanceActionResult<ConfigureInstanceResult>> { [property: Key(1)] InstanceConfiguration Configuration
public Task<InstanceActionResult<ConfigureInstanceResult>> Accept(IMessageToAgentListener listener) { ) : IMessageToAgent, IMessageWithReply {
public Task Accept(IMessageToAgentListener listener) {
return listener.HandleConfigureInstance(this); return listener.HandleConfigureInstance(this);
} }
} }

View File

@@ -1,13 +1,13 @@
using MemoryPack; using MessagePack;
using Phantom.Common.Data.Replies;
namespace Phantom.Common.Messages.ToAgent; namespace Phantom.Common.Messages.ToAgent;
[MemoryPackable] [MessagePackObject]
public sealed partial record LaunchInstanceMessage( public sealed record LaunchInstanceMessage(
[property: MemoryPackOrder(0)] Guid InstanceGuid [property: Key(0)] uint SequenceId,
) : IMessageToAgent<InstanceActionResult<LaunchInstanceResult>> { [property: Key(1)] Guid InstanceGuid
public Task<InstanceActionResult<LaunchInstanceResult>> Accept(IMessageToAgentListener listener) { ) : IMessageToAgent, IMessageWithReply {
public Task Accept(IMessageToAgentListener listener) {
return listener.HandleLaunchInstance(this); return listener.HandleLaunchInstance(this);
} }
} }

View File

@@ -1,14 +1,13 @@
using MemoryPack; using MessagePack;
using Phantom.Common.Data.Replies; using Phantom.Common.Data.Replies;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.ToAgent; namespace Phantom.Common.Messages.ToAgent;
[MemoryPackable] [MessagePackObject]
public sealed partial record RegisterAgentFailureMessage( public sealed record RegisterAgentFailureMessage(
[property: MemoryPackOrder(0)] RegisterAgentFailure FailureKind [property: Key(0)] RegisterAgentFailure FailureKind
) : IMessageToAgent { ) : IMessageToAgent {
public Task<NoReply> Accept(IMessageToAgentListener listener) { public Task Accept(IMessageToAgentListener listener) {
return listener.HandleRegisterAgentFailure(this); return listener.HandleRegisterAgentFailureResult(this);
} }
} }

View File

@@ -1,15 +1,14 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using MemoryPack; using MessagePack;
using Phantom.Common.Data.Instance; using Phantom.Common.Data.Instance;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.ToAgent; namespace Phantom.Common.Messages.ToAgent;
[MemoryPackable] [MessagePackObject]
public sealed partial record RegisterAgentSuccessMessage( public sealed record RegisterAgentSuccessMessage(
[property: MemoryPackOrder(0)] ImmutableArray<InstanceConfiguration> InitialInstances [property: Key(0)] ImmutableArray<InstanceConfiguration> InitialInstances
) : IMessageToAgent { ) : IMessageToAgent {
public Task<NoReply> Accept(IMessageToAgentListener listener) { public Task Accept(IMessageToAgentListener listener) {
return listener.HandleRegisterAgentSuccess(this); return listener.HandleRegisterAgentSuccessResult(this);
} }
} }

View File

@@ -1,14 +1,14 @@
using MemoryPack; using MessagePack;
using Phantom.Common.Data.Replies;
namespace Phantom.Common.Messages.ToAgent; namespace Phantom.Common.Messages.ToAgent;
[MemoryPackable] [MessagePackObject]
public sealed partial record SendCommandToInstanceMessage( public sealed record SendCommandToInstanceMessage(
[property: MemoryPackOrder(0)] Guid InstanceGuid, [property: Key(0)] uint SequenceId,
[property: MemoryPackOrder(1)] string Command [property: Key(1)] Guid InstanceGuid,
) : IMessageToAgent<InstanceActionResult<SendCommandToInstanceResult>> { [property: Key(2)] string Command
public Task<InstanceActionResult<SendCommandToInstanceResult>> Accept(IMessageToAgentListener listener) { ) : IMessageToAgent, IMessageWithReply {
public Task Accept(IMessageToAgentListener listener) {
return listener.HandleSendCommandToInstance(this); return listener.HandleSendCommandToInstance(this);
} }
} }

View File

@@ -0,0 +1,10 @@
using MessagePack;
namespace Phantom.Common.Messages.ToAgent;
[MessagePackObject]
public sealed record ShutdownAgentMessage : IMessageToAgent {
public Task Accept(IMessageToAgentListener listener) {
return listener.HandleShutdownAgent(this);
}
}

View File

@@ -1,15 +1,13 @@
using MemoryPack; using MessagePack;
using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies;
namespace Phantom.Common.Messages.ToAgent; namespace Phantom.Common.Messages.ToAgent;
[MemoryPackable] [MessagePackObject]
public sealed partial record StopInstanceMessage( public sealed record StopInstanceMessage(
[property: MemoryPackOrder(0)] Guid InstanceGuid, [property: Key(0)] uint SequenceId,
[property: MemoryPackOrder(1)] MinecraftStopStrategy StopStrategy [property: Key(1)] Guid InstanceGuid
) : IMessageToAgent<InstanceActionResult<StopInstanceResult>> { ) : IMessageToAgent, IMessageWithReply {
public Task<InstanceActionResult<StopInstanceResult>> Accept(IMessageToAgentListener listener) { public Task Accept(IMessageToAgentListener listener) {
return listener.HandleStopInstance(this); return listener.HandleStopInstance(this);
} }
} }

View File

@@ -1,15 +1,14 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using MemoryPack; using MessagePack;
using Phantom.Common.Data.Java; using Phantom.Common.Data.Java;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.ToServer; namespace Phantom.Common.Messages.ToServer;
[MemoryPackable] [MessagePackObject]
public sealed partial record AdvertiseJavaRuntimesMessage( public sealed record AdvertiseJavaRuntimesMessage(
[property: MemoryPackOrder(0)] ImmutableArray<TaggedJavaRuntime> Runtimes [property: Key(0)] ImmutableArray<TaggedJavaRuntime> Runtimes
) : IMessageToServer { ) : IMessageToServer {
public Task<NoReply> Accept(IMessageToServerListener listener) { public Task Accept(IMessageToServerListener listener) {
return listener.HandleAdvertiseJavaRuntimes(this); return listener.HandleAdvertiseJavaRuntimes(this);
} }
} }

View File

@@ -1,11 +1,10 @@
using MemoryPack; using MessagePack;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.ToServer; namespace Phantom.Common.Messages.ToServer;
[MemoryPackable] [MessagePackObject]
public sealed partial record AgentIsAliveMessage : IMessageToServer { public sealed record AgentIsAliveMessage : IMessageToServer {
public Task<NoReply> Accept(IMessageToServerListener listener) { public Task Accept(IMessageToServerListener listener) {
return listener.HandleAgentIsAlive(this); return listener.HandleAgentIsAlive(this);
} }
} }

View File

@@ -1,15 +1,14 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using MemoryPack; using MessagePack;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.ToServer; namespace Phantom.Common.Messages.ToServer;
[MemoryPackable] [MessagePackObject]
public sealed partial record InstanceOutputMessage( public sealed record InstanceOutputMessage(
[property: MemoryPackOrder(0)] Guid InstanceGuid, [property: Key(0)] Guid InstanceGuid,
[property: MemoryPackOrder(1)] ImmutableArray<string> Lines [property: Key(1)] ImmutableArray<string> Lines
) : IMessageToServer { ) : IMessageToServer {
public Task<NoReply> Accept(IMessageToServerListener listener) { public Task Accept(IMessageToServerListener listener) {
return listener.HandleInstanceOutput(this); return listener.HandleInstanceOutput(this);
} }
} }

View File

@@ -1,15 +1,14 @@
using MemoryPack; using MessagePack;
using Phantom.Common.Data.Agent; using Phantom.Common.Data.Agent;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.ToServer; namespace Phantom.Common.Messages.ToServer;
[MemoryPackable] [MessagePackObject]
public sealed partial record RegisterAgentMessage( public sealed record RegisterAgentMessage(
[property: MemoryPackOrder(0)] AgentAuthToken AuthToken, [property: Key(0)] AgentAuthToken AuthToken,
[property: MemoryPackOrder(1)] AgentInfo AgentInfo [property: Key(1)] AgentInfo AgentInfo
) : IMessageToServer { ) : IMessageToServer {
public Task<NoReply> Accept(IMessageToServerListener listener) { public Task Accept(IMessageToServerListener listener) {
return listener.HandleRegisterAgent(this); return listener.HandleRegisterAgent(this);
} }
} }

View File

@@ -1,15 +1,14 @@
using MemoryPack; using MessagePack;
using Phantom.Common.Data.Instance; using Phantom.Common.Data.Instance;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.ToServer; namespace Phantom.Common.Messages.ToServer;
[MemoryPackable] [MessagePackObject]
public sealed partial record ReportInstanceStatusMessage( public sealed record ReportInstanceStatusMessage(
[property: MemoryPackOrder(0)] Guid InstanceGuid, [property: Key(0)] Guid InstanceGuid,
[property: MemoryPackOrder(1)] IInstanceStatus InstanceStatus [property: Key(1)] InstanceStatus InstanceStatus
) : IMessageToServer { ) : IMessageToServer {
public Task<NoReply> Accept(IMessageToServerListener listener) { public Task Accept(IMessageToServerListener listener) {
return listener.HandleReportInstanceStatus(this); return listener.HandleReportInstanceStatus(this);
} }
} }

View File

@@ -0,0 +1,22 @@
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);
}
}

View File

@@ -1,13 +1,12 @@
using MemoryPack; using MessagePack;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.ToServer; namespace Phantom.Common.Messages.ToServer;
[MemoryPackable] [MessagePackObject]
public sealed partial record UnregisterAgentMessage( public sealed record UnregisterAgentMessage(
[property: MemoryPackOrder(0)] Guid AgentGuid [property: Key(0)] Guid AgentGuid
) : IMessageToServer { ) : IMessageToServer {
public Task<NoReply> Accept(IMessageToServerListener listener) { public Task Accept(IMessageToServerListener listener) {
return listener.HandleUnregisterAgent(this); return listener.HandleUnregisterAgent(this);
} }
} }

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