mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2025-09-16 00:32:12 +02:00
Compare commits
1 Commits
c0243dc749
...
experiment
Author | SHA1 | Date | |
---|---|---|---|
fad7b35e04
|
@@ -1,9 +0,0 @@
|
|||||||
# Ignore hidden files
|
|
||||||
.*
|
|
||||||
|
|
||||||
# Include .git for build version information
|
|
||||||
!.git
|
|
||||||
|
|
||||||
# Not needed for building
|
|
||||||
AddMigration.*
|
|
||||||
*.DotSettings.user
|
|
@@ -5,13 +5,13 @@
|
|||||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent1" />
|
<option name="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" />
|
||||||
|
@@ -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" />
|
||||||
|
@@ -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
2
.workdir/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/Agent*/data
|
||||||
|
/Agent*/temp
|
2
.workdir/Agent1/.gitignore
vendored
2
.workdir/Agent1/.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
*
|
|
||||||
!.gitignore
|
|
1
.workdir/Agent1/secrets/agent.key
Normal file
1
.workdir/Agent1/secrets/agent.key
Normal file
@@ -0,0 +1 @@
|
|||||||
|
tLkn<EFBFBD>Z<EFBFBD><EFBFBD>}ы<><D18B>|~2<><32>><3E><>р<EFBFBD><D180>O*<2A> p
|
1
.workdir/Agent1/secrets/agent.token
Normal file
1
.workdir/Agent1/secrets/agent.token
Normal file
@@ -0,0 +1 @@
|
|||||||
|
TH7Z2MJKMR975N6HFBKQP9WQFMWWN5
|
2
.workdir/Agent2/.gitignore
vendored
2
.workdir/Agent2/.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
*
|
|
||||||
!.gitignore
|
|
1
.workdir/Agent2/secrets/agent.key
Normal file
1
.workdir/Agent2/secrets/agent.key
Normal file
@@ -0,0 +1 @@
|
|||||||
|
tLkn<EFBFBD>Z<EFBFBD><EFBFBD>}ы<><D18B>|~2<><32>><3E><>р<EFBFBD><D180>O*<2A> p
|
1
.workdir/Agent2/secrets/agent.token
Normal file
1
.workdir/Agent2/secrets/agent.token
Normal file
@@ -0,0 +1 @@
|
|||||||
|
TH7Z2MJKMR975N6HFBKQP9WQFMWWN5
|
2
.workdir/Agent3/.gitignore
vendored
2
.workdir/Agent3/.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
*
|
|
||||||
!.gitignore
|
|
1
.workdir/Agent3/secrets/agent.key
Normal file
1
.workdir/Agent3/secrets/agent.key
Normal file
@@ -0,0 +1 @@
|
|||||||
|
tLkn<EFBFBD>Z<EFBFBD><EFBFBD>}ы<><D18B>|~2<><32>><3E><>р<EFBFBD><D180>O*<2A> p
|
1
.workdir/Agent3/secrets/agent.token
Normal file
1
.workdir/Agent3/secrets/agent.token
Normal file
@@ -0,0 +1 @@
|
|||||||
|
TH7Z2MJKMR975N6HFBKQP9WQFMWWN5
|
1
.workdir/Server/.gitignore
vendored
1
.workdir/Server/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
/keys
|
|
Binary file not shown.
1
.workdir/Server/secrets/agent.token
Normal file
1
.workdir/Server/secrets/agent.token
Normal file
@@ -0,0 +1 @@
|
|||||||
|
TH7Z2MJKMR975N6HFBKQP9WQFMWWN5
|
@@ -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>
|
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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
|
||||||
|
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
|
@@ -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>
|
||||||
|
@@ -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() {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,18 +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 class MinecraftServerExecutables : IDisposable {
|
public sealed class MinecraftServerExecutables {
|
||||||
private static readonly ILogger Logger = PhantomLogger.Create<MinecraftServerExecutables>();
|
private static readonly ILogger Logger = PhantomLogger.Create<MinecraftServerExecutables>();
|
||||||
|
|
||||||
private static readonly Regex VersionFolderSanitizeRegex = new (@"[^a-zA-Z0-9_\-\.]", RegexOptions.Compiled);
|
private static readonly Regex VersionFolderSanitizeRegex = new (@"[^a-zA-Z0-9_\-\.]", RegexOptions.Compiled);
|
||||||
|
|
||||||
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) {
|
||||||
@@ -28,7 +25,7 @@ public sealed 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;
|
||||||
@@ -43,7 +40,7 @@ public sealed 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);
|
||||||
@@ -56,8 +53,4 @@ public sealed class MinecraftServerExecutables : IDisposable {
|
|||||||
|
|
||||||
return await downloader.Task.WaitAsync(cancellationToken);
|
return await downloader.Task.WaitAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
|
||||||
minecraftVersions.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -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
|
@@ -1,42 +0,0 @@
|
|||||||
using NetMQ.Sockets;
|
|
||||||
using Phantom.Common.Logging;
|
|
||||||
using Phantom.Common.Messages.ToServer;
|
|
||||||
using Phantom.Utils.Runtime;
|
|
||||||
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 ClientSocket socket;
|
|
||||||
private readonly CancellationTokenSource cancellationTokenSource = new ();
|
|
||||||
|
|
||||||
public KeepAliveLoop(ClientSocket socket, TaskManager taskManager) {
|
|
||||||
this.socket = socket;
|
|
||||||
taskManager.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 socket.SendMessage(new AgentIsAliveMessage());
|
|
||||||
}
|
|
||||||
} catch (OperationCanceledException) {
|
|
||||||
// Ignore.
|
|
||||||
} finally {
|
|
||||||
cancellationTokenSource.Dispose();
|
|
||||||
Logger.Information("Stopped keep-alive loop.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Cancel() {
|
|
||||||
cancellationTokenSource.Cancel();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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>
|
||||||
|
@@ -4,37 +4,30 @@ using Phantom.Common.Data.Agent;
|
|||||||
using Phantom.Common.Messages;
|
using Phantom.Common.Messages;
|
||||||
using Phantom.Common.Messages.ToServer;
|
using Phantom.Common.Messages.ToServer;
|
||||||
using Phantom.Utils.Rpc;
|
using Phantom.Utils.Rpc;
|
||||||
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<ClientSocket, 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<ClientSocket, 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<ClientSocket, IMessageToAgentListener> messageListenerFactory, SemaphoreSlim disconnectSemaphore, CancellationToken receiveCancellationToken) : base(socket, config.Logger) {
|
|
||||||
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) {
|
||||||
@@ -46,50 +39,34 @@ public sealed class RpcLauncher : RpcRuntime<ClientSocket> {
|
|||||||
logger.Information("ZeroMQ client ready.");
|
logger.Information("ZeroMQ client ready.");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Run(ClientSocket socket, TaskManager taskManager) {
|
protected override async Task Run(ClientSocket socket, CancellationToken cancellationToken) {
|
||||||
var logger = config.Logger;
|
var logger = config.Logger;
|
||||||
|
|
||||||
var listener = messageListenerFactory(socket);
|
var listener = messageListenerFactory(socket);
|
||||||
|
|
||||||
|
ServerMessaging.SetCurrentSocket(socket, cancellationToken);
|
||||||
|
|
||||||
ServerMessaging.SetCurrentSocket(socket);
|
// TODO optimize msg
|
||||||
var keepAliveLoop = new KeepAliveLoop(socket, taskManager);
|
await foreach (var bytes in socket.ReceiveBytesAsyncEnumerable(cancellationToken)) {
|
||||||
|
if (logger.IsEnabled(LogEventLevel.Verbose)) {
|
||||||
try {
|
if (bytes.Length > 0 && MessageRegistries.ToAgent.TryGetType(bytes, out var type)) {
|
||||||
while (!receiveCancellationToken.IsCancellationRequested) {
|
logger.Verbose("Received {MessageType} ({Bytes} B) from server.", type.Name, bytes.Length);
|
||||||
var data = socket.Receive(receiveCancellationToken);
|
}
|
||||||
|
else {
|
||||||
LogMessageType(logger, data);
|
logger.Verbose("Received {Bytes} B message from server.", bytes.Length);
|
||||||
|
|
||||||
if (data.Length > 0) {
|
|
||||||
MessageRegistries.ToAgent.Handle(data, listener, taskManager, receiveCancellationToken);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} 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) {
|
|
||||||
if (!logger.IsEnabled(LogEventLevel.Verbose)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.Length > 0 && MessageRegistries.ToAgent.TryGetType(data, out var type)) {
|
|
||||||
logger.Verbose("Received {MessageType} ({Bytes} B) from server.", type.Name, data.Length);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
logger.Verbose("Received {Bytes} B message from server.", data.Length);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task Disconnect(ClientSocket socket) {
|
protected override async Task Disconnect(ClientSocket socket) {
|
||||||
var unregisterTimeoutTask = Task.Delay(TimeSpan.FromSeconds(5), CancellationToken.None);
|
var timeoutTask = Task.Delay(TimeSpan.FromSeconds(5));
|
||||||
var finishedTask = await Task.WhenAny(socket.SendMessage(new UnregisterAgentMessage(agentGuid)), unregisterTimeoutTask);
|
var finishedTask = await Task.WhenAny(socket.SendMessage(new UnregisterAgentMessage(agentGuid)), timeoutTask);
|
||||||
if (finishedTask == unregisterTimeoutTask) {
|
if (finishedTask == timeoutTask) {
|
||||||
config.Logger.Error("Timed out communicating agent shutdown with the server.");
|
config.Logger.Error("Timed out communicating agent shutdown with the server.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
using NetMQ.Sockets;
|
using NetMQ.Sockets;
|
||||||
using Phantom.Common.Logging;
|
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;
|
||||||
@@ -8,23 +9,47 @@ namespace Phantom.Agent.Rpc;
|
|||||||
public static class ServerMessaging {
|
public static class ServerMessaging {
|
||||||
private static readonly ILogger Logger = PhantomLogger.Create(typeof(ServerMessaging));
|
private static readonly ILogger Logger = PhantomLogger.Create(typeof(ServerMessaging));
|
||||||
|
|
||||||
|
private static readonly TimeSpan KeepAliveInterval = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
private static ClientSocket? CurrentSocket { get; set; }
|
private static ClientSocket? CurrentSocket { get; set; }
|
||||||
private static readonly object SetCurrentSocketLock = new ();
|
private static readonly object SetCurrentSocketLock = new ();
|
||||||
|
|
||||||
internal static void SetCurrentSocket(ClientSocket socket) {
|
internal static void SetCurrentSocket(ClientSocket socket, CancellationToken cancellationToken) {
|
||||||
|
Logger.Information("Server socket ready.");
|
||||||
|
|
||||||
|
bool isFirstSet = false;
|
||||||
lock (SetCurrentSocketLock) {
|
lock (SetCurrentSocketLock) {
|
||||||
if (CurrentSocket != null) {
|
if (CurrentSocket == null) {
|
||||||
throw new InvalidOperationException("Server socket can only be set once.");
|
isFirstSet = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
CurrentSocket = socket;
|
CurrentSocket = socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Information("Server socket ready.");
|
if (isFirstSet) {
|
||||||
|
Task.Factory.StartNew(static o => SendKeepAliveLoop((CancellationToken) o!), cancellationToken, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task SendMessage<TMessage>(TMessage message) where TMessage : IMessageToServer {
|
public static async Task SendMessage<TMessage>(TMessage message) where TMessage : IMessageToServer {
|
||||||
var currentSocket = CurrentSocket ?? throw new InvalidOperationException("Server socket not ready.");
|
var currentSocket = CurrentSocket ?? throw new InvalidOperationException("Server socket not ready.");
|
||||||
await currentSocket.SendMessage(message);
|
await currentSocket.SendMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task SendKeepAliveLoop(CancellationToken cancellationToken) {
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
await Task.Delay(KeepAliveInterval, cancellationToken);
|
||||||
|
|
||||||
|
var currentSocket = CurrentSocket;
|
||||||
|
if (currentSocket != null) {
|
||||||
|
await currentSocket.SendMessage(new AgentIsAliveMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (OperationCanceledException) {
|
||||||
|
// Ignore.
|
||||||
|
} finally {
|
||||||
|
Logger.Information("Stopped keep-alive loop.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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();
|
|
||||||
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,12 +23,6 @@ public sealed class AgentServices {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task Shutdown() {
|
public async Task Shutdown() {
|
||||||
Logger.Information("Stopping instances...");
|
|
||||||
await InstanceSessionManager.StopAll();
|
await InstanceSessionManager.StopAll();
|
||||||
|
|
||||||
Logger.Information("Stopping task manager...");
|
|
||||||
await TaskManager.Stop();
|
|
||||||
|
|
||||||
Logger.Information("Services stopped.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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.SendMessage(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) {
|
||||||
@@ -88,29 +80,41 @@ sealed class Instance : IDisposable {
|
|||||||
public async Task<LaunchInstanceResult> Launch(CancellationToken cancellationToken) {
|
public async Task<LaunchInstanceResult> Launch(CancellationToken cancellationToken) {
|
||||||
await stateTransitioningActionSemaphore.WaitAsync(cancellationToken);
|
await stateTransitioningActionSemaphore.WaitAsync(cancellationToken);
|
||||||
try {
|
try {
|
||||||
return TransitionStateAndReturn(currentState.Launch(new InstanceContextImpl(this)));
|
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;
|
||||||
@@ -137,10 +141,10 @@ 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(async () => {
|
Task.Run(async () => {
|
||||||
if (myStatusUpdateCounter == statusUpdateCounter) {
|
if (myStatusUpdateCounter == statusUpdateCounter) {
|
||||||
instance.currentStatus = newStatus;
|
instance.currentStatus = newStatus;
|
||||||
await ServerMessaging.SendMessage(new ReportInstanceStatusMessage(Configuration.InstanceGuid, newStatus));
|
await ServerMessaging.SendMessage(new ReportInstanceStatusMessage(Configuration.InstanceGuid, newStatus));
|
||||||
@@ -152,8 +156,6 @@ sealed class Instance : IDisposable {
|
|||||||
instance.stateTransitioningActionSemaphore.Wait();
|
instance.stateTransitioningActionSemaphore.Wait();
|
||||||
try {
|
try {
|
||||||
instance.TransitionState(newState());
|
instance.TransitionState(newState());
|
||||||
} catch (Exception e) {
|
|
||||||
instance.logger.Error(e, "Caught exception during state transition.");
|
|
||||||
} finally {
|
} finally {
|
||||||
instance.stateTransitioningActionSemaphore.Release();
|
instance.stateTransitioningActionSemaphore.Release();
|
||||||
}
|
}
|
||||||
|
@@ -19,7 +19,7 @@ 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> newState);
|
public abstract void TransitionState(Func<IInstanceState> newState);
|
||||||
|
|
||||||
public void TransitionState(IInstanceState newState) {
|
public void TransitionState(IInstanceState newState) {
|
||||||
|
@@ -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(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.SendMessage(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.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
@@ -7,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;
|
||||||
@@ -22,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 ();
|
||||||
@@ -31,11 +27,10 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -67,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)
|
||||||
@@ -118,7 +112,7 @@ sealed class InstanceSessionManager : IDisposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<StopInstanceResult> Stop(Guid instanceGuid, MinecraftStopStrategy stopStrategy) {
|
public async Task<StopInstanceResult> Stop(Guid instanceGuid) {
|
||||||
try {
|
try {
|
||||||
await semaphore.WaitAsync(shutdownCancellationToken);
|
await semaphore.WaitAsync(shutdownCancellationToken);
|
||||||
} catch (OperationCanceledException) {
|
} catch (OperationCanceledException) {
|
||||||
@@ -130,7 +124,7 @@ sealed class InstanceSessionManager : IDisposable {
|
|||||||
return StopInstanceResult.InstanceDoesNotExist;
|
return StopInstanceResult.InstanceDoesNotExist;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return await instance.Stop(stopStrategy);
|
return await instance.Stop();
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
semaphore.Release();
|
semaphore.Release();
|
||||||
@@ -172,7 +166,6 @@ sealed class InstanceSessionManager : IDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
minecraftServerExecutables.Dispose();
|
|
||||||
shutdownCancellationTokenSource.Dispose();
|
shutdownCancellationTokenSource.Dispose();
|
||||||
semaphore.Dispose();
|
semaphore.Dispose();
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
|
@@ -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(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,7 +53,7 @@ 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);
|
||||||
context.ReportStatus(InstanceStatus.NotRunning);
|
context.ReportStatus(InstanceStatus.IsNotRunning);
|
||||||
return new InstanceNotRunningState();
|
return new InstanceNotRunningState();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -78,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());
|
||||||
}
|
}
|
||||||
@@ -91,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) {
|
||||||
|
@@ -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) {
|
||||||
|
@@ -1,130 +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.ReportStatus(InstanceStatus.Failed(InstanceLaunchFailReason.UnknownError));
|
context.ReportStatus(new InstanceStatus.Failed(InstanceLaunchFailReason.UnknownError));
|
||||||
context.LaunchServices.TaskManager.Run(() => context.TransitionState(new InstanceNotRunningState()));
|
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.ReportStatus(InstanceStatus.NotRunning);
|
context.ReportStatus(InstanceStatus.IsNotRunning);
|
||||||
context.TransitionState(new InstanceNotRunningState());
|
context.TransitionState(new InstanceNotRunningState());
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
context.Logger.Information("Session ended unexpectedly, restarting...");
|
|
||||||
context.ReportStatus(InstanceStatus.Restarting);
|
|
||||||
context.TransitionState(new InstanceLaunchingState(context));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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(() => 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);
|
||||||
@@ -139,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() {
|
||||||
@@ -155,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,5 @@
|
|||||||
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;
|
||||||
|
|
||||||
@@ -15,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(DoStop);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DoStop() {
|
private async Task DoStop() {
|
||||||
@@ -32,7 +27,7 @@ sealed class InstanceStoppingState : IInstanceState, IDisposable {
|
|||||||
await DoWaitForSessionToEnd();
|
await DoWaitForSessionToEnd();
|
||||||
} finally {
|
} finally {
|
||||||
context.Logger.Information("Session stopped.");
|
context.Logger.Information("Session stopped.");
|
||||||
context.ReportStatus(InstanceStatus.NotRunning);
|
context.ReportStatus(InstanceStatus.IsNotRunning);
|
||||||
context.TransitionState(new InstanceNotRunningState());
|
context.TransitionState(new InstanceNotRunningState());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,7 +35,7 @@ sealed class InstanceStoppingState : IInstanceState, IDisposable {
|
|||||||
private async Task DoSendStopCommand() {
|
private async Task DoSendStopCommand() {
|
||||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||||
try {
|
try {
|
||||||
await session.SendCommand(MinecraftCommand.Stop, cts.Token);
|
await session.SendCommand("stop", cts.Token);
|
||||||
} catch (OperationCanceledException) {
|
} catch (OperationCanceledException) {
|
||||||
// ignore
|
// ignore
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -62,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) {
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
using NetMQ.Sockets;
|
using NetMQ.Sockets;
|
||||||
using Phantom.Agent.Rpc;
|
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.ToAgent;
|
using Phantom.Common.Messages.ToAgent;
|
||||||
using Phantom.Common.Messages.ToServer;
|
using Phantom.Common.Messages.ToServer;
|
||||||
@@ -50,6 +49,11 @@ public sealed class MessageListener : IMessageToAgentListener {
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task HandleShutdownAgent(ShutdownAgentMessage message) {
|
||||||
|
shutdownTokenSource.Cancel();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task HandleConfigureInstance(ConfigureInstanceMessage message) {
|
public async Task HandleConfigureInstance(ConfigureInstanceMessage message) {
|
||||||
await socket.SendSimpleReply(message, await agent.InstanceSessionManager.Configure(message.Configuration));
|
await socket.SendSimpleReply(message, await agent.InstanceSessionManager.Configure(message.Configuration));
|
||||||
}
|
}
|
||||||
@@ -59,7 +63,7 @@ public sealed class MessageListener : IMessageToAgentListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task HandleStopInstance(StopInstanceMessage message) {
|
public async Task HandleStopInstance(StopInstanceMessage message) {
|
||||||
await socket.SendSimpleReply(message, await agent.InstanceSessionManager.Stop(message.InstanceGuid, message.StopStrategy));
|
await socket.SendSimpleReply(message, await agent.InstanceSessionManager.Stop(message.InstanceGuid));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task HandleSendCommandToInstance(SendCommandToInstanceMessage message) {
|
public async Task HandleSendCommandToInstance(SendCommandToInstanceMessage message) {
|
||||||
|
@@ -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(typeof(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);
|
|
||||||
}
|
|
||||||
}
|
|
32
Agent/Phantom.Agent/CertificateFile.cs
Normal file
32
Agent/Phantom.Agent/CertificateFile.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@@ -1,6 +1,4 @@
|
|||||||
using System.Reflection;
|
using Phantom.Agent;
|
||||||
using NetMQ.Sockets;
|
|
||||||
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;
|
||||||
@@ -9,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,39 +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(ClientSocket socket) {
|
|
||||||
return new MessageListener(socket, 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 rpcTask = RpcLauncher.Launch(new RpcConfiguration(PhantomLogger.Create("Rpc"), serverHost, serverPort, serverCertificate), 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();
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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>
|
||||||
|
@@ -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;
|
||||||
|
|
||||||
public 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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
|
||||||
);
|
);
|
||||||
|
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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) {
|
||||||
|
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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
|
|
||||||
);
|
);
|
||||||
|
@@ -1,31 +1,25 @@
|
|||||||
namespace Phantom.Common.Data.Instance;
|
namespace Phantom.Common.Data.Instance;
|
||||||
|
|
||||||
public enum InstanceLaunchFailReason {
|
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."
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
63
Common/Phantom.Common.Data/Instance/InstanceStatus.cs
Normal file
63
Common/Phantom.Common.Data/Instance/InstanceStatus.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@@ -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)) {
|
||||||
|
@@ -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
|
||||||
);
|
);
|
||||||
|
@@ -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);
|
|
||||||
}
|
|
@@ -1,7 +0,0 @@
|
|||||||
namespace Phantom.Common.Data.Minecraft;
|
|
||||||
|
|
||||||
public sealed record MinecraftVersion(
|
|
||||||
string Id,
|
|
||||||
MinecraftVersionType Type,
|
|
||||||
string MetadataUrl
|
|
||||||
);
|
|
@@ -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"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@@ -7,7 +7,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MemoryPack" />
|
<PackageReference Include="MessagePack.Annotations" Version="2.4.35" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@@ -1,48 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using MemoryPack;
|
|
||||||
|
|
||||||
namespace Phantom.Common.Data;
|
|
||||||
|
|
||||||
[MemoryPackable]
|
|
||||||
public readonly partial record struct PortRange(
|
|
||||||
[property: MemoryPackOrder(0)] ushort FirstPort,
|
|
||||||
[property: MemoryPackOrder(1)] ushort LastPort
|
|
||||||
) {
|
|
||||||
internal bool Contains(ushort port) {
|
|
||||||
return port >= FirstPort && port <= LastPort;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void ToString(StringBuilder builder) {
|
|
||||||
builder.Append(FirstPort);
|
|
||||||
|
|
||||||
if (LastPort != FirstPort) {
|
|
||||||
builder.Append('-');
|
|
||||||
builder.Append(LastPort);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static PortRange Parse(ReadOnlySpan<char> definition) {
|
|
||||||
int separatorIndex = definition.IndexOf('-');
|
|
||||||
if (separatorIndex == -1) {
|
|
||||||
var port = ParsePort(definition.Trim());
|
|
||||||
return new PortRange(port, port);
|
|
||||||
}
|
|
||||||
|
|
||||||
var firstPort = ParsePort(definition[..separatorIndex].Trim());
|
|
||||||
var lastPort = ParsePort(definition[(separatorIndex + 1)..].Trim());
|
|
||||||
if (lastPort < firstPort) {
|
|
||||||
throw new FormatException("Invalid port range '" + firstPort + "-" + lastPort + "'.");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return new PortRange(firstPort, lastPort);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ushort ParsePort(ReadOnlySpan<char> port) {
|
|
||||||
try {
|
|
||||||
return ushort.Parse(port);
|
|
||||||
} catch (Exception) {
|
|
||||||
throw new FormatException("Invalid port '" + port.ToString() + "'.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,17 +1,17 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using 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) {
|
||||||
|
@@ -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>
|
||||||
|
@@ -9,4 +9,5 @@ public interface IMessageToAgentListener {
|
|||||||
Task HandleLaunchInstance(LaunchInstanceMessage message);
|
Task HandleLaunchInstance(LaunchInstanceMessage message);
|
||||||
Task HandleStopInstance(StopInstanceMessage message);
|
Task HandleStopInstance(StopInstanceMessage message);
|
||||||
Task HandleSendCommandToInstance(SendCommandToInstanceMessage message);
|
Task HandleSendCommandToInstance(SendCommandToInstanceMessage message);
|
||||||
|
Task HandleShutdownAgent(ShutdownAgentMessage message);
|
||||||
}
|
}
|
||||||
|
@@ -16,6 +16,7 @@ public static class MessageRegistries {
|
|||||||
ToAgent.Add<LaunchInstanceMessage>(3);
|
ToAgent.Add<LaunchInstanceMessage>(3);
|
||||||
ToAgent.Add<StopInstanceMessage>(4);
|
ToAgent.Add<StopInstanceMessage>(4);
|
||||||
ToAgent.Add<SendCommandToInstanceMessage>(5);
|
ToAgent.Add<SendCommandToInstanceMessage>(5);
|
||||||
|
ToAgent.Add<ShutdownAgentMessage>(6);
|
||||||
|
|
||||||
ToServer.Add<RegisterAgentMessage>(0);
|
ToServer.Add<RegisterAgentMessage>(0);
|
||||||
ToServer.Add<UnregisterAgentMessage>(1);
|
ToServer.Add<UnregisterAgentMessage>(1);
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
using MemoryPack;
|
using MessagePack;
|
||||||
using Phantom.Common.Data.Instance;
|
using Phantom.Common.Data.Instance;
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.ToAgent;
|
namespace Phantom.Common.Messages.ToAgent;
|
||||||
|
|
||||||
[MemoryPackable]
|
[MessagePackObject]
|
||||||
public sealed partial record ConfigureInstanceMessage(
|
public sealed record ConfigureInstanceMessage(
|
||||||
[property: MemoryPackOrder(0)] uint SequenceId,
|
[property: Key(0)] uint SequenceId,
|
||||||
[property: MemoryPackOrder(1)] InstanceConfiguration Configuration
|
[property: Key(1)] InstanceConfiguration Configuration
|
||||||
) : IMessageToAgent, IMessageWithReply {
|
) : IMessageToAgent, IMessageWithReply {
|
||||||
public Task Accept(IMessageToAgentListener listener) {
|
public Task Accept(IMessageToAgentListener listener) {
|
||||||
return listener.HandleConfigureInstance(this);
|
return listener.HandleConfigureInstance(this);
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
using MemoryPack;
|
using MessagePack;
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.ToAgent;
|
namespace Phantom.Common.Messages.ToAgent;
|
||||||
|
|
||||||
[MemoryPackable]
|
[MessagePackObject]
|
||||||
public sealed partial record LaunchInstanceMessage(
|
public sealed record LaunchInstanceMessage(
|
||||||
[property: MemoryPackOrder(0)] uint SequenceId,
|
[property: Key(0)] uint SequenceId,
|
||||||
[property: MemoryPackOrder(1)] Guid InstanceGuid
|
[property: Key(1)] Guid InstanceGuid
|
||||||
) : IMessageToAgent, IMessageWithReply {
|
) : IMessageToAgent, IMessageWithReply {
|
||||||
public Task Accept(IMessageToAgentListener listener) {
|
public Task Accept(IMessageToAgentListener listener) {
|
||||||
return listener.HandleLaunchInstance(this);
|
return listener.HandleLaunchInstance(this);
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
using MemoryPack;
|
using MessagePack;
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
|
|
||||||
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 Accept(IMessageToAgentListener listener) {
|
public Task Accept(IMessageToAgentListener listener) {
|
||||||
return listener.HandleRegisterAgentFailureResult(this);
|
return listener.HandleRegisterAgentFailureResult(this);
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using MemoryPack;
|
using MessagePack;
|
||||||
using Phantom.Common.Data.Instance;
|
using Phantom.Common.Data.Instance;
|
||||||
|
|
||||||
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 Accept(IMessageToAgentListener listener) {
|
public Task Accept(IMessageToAgentListener listener) {
|
||||||
return listener.HandleRegisterAgentSuccessResult(this);
|
return listener.HandleRegisterAgentSuccessResult(this);
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
using MemoryPack;
|
using MessagePack;
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.ToAgent;
|
namespace Phantom.Common.Messages.ToAgent;
|
||||||
|
|
||||||
[MemoryPackable]
|
[MessagePackObject]
|
||||||
public sealed partial record SendCommandToInstanceMessage(
|
public sealed record SendCommandToInstanceMessage(
|
||||||
[property: MemoryPackOrder(0)] uint SequenceId,
|
[property: Key(0)] uint SequenceId,
|
||||||
[property: MemoryPackOrder(1)] Guid InstanceGuid,
|
[property: Key(1)] Guid InstanceGuid,
|
||||||
[property: MemoryPackOrder(2)] string Command
|
[property: Key(2)] string Command
|
||||||
) : IMessageToAgent, IMessageWithReply {
|
) : IMessageToAgent, IMessageWithReply {
|
||||||
public Task Accept(IMessageToAgentListener listener) {
|
public Task Accept(IMessageToAgentListener listener) {
|
||||||
return listener.HandleSendCommandToInstance(this);
|
return listener.HandleSendCommandToInstance(this);
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,13 +1,11 @@
|
|||||||
using MemoryPack;
|
using MessagePack;
|
||||||
using Phantom.Common.Data.Minecraft;
|
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.ToAgent;
|
namespace Phantom.Common.Messages.ToAgent;
|
||||||
|
|
||||||
[MemoryPackable]
|
[MessagePackObject]
|
||||||
public sealed partial record StopInstanceMessage(
|
public sealed record StopInstanceMessage(
|
||||||
[property: MemoryPackOrder(0)] uint SequenceId,
|
[property: Key(0)] uint SequenceId,
|
||||||
[property: MemoryPackOrder(1)] Guid InstanceGuid,
|
[property: Key(1)] Guid InstanceGuid
|
||||||
[property: MemoryPackOrder(2)] MinecraftStopStrategy StopStrategy
|
|
||||||
) : IMessageToAgent, IMessageWithReply {
|
) : IMessageToAgent, IMessageWithReply {
|
||||||
public Task Accept(IMessageToAgentListener listener) {
|
public Task Accept(IMessageToAgentListener listener) {
|
||||||
return listener.HandleStopInstance(this);
|
return listener.HandleStopInstance(this);
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using MemoryPack;
|
using MessagePack;
|
||||||
using Phantom.Common.Data.Java;
|
using Phantom.Common.Data.Java;
|
||||||
|
|
||||||
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 Accept(IMessageToServerListener listener) {
|
public Task Accept(IMessageToServerListener listener) {
|
||||||
return listener.HandleAdvertiseJavaRuntimes(this);
|
return listener.HandleAdvertiseJavaRuntimes(this);
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
using MemoryPack;
|
using MessagePack;
|
||||||
|
|
||||||
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 Accept(IMessageToServerListener listener) {
|
public Task Accept(IMessageToServerListener listener) {
|
||||||
return listener.HandleAgentIsAlive(this);
|
return listener.HandleAgentIsAlive(this);
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using MemoryPack;
|
using MessagePack;
|
||||||
|
|
||||||
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 Accept(IMessageToServerListener listener) {
|
public Task Accept(IMessageToServerListener listener) {
|
||||||
return listener.HandleInstanceOutput(this);
|
return listener.HandleInstanceOutput(this);
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
using MemoryPack;
|
using MessagePack;
|
||||||
using Phantom.Common.Data.Agent;
|
using Phantom.Common.Data.Agent;
|
||||||
|
|
||||||
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 Accept(IMessageToServerListener listener) {
|
public Task Accept(IMessageToServerListener listener) {
|
||||||
return listener.HandleRegisterAgent(this);
|
return listener.HandleRegisterAgent(this);
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
using MemoryPack;
|
using MessagePack;
|
||||||
using Phantom.Common.Data.Instance;
|
using Phantom.Common.Data.Instance;
|
||||||
|
|
||||||
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 Accept(IMessageToServerListener listener) {
|
public Task Accept(IMessageToServerListener listener) {
|
||||||
return listener.HandleReportInstanceStatus(this);
|
return listener.HandleReportInstanceStatus(this);
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using MemoryPack;
|
using MessagePack;
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.ToServer;
|
namespace Phantom.Common.Messages.ToServer;
|
||||||
|
|
||||||
[MemoryPackable]
|
[MessagePackObject]
|
||||||
public sealed partial record SimpleReplyMessage(
|
public sealed record SimpleReplyMessage(
|
||||||
[property: MemoryPackOrder(0)] uint SequenceId,
|
[property: Key(0)] uint SequenceId,
|
||||||
[property: MemoryPackOrder(1)] int EnumValue
|
[property: Key(1)] int EnumValue
|
||||||
) : IMessageToServer {
|
) : IMessageToServer {
|
||||||
public static SimpleReplyMessage FromEnum<TEnum>(uint sequenceId, TEnum enumValue) where TEnum : Enum {
|
public static SimpleReplyMessage FromEnum<TEnum>(uint sequenceId, TEnum enumValue) where TEnum : Enum {
|
||||||
if (Unsafe.SizeOf<TEnum>() != Unsafe.SizeOf<int>()) {
|
if (Unsafe.SizeOf<TEnum>() != Unsafe.SizeOf<int>()) {
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
using MemoryPack;
|
using MessagePack;
|
||||||
|
|
||||||
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 Accept(IMessageToServerListener listener) {
|
public Task Accept(IMessageToServerListener listener) {
|
||||||
return listener.HandleUnregisterAgent(this);
|
return listener.HandleUnregisterAgent(this);
|
||||||
|
@@ -1,49 +0,0 @@
|
|||||||
using System.Collections.Immutable;
|
|
||||||
|
|
||||||
namespace Phantom.Common.Minecraft;
|
|
||||||
|
|
||||||
public static class JvmArgumentsHelper {
|
|
||||||
public static ImmutableArray<string> Split(string arguments) {
|
|
||||||
return arguments.Split('\n', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToImmutableArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ValidationError? Validate(string arguments) {
|
|
||||||
return Validate(Split(arguments));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ValidationError? Validate(ImmutableArray<string> arguments) {
|
|
||||||
if (!arguments.All(static argument => argument.StartsWith('-'))) {
|
|
||||||
return ValidationError.InvalidFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO not perfect, but good enough
|
|
||||||
if (arguments.Any(static argument => argument.Contains("-Xmx"))) {
|
|
||||||
return ValidationError.XmxNotAllowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (arguments.Any(static argument => argument.Contains("-Xms"))) {
|
|
||||||
return ValidationError.XmsNotAllowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string Join(ImmutableArray<string> arguments) {
|
|
||||||
return string.Join('\n', arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ValidationError {
|
|
||||||
InvalidFormat,
|
|
||||||
XmxNotAllowed,
|
|
||||||
XmsNotAllowed
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string ToSentence(this ValidationError? result) {
|
|
||||||
return result switch {
|
|
||||||
ValidationError.InvalidFormat => "Invalid format.",
|
|
||||||
ValidationError.XmxNotAllowed => "The -Xmx argument must not be specified manually.",
|
|
||||||
ValidationError.XmsNotAllowed => "The -Xms argument must not be specified manually.",
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(result), result, null)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,201 +0,0 @@
|
|||||||
using System.Collections.Immutable;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Net.Http.Json;
|
|
||||||
using System.Text.Json;
|
|
||||||
using Phantom.Common.Data.Minecraft;
|
|
||||||
using Phantom.Common.Logging;
|
|
||||||
using Phantom.Utils.Cryptography;
|
|
||||||
using Phantom.Utils.IO;
|
|
||||||
using Phantom.Utils.Runtime;
|
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
namespace Phantom.Common.Minecraft;
|
|
||||||
|
|
||||||
public sealed class MinecraftVersions : IDisposable {
|
|
||||||
private static readonly ILogger Logger = PhantomLogger.Create<MinecraftVersions>();
|
|
||||||
private static readonly TimeSpan CacheRetentionTime = TimeSpan.FromMinutes(10);
|
|
||||||
|
|
||||||
private const string VersionManifestUrl = "https://launchermeta.mojang.com/mc/game/version_manifest.json";
|
|
||||||
|
|
||||||
private readonly HttpClient http = new ();
|
|
||||||
|
|
||||||
private readonly Stopwatch cacheTimer = new ();
|
|
||||||
private readonly SemaphoreSlim cachedVersionsSemaphore = new (1, 1);
|
|
||||||
|
|
||||||
private ImmutableArray<MinecraftVersion>? cachedVersions;
|
|
||||||
private ImmutableArray<MinecraftVersion>? CachedVersionsUnlessExpired => cacheTimer.IsRunning && cacheTimer.Elapsed < CacheRetentionTime ? cachedVersions : null;
|
|
||||||
|
|
||||||
public void Dispose() {
|
|
||||||
http.Dispose();
|
|
||||||
cachedVersionsSemaphore.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ImmutableArray<MinecraftVersion>> GetVersions(CancellationToken cancellationToken) {
|
|
||||||
if (CachedVersionsUnlessExpired is {} earlyResult) {
|
|
||||||
return earlyResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await cachedVersionsSemaphore.WaitAsync(cancellationToken);
|
|
||||||
} catch (OperationCanceledException) {
|
|
||||||
return ImmutableArray<MinecraftVersion>.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (CachedVersionsUnlessExpired is {} racedResult) {
|
|
||||||
return racedResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImmutableArray<MinecraftVersion> versions = await FetchVersions(cancellationToken) ?? ImmutableArray<MinecraftVersion>.Empty;
|
|
||||||
Logger.Information("Refreshed Minecraft version cache, {Versions} version(s) found.", versions.Length);
|
|
||||||
|
|
||||||
cachedVersions = versions;
|
|
||||||
cacheTimer.Restart();
|
|
||||||
return versions;
|
|
||||||
} finally {
|
|
||||||
cachedVersionsSemaphore.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<ImmutableArray<MinecraftVersion>?> FetchVersions(CancellationToken cancellationToken) {
|
|
||||||
return await FetchOrFailSilently(async () => {
|
|
||||||
var versionManifest = await FetchJson(http, VersionManifestUrl, "version manifest", cancellationToken);
|
|
||||||
return GetVersionsFromManifest(versionManifest);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<MinecraftServerExecutableInfo?> GetServerExecutableInfo(string version, CancellationToken cancellationToken) {
|
|
||||||
return await FetchOrFailSilently(async () => {
|
|
||||||
var versions = await GetVersions(cancellationToken);
|
|
||||||
var versionObject = versions.FirstOrDefault(v => v.Id == version);
|
|
||||||
if (versionObject == null) {
|
|
||||||
Logger.Error("Version {Version} was not found in version manifest.", version);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var versionMetadata = await FetchJson(http, versionObject.MetadataUrl, "version metadata", cancellationToken);
|
|
||||||
return GetServerExecutableInfoFromMetadata(versionMetadata);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<T?> FetchOrFailSilently<T>(Func<Task<T?>> task) {
|
|
||||||
try {
|
|
||||||
return await task();
|
|
||||||
} catch (OperationCanceledException) {
|
|
||||||
return default;
|
|
||||||
} catch (StopProcedureException) {
|
|
||||||
return default;
|
|
||||||
} catch (Exception e) {
|
|
||||||
Logger.Error(e, "An unexpected error occurred.");
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<JsonElement> FetchJson(HttpClient http, string url, string description, CancellationToken cancellationToken) {
|
|
||||||
Logger.Information("Fetching {Description} JSON from: {Url}", description, url);
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await http.GetFromJsonAsync<JsonElement>(url, cancellationToken);
|
|
||||||
} catch (OperationCanceledException) {
|
|
||||||
throw StopProcedureException.Instance;
|
|
||||||
} catch (HttpRequestException e) {
|
|
||||||
Logger.Error(e, "Unable to download {Description}.", description);
|
|
||||||
throw StopProcedureException.Instance;
|
|
||||||
} catch (Exception e) {
|
|
||||||
Logger.Error(e, "Unable to parse {Description} as JSON.", description);
|
|
||||||
throw StopProcedureException.Instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ImmutableArray<MinecraftVersion> GetVersionsFromManifest(JsonElement versionManifest) {
|
|
||||||
JsonElement versionsElement = GetJsonPropertyOrThrow(versionManifest, "versions", JsonValueKind.Array, "version manifest");
|
|
||||||
var foundVersions = ImmutableArray.CreateBuilder<MinecraftVersion>(versionsElement.GetArrayLength());
|
|
||||||
|
|
||||||
foreach (var versionElement in versionsElement.EnumerateArray()) {
|
|
||||||
try {
|
|
||||||
foundVersions.Add(GetVersionFromManifestEntry(versionElement));
|
|
||||||
} catch (StopProcedureException) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return foundVersions.ToImmutable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MinecraftVersion GetVersionFromManifestEntry(JsonElement versionElement) {
|
|
||||||
JsonElement idElement = GetJsonPropertyOrThrow(versionElement, "id", JsonValueKind.String, "version entry in version manifest");
|
|
||||||
string id = idElement.GetString() ?? throw new InvalidOperationException();
|
|
||||||
|
|
||||||
JsonElement typeElement = GetJsonPropertyOrThrow(versionElement, "type", JsonValueKind.String, "version entry in version manifest");
|
|
||||||
string? typeString = typeElement.GetString();
|
|
||||||
|
|
||||||
var type = MinecraftVersionTypes.FromString(typeString);
|
|
||||||
if (type == MinecraftVersionType.Other) {
|
|
||||||
Logger.Verbose("Unknown version type: {Type} ({Version})", typeString, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 an accepted URL: {Url}", url);
|
|
||||||
throw StopProcedureException.Instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new MinecraftVersion(id, type, url);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MinecraftServerExecutableInfo GetServerExecutableInfoFromMetadata(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 MinecraftServerExecutableInfo(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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,16 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\Utils\Phantom.Utils.IO\Phantom.Utils.IO.csproj" />
|
|
||||||
<ProjectReference Include="..\..\Utils\Phantom.Utils.Runtime\Phantom.Utils.Runtime.csproj" />
|
|
||||||
<ProjectReference Include="..\Phantom.Common.Data\Phantom.Common.Data.csproj" />
|
|
||||||
<ProjectReference Include="..\Phantom.Common.Logging\Phantom.Common.Logging.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@@ -1,11 +0,0 @@
|
|||||||
<Project>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<LangVersion>11</LangVersion>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<Version>0.0.1</Version>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@@ -1,11 +0,0 @@
|
|||||||
<Project>
|
|
||||||
|
|
||||||
<Sdk Name="Microsoft.Build.CentralPackageVersions" Version="2.1.3" />
|
|
||||||
|
|
||||||
<Target Name="SetSourceRevisionId" BeforeTargets="InitializeSourceControlInformation">
|
|
||||||
<Exec Command="git describe --always --abbrev=8" ConsoleToMSBuild="True" IgnoreExitCode="False">
|
|
||||||
<Output PropertyName="SourceRevisionId" TaskParameter="ConsoleOutput" />
|
|
||||||
</Exec>
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
</Project>
|
|
78
Dockerfile
78
Dockerfile
@@ -1,78 +0,0 @@
|
|||||||
# +---------------------------+
|
|
||||||
# | Prepare build environment |
|
|
||||||
# +---------------------------+
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS phantom-base-builder
|
|
||||||
|
|
||||||
ADD . /app
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
RUN dotnet restore
|
|
||||||
|
|
||||||
|
|
||||||
# +---------------------+
|
|
||||||
# | Build Phantom Agent |
|
|
||||||
# +---------------------+
|
|
||||||
FROM phantom-base-builder AS phantom-agent-builder
|
|
||||||
|
|
||||||
RUN dotnet publish Agent/Phantom.Agent/Phantom.Agent.csproj -c Release -o /app/out
|
|
||||||
|
|
||||||
|
|
||||||
# +----------------------+
|
|
||||||
# | Build Phantom Server |
|
|
||||||
# +----------------------+
|
|
||||||
FROM phantom-base-builder AS phantom-server-builder
|
|
||||||
|
|
||||||
RUN dotnet publish Server/Phantom.Server.Web/Phantom.Server.Web.csproj -c Release -o /app/out
|
|
||||||
RUN dotnet publish Server/Phantom.Server/Phantom.Server.csproj -c Release -o /app/out
|
|
||||||
|
|
||||||
|
|
||||||
# +------------------------------+
|
|
||||||
# | Download older Java versions |
|
|
||||||
# +------------------------------+
|
|
||||||
FROM ubuntu:focal AS java-legacy
|
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y \
|
|
||||||
openjdk-8-jre-headless \
|
|
||||||
openjdk-16-jre-headless \
|
|
||||||
openjdk-17-jre-headless
|
|
||||||
|
|
||||||
|
|
||||||
# +------------------------------+
|
|
||||||
# | Finalize Phantom Agent image |
|
|
||||||
# +------------------------------+
|
|
||||||
FROM mcr.microsoft.com/dotnet/runtime:7.0-jammy AS phantom-agent
|
|
||||||
|
|
||||||
COPY --from=java-legacy /usr/lib/jvm/java-8-openjdk-amd64 /usr/lib/jvm/java-8-openjdk-amd64
|
|
||||||
COPY --from=java-legacy /usr/lib/jvm/java-16-openjdk-amd64 /usr/lib/jvm/java-16-openjdk-amd64
|
|
||||||
COPY --from=java-legacy /usr/lib/jvm/java-17-openjdk-amd64 /usr/lib/jvm/java-17-openjdk-amd64
|
|
||||||
|
|
||||||
COPY --from=phantom-agent-builder --chmod=755 /app/out /app
|
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y \
|
|
||||||
openjdk-18-jre-headless
|
|
||||||
|
|
||||||
RUN mkdir /data
|
|
||||||
RUN chmod 777 /data
|
|
||||||
WORKDIR /data
|
|
||||||
|
|
||||||
ENTRYPOINT ["dotnet", "/app/Phantom.Agent.dll"]
|
|
||||||
|
|
||||||
|
|
||||||
# +-------------------------------+
|
|
||||||
# | Finalize Phantom Server image |
|
|
||||||
# +-------------------------------+
|
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS phantom-server
|
|
||||||
|
|
||||||
COPY --from=phantom-server-builder --chmod=755 /app/out /app
|
|
||||||
|
|
||||||
RUN mkdir /data
|
|
||||||
RUN chmod 777 /data
|
|
||||||
WORKDIR /data
|
|
||||||
|
|
||||||
ENTRYPOINT ["dotnet", "/app/Phantom.Server.dll"]
|
|
373
LICENSE
373
LICENSE
@@ -1,373 +0,0 @@
|
|||||||
Mozilla Public License Version 2.0
|
|
||||||
==================================
|
|
||||||
|
|
||||||
1. Definitions
|
|
||||||
--------------
|
|
||||||
|
|
||||||
1.1. "Contributor"
|
|
||||||
means each individual or legal entity that creates, contributes to
|
|
||||||
the creation of, or owns Covered Software.
|
|
||||||
|
|
||||||
1.2. "Contributor Version"
|
|
||||||
means the combination of the Contributions of others (if any) used
|
|
||||||
by a Contributor and that particular Contributor's Contribution.
|
|
||||||
|
|
||||||
1.3. "Contribution"
|
|
||||||
means Covered Software of a particular Contributor.
|
|
||||||
|
|
||||||
1.4. "Covered Software"
|
|
||||||
means Source Code Form to which the initial Contributor has attached
|
|
||||||
the notice in Exhibit A, the Executable Form of such Source Code
|
|
||||||
Form, and Modifications of such Source Code Form, in each case
|
|
||||||
including portions thereof.
|
|
||||||
|
|
||||||
1.5. "Incompatible With Secondary Licenses"
|
|
||||||
means
|
|
||||||
|
|
||||||
(a) that the initial Contributor has attached the notice described
|
|
||||||
in Exhibit B to the Covered Software; or
|
|
||||||
|
|
||||||
(b) that the Covered Software was made available under the terms of
|
|
||||||
version 1.1 or earlier of the License, but not also under the
|
|
||||||
terms of a Secondary License.
|
|
||||||
|
|
||||||
1.6. "Executable Form"
|
|
||||||
means any form of the work other than Source Code Form.
|
|
||||||
|
|
||||||
1.7. "Larger Work"
|
|
||||||
means a work that combines Covered Software with other material, in
|
|
||||||
a separate file or files, that is not Covered Software.
|
|
||||||
|
|
||||||
1.8. "License"
|
|
||||||
means this document.
|
|
||||||
|
|
||||||
1.9. "Licensable"
|
|
||||||
means having the right to grant, to the maximum extent possible,
|
|
||||||
whether at the time of the initial grant or subsequently, any and
|
|
||||||
all of the rights conveyed by this License.
|
|
||||||
|
|
||||||
1.10. "Modifications"
|
|
||||||
means any of the following:
|
|
||||||
|
|
||||||
(a) any file in Source Code Form that results from an addition to,
|
|
||||||
deletion from, or modification of the contents of Covered
|
|
||||||
Software; or
|
|
||||||
|
|
||||||
(b) any new file in Source Code Form that contains any Covered
|
|
||||||
Software.
|
|
||||||
|
|
||||||
1.11. "Patent Claims" of a Contributor
|
|
||||||
means any patent claim(s), including without limitation, method,
|
|
||||||
process, and apparatus claims, in any patent Licensable by such
|
|
||||||
Contributor that would be infringed, but for the grant of the
|
|
||||||
License, by the making, using, selling, offering for sale, having
|
|
||||||
made, import, or transfer of either its Contributions or its
|
|
||||||
Contributor Version.
|
|
||||||
|
|
||||||
1.12. "Secondary License"
|
|
||||||
means either the GNU General Public License, Version 2.0, the GNU
|
|
||||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
|
||||||
Public License, Version 3.0, or any later versions of those
|
|
||||||
licenses.
|
|
||||||
|
|
||||||
1.13. "Source Code Form"
|
|
||||||
means the form of the work preferred for making modifications.
|
|
||||||
|
|
||||||
1.14. "You" (or "Your")
|
|
||||||
means an individual or a legal entity exercising rights under this
|
|
||||||
License. For legal entities, "You" includes any entity that
|
|
||||||
controls, is controlled by, or is under common control with You. For
|
|
||||||
purposes of this definition, "control" means (a) the power, direct
|
|
||||||
or indirect, to cause the direction or management of such entity,
|
|
||||||
whether by contract or otherwise, or (b) ownership of more than
|
|
||||||
fifty percent (50%) of the outstanding shares or beneficial
|
|
||||||
ownership of such entity.
|
|
||||||
|
|
||||||
2. License Grants and Conditions
|
|
||||||
--------------------------------
|
|
||||||
|
|
||||||
2.1. Grants
|
|
||||||
|
|
||||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
|
||||||
non-exclusive license:
|
|
||||||
|
|
||||||
(a) under intellectual property rights (other than patent or trademark)
|
|
||||||
Licensable by such Contributor to use, reproduce, make available,
|
|
||||||
modify, display, perform, distribute, and otherwise exploit its
|
|
||||||
Contributions, either on an unmodified basis, with Modifications, or
|
|
||||||
as part of a Larger Work; and
|
|
||||||
|
|
||||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
|
||||||
for sale, have made, import, and otherwise transfer either its
|
|
||||||
Contributions or its Contributor Version.
|
|
||||||
|
|
||||||
2.2. Effective Date
|
|
||||||
|
|
||||||
The licenses granted in Section 2.1 with respect to any Contribution
|
|
||||||
become effective for each Contribution on the date the Contributor first
|
|
||||||
distributes such Contribution.
|
|
||||||
|
|
||||||
2.3. Limitations on Grant Scope
|
|
||||||
|
|
||||||
The licenses granted in this Section 2 are the only rights granted under
|
|
||||||
this License. No additional rights or licenses will be implied from the
|
|
||||||
distribution or licensing of Covered Software under this License.
|
|
||||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
|
||||||
Contributor:
|
|
||||||
|
|
||||||
(a) for any code that a Contributor has removed from Covered Software;
|
|
||||||
or
|
|
||||||
|
|
||||||
(b) for infringements caused by: (i) Your and any other third party's
|
|
||||||
modifications of Covered Software, or (ii) the combination of its
|
|
||||||
Contributions with other software (except as part of its Contributor
|
|
||||||
Version); or
|
|
||||||
|
|
||||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
|
||||||
its Contributions.
|
|
||||||
|
|
||||||
This License does not grant any rights in the trademarks, service marks,
|
|
||||||
or logos of any Contributor (except as may be necessary to comply with
|
|
||||||
the notice requirements in Section 3.4).
|
|
||||||
|
|
||||||
2.4. Subsequent Licenses
|
|
||||||
|
|
||||||
No Contributor makes additional grants as a result of Your choice to
|
|
||||||
distribute the Covered Software under a subsequent version of this
|
|
||||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
|
||||||
permitted under the terms of Section 3.3).
|
|
||||||
|
|
||||||
2.5. Representation
|
|
||||||
|
|
||||||
Each Contributor represents that the Contributor believes its
|
|
||||||
Contributions are its original creation(s) or it has sufficient rights
|
|
||||||
to grant the rights to its Contributions conveyed by this License.
|
|
||||||
|
|
||||||
2.6. Fair Use
|
|
||||||
|
|
||||||
This License is not intended to limit any rights You have under
|
|
||||||
applicable copyright doctrines of fair use, fair dealing, or other
|
|
||||||
equivalents.
|
|
||||||
|
|
||||||
2.7. Conditions
|
|
||||||
|
|
||||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
|
||||||
in Section 2.1.
|
|
||||||
|
|
||||||
3. Responsibilities
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
3.1. Distribution of Source Form
|
|
||||||
|
|
||||||
All distribution of Covered Software in Source Code Form, including any
|
|
||||||
Modifications that You create or to which You contribute, must be under
|
|
||||||
the terms of this License. You must inform recipients that the Source
|
|
||||||
Code Form of the Covered Software is governed by the terms of this
|
|
||||||
License, and how they can obtain a copy of this License. You may not
|
|
||||||
attempt to alter or restrict the recipients' rights in the Source Code
|
|
||||||
Form.
|
|
||||||
|
|
||||||
3.2. Distribution of Executable Form
|
|
||||||
|
|
||||||
If You distribute Covered Software in Executable Form then:
|
|
||||||
|
|
||||||
(a) such Covered Software must also be made available in Source Code
|
|
||||||
Form, as described in Section 3.1, and You must inform recipients of
|
|
||||||
the Executable Form how they can obtain a copy of such Source Code
|
|
||||||
Form by reasonable means in a timely manner, at a charge no more
|
|
||||||
than the cost of distribution to the recipient; and
|
|
||||||
|
|
||||||
(b) You may distribute such Executable Form under the terms of this
|
|
||||||
License, or sublicense it under different terms, provided that the
|
|
||||||
license for the Executable Form does not attempt to limit or alter
|
|
||||||
the recipients' rights in the Source Code Form under this License.
|
|
||||||
|
|
||||||
3.3. Distribution of a Larger Work
|
|
||||||
|
|
||||||
You may create and distribute a Larger Work under terms of Your choice,
|
|
||||||
provided that You also comply with the requirements of this License for
|
|
||||||
the Covered Software. If the Larger Work is a combination of Covered
|
|
||||||
Software with a work governed by one or more Secondary Licenses, and the
|
|
||||||
Covered Software is not Incompatible With Secondary Licenses, this
|
|
||||||
License permits You to additionally distribute such Covered Software
|
|
||||||
under the terms of such Secondary License(s), so that the recipient of
|
|
||||||
the Larger Work may, at their option, further distribute the Covered
|
|
||||||
Software under the terms of either this License or such Secondary
|
|
||||||
License(s).
|
|
||||||
|
|
||||||
3.4. Notices
|
|
||||||
|
|
||||||
You may not remove or alter the substance of any license notices
|
|
||||||
(including copyright notices, patent notices, disclaimers of warranty,
|
|
||||||
or limitations of liability) contained within the Source Code Form of
|
|
||||||
the Covered Software, except that You may alter any license notices to
|
|
||||||
the extent required to remedy known factual inaccuracies.
|
|
||||||
|
|
||||||
3.5. Application of Additional Terms
|
|
||||||
|
|
||||||
You may choose to offer, and to charge a fee for, warranty, support,
|
|
||||||
indemnity or liability obligations to one or more recipients of Covered
|
|
||||||
Software. However, You may do so only on Your own behalf, and not on
|
|
||||||
behalf of any Contributor. You must make it absolutely clear that any
|
|
||||||
such warranty, support, indemnity, or liability obligation is offered by
|
|
||||||
You alone, and You hereby agree to indemnify every Contributor for any
|
|
||||||
liability incurred by such Contributor as a result of warranty, support,
|
|
||||||
indemnity or liability terms You offer. You may include additional
|
|
||||||
disclaimers of warranty and limitations of liability specific to any
|
|
||||||
jurisdiction.
|
|
||||||
|
|
||||||
4. Inability to Comply Due to Statute or Regulation
|
|
||||||
---------------------------------------------------
|
|
||||||
|
|
||||||
If it is impossible for You to comply with any of the terms of this
|
|
||||||
License with respect to some or all of the Covered Software due to
|
|
||||||
statute, judicial order, or regulation then You must: (a) comply with
|
|
||||||
the terms of this License to the maximum extent possible; and (b)
|
|
||||||
describe the limitations and the code they affect. Such description must
|
|
||||||
be placed in a text file included with all distributions of the Covered
|
|
||||||
Software under this License. Except to the extent prohibited by statute
|
|
||||||
or regulation, such description must be sufficiently detailed for a
|
|
||||||
recipient of ordinary skill to be able to understand it.
|
|
||||||
|
|
||||||
5. Termination
|
|
||||||
--------------
|
|
||||||
|
|
||||||
5.1. The rights granted under this License will terminate automatically
|
|
||||||
if You fail to comply with any of its terms. However, if You become
|
|
||||||
compliant, then the rights granted under this License from a particular
|
|
||||||
Contributor are reinstated (a) provisionally, unless and until such
|
|
||||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
|
||||||
ongoing basis, if such Contributor fails to notify You of the
|
|
||||||
non-compliance by some reasonable means prior to 60 days after You have
|
|
||||||
come back into compliance. Moreover, Your grants from a particular
|
|
||||||
Contributor are reinstated on an ongoing basis if such Contributor
|
|
||||||
notifies You of the non-compliance by some reasonable means, this is the
|
|
||||||
first time You have received notice of non-compliance with this License
|
|
||||||
from such Contributor, and You become compliant prior to 30 days after
|
|
||||||
Your receipt of the notice.
|
|
||||||
|
|
||||||
5.2. If You initiate litigation against any entity by asserting a patent
|
|
||||||
infringement claim (excluding declaratory judgment actions,
|
|
||||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
|
||||||
directly or indirectly infringes any patent, then the rights granted to
|
|
||||||
You by any and all Contributors for the Covered Software under Section
|
|
||||||
2.1 of this License shall terminate.
|
|
||||||
|
|
||||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
|
||||||
end user license agreements (excluding distributors and resellers) which
|
|
||||||
have been validly granted by You or Your distributors under this License
|
|
||||||
prior to termination shall survive termination.
|
|
||||||
|
|
||||||
************************************************************************
|
|
||||||
* *
|
|
||||||
* 6. Disclaimer of Warranty *
|
|
||||||
* ------------------------- *
|
|
||||||
* *
|
|
||||||
* Covered Software is provided under this License on an "as is" *
|
|
||||||
* basis, without warranty of any kind, either expressed, implied, or *
|
|
||||||
* statutory, including, without limitation, warranties that the *
|
|
||||||
* Covered Software is free of defects, merchantable, fit for a *
|
|
||||||
* particular purpose or non-infringing. The entire risk as to the *
|
|
||||||
* quality and performance of the Covered Software is with You. *
|
|
||||||
* Should any Covered Software prove defective in any respect, You *
|
|
||||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
|
||||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
|
||||||
* essential part of this License. No use of any Covered Software is *
|
|
||||||
* authorized under this License except under this disclaimer. *
|
|
||||||
* *
|
|
||||||
************************************************************************
|
|
||||||
|
|
||||||
************************************************************************
|
|
||||||
* *
|
|
||||||
* 7. Limitation of Liability *
|
|
||||||
* -------------------------- *
|
|
||||||
* *
|
|
||||||
* Under no circumstances and under no legal theory, whether tort *
|
|
||||||
* (including negligence), contract, or otherwise, shall any *
|
|
||||||
* Contributor, or anyone who distributes Covered Software as *
|
|
||||||
* permitted above, be liable to You for any direct, indirect, *
|
|
||||||
* special, incidental, or consequential damages of any character *
|
|
||||||
* including, without limitation, damages for lost profits, loss of *
|
|
||||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
|
||||||
* and all other commercial damages or losses, even if such party *
|
|
||||||
* shall have been informed of the possibility of such damages. This *
|
|
||||||
* limitation of liability shall not apply to liability for death or *
|
|
||||||
* personal injury resulting from such party's negligence to the *
|
|
||||||
* extent applicable law prohibits such limitation. Some *
|
|
||||||
* jurisdictions do not allow the exclusion or limitation of *
|
|
||||||
* incidental or consequential damages, so this exclusion and *
|
|
||||||
* limitation may not apply to You. *
|
|
||||||
* *
|
|
||||||
************************************************************************
|
|
||||||
|
|
||||||
8. Litigation
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Any litigation relating to this License may be brought only in the
|
|
||||||
courts of a jurisdiction where the defendant maintains its principal
|
|
||||||
place of business and such litigation shall be governed by laws of that
|
|
||||||
jurisdiction, without reference to its conflict-of-law provisions.
|
|
||||||
Nothing in this Section shall prevent a party's ability to bring
|
|
||||||
cross-claims or counter-claims.
|
|
||||||
|
|
||||||
9. Miscellaneous
|
|
||||||
----------------
|
|
||||||
|
|
||||||
This License represents the complete agreement concerning the subject
|
|
||||||
matter hereof. If any provision of this License is held to be
|
|
||||||
unenforceable, such provision shall be reformed only to the extent
|
|
||||||
necessary to make it enforceable. Any law or regulation which provides
|
|
||||||
that the language of a contract shall be construed against the drafter
|
|
||||||
shall not be used to construe this License against a Contributor.
|
|
||||||
|
|
||||||
10. Versions of the License
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
10.1. New Versions
|
|
||||||
|
|
||||||
Mozilla Foundation is the license steward. Except as provided in Section
|
|
||||||
10.3, no one other than the license steward has the right to modify or
|
|
||||||
publish new versions of this License. Each version will be given a
|
|
||||||
distinguishing version number.
|
|
||||||
|
|
||||||
10.2. Effect of New Versions
|
|
||||||
|
|
||||||
You may distribute the Covered Software under the terms of the version
|
|
||||||
of the License under which You originally received the Covered Software,
|
|
||||||
or under the terms of any subsequent version published by the license
|
|
||||||
steward.
|
|
||||||
|
|
||||||
10.3. Modified Versions
|
|
||||||
|
|
||||||
If you create software not governed by this License, and you want to
|
|
||||||
create a new license for such software, you may create and use a
|
|
||||||
modified version of this License if you rename the license and remove
|
|
||||||
any references to the name of the license steward (except to note that
|
|
||||||
such modified license differs from this License).
|
|
||||||
|
|
||||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
|
||||||
Licenses
|
|
||||||
|
|
||||||
If You choose to distribute Source Code Form that is Incompatible With
|
|
||||||
Secondary Licenses under the terms of this version of the License, the
|
|
||||||
notice described in Exhibit B of this License must be attached.
|
|
||||||
|
|
||||||
Exhibit A - Source Code Form License Notice
|
|
||||||
-------------------------------------------
|
|
||||||
|
|
||||||
This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
If it is not possible or desirable to put the notice in a particular
|
|
||||||
file, then You may include the notice in a location (such as a LICENSE
|
|
||||||
file in a relevant directory) where a recipient would be likely to look
|
|
||||||
for such a notice.
|
|
||||||
|
|
||||||
You may add additional accurate notices of copyright ownership.
|
|
||||||
|
|
||||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
|
||||||
---------------------------------------------------------
|
|
||||||
|
|
||||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
|
||||||
defined by the Mozilla Public License, v. 2.0.
|
|
@@ -1,34 +0,0 @@
|
|||||||
<Project>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Update="Microsoft.AspNetCore.Components.Authorization" Version="7.0.0-rc.1.22427.2" />
|
|
||||||
<PackageReference Update="Microsoft.AspNetCore.Components.Web" Version="7.0.0-rc.1.22427.2" />
|
|
||||||
<PackageReference Update="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.0-rc.1.22427.2" />
|
|
||||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Tools" Version="7.0.0-rc.1.22426.7" />
|
|
||||||
<PackageReference Update="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.0-rc.1" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Update="Kajabity.Tools.Java" Version="0.3.7879.40798" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Update="MemoryPack" Version="1.4.1" />
|
|
||||||
<PackageReference Update="NetMQ" Version="4.0.1.10" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Update="Serilog" Version="2.12.0" />
|
|
||||||
<PackageReference Update="Serilog.AspNetCore" Version="6.0.1" />
|
|
||||||
<PackageReference Update="Serilog.Sinks.Console" Version="4.1.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Update="coverlet.collector" Version="3.1.2" />
|
|
||||||
<PackageReference Update="Microsoft.NET.Test.Sdk" Version="17.3.2" />
|
|
||||||
<PackageReference Update="NUnit" Version="3.13.3" />
|
|
||||||
<PackageReference Update="NUnit.Analyzers" Version="3.3.0" />
|
|
||||||
<PackageReference Update="NUnit3TestAdapter" Version="4.2.1" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@@ -26,8 +26,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Common.Data.Tests",
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Common.Logging", "Common\Phantom.Common.Logging\Phantom.Common.Logging.csproj", "{D7F55010-B3ED-42A5-8D83-E754FFC5F2A2}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Common.Logging", "Common\Phantom.Common.Logging\Phantom.Common.Logging.csproj", "{D7F55010-B3ED-42A5-8D83-E754FFC5F2A2}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Common.Minecraft", "Common\Phantom.Common.Minecraft\Phantom.Common.Minecraft.csproj", "{48278E42-17BB-442B-9877-ACCE0C02C268}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Common.Messages", "Common\Phantom.Common.Messages\Phantom.Common.Messages.csproj", "{95B55357-F8F0-48C2-A1C2-5EA997651783}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Common.Messages", "Common\Phantom.Common.Messages\Phantom.Common.Messages.csproj", "{95B55357-F8F0-48C2-A1C2-5EA997651783}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Server", "Server\Phantom.Server\Phantom.Server.csproj", "{A0F1C595-96B6-4DBF-8C16-6B99223F8F35}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Server", "Server\Phantom.Server\Phantom.Server.csproj", "{A0F1C595-96B6-4DBF-8C16-6B99223F8F35}"
|
||||||
@@ -46,8 +44,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Server.Web.Bootstra
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Server.Web.Components", "Server\Phantom.Server.Web.Components\Phantom.Server.Web.Components.csproj", "{3F4F9059-F869-42D3-B92C-90D27ADFC42D}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Server.Web.Components", "Server\Phantom.Server.Web.Components\Phantom.Server.Web.Components.csproj", "{3F4F9059-F869-42D3-B92C-90D27ADFC42D}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Server.Web.Identity", "Server\Phantom.Server.Web.Identity\Phantom.Server.Web.Identity.csproj", "{A9870842-FE7A-4760-95DC-9D485DDDA31F}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Utils.Collections", "Utils\Phantom.Utils.Collections\Phantom.Utils.Collections.csproj", "{444AC6C1-E0E1-45C3-965E-BFA818D70913}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Utils.Collections", "Utils\Phantom.Utils.Collections\Phantom.Utils.Collections.csproj", "{444AC6C1-E0E1-45C3-965E-BFA818D70913}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Utils.Collections.Tests", "Utils\Phantom.Utils.Collections.Tests\Phantom.Utils.Collections.Tests.csproj", "{C418CCDB-2D7E-4B66-8C86-029928AA80A8}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Utils.Collections.Tests", "Utils\Phantom.Utils.Collections.Tests\Phantom.Utils.Collections.Tests.csproj", "{C418CCDB-2D7E-4B66-8C86-029928AA80A8}"
|
||||||
@@ -98,10 +94,6 @@ Global
|
|||||||
{D7F55010-B3ED-42A5-8D83-E754FFC5F2A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{D7F55010-B3ED-42A5-8D83-E754FFC5F2A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{D7F55010-B3ED-42A5-8D83-E754FFC5F2A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{D7F55010-B3ED-42A5-8D83-E754FFC5F2A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{D7F55010-B3ED-42A5-8D83-E754FFC5F2A2}.Release|Any CPU.Build.0 = Release|Any CPU
|
{D7F55010-B3ED-42A5-8D83-E754FFC5F2A2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{48278E42-17BB-442B-9877-ACCE0C02C268}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{48278E42-17BB-442B-9877-ACCE0C02C268}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{48278E42-17BB-442B-9877-ACCE0C02C268}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{48278E42-17BB-442B-9877-ACCE0C02C268}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{95B55357-F8F0-48C2-A1C2-5EA997651783}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{95B55357-F8F0-48C2-A1C2-5EA997651783}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{95B55357-F8F0-48C2-A1C2-5EA997651783}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{95B55357-F8F0-48C2-A1C2-5EA997651783}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{95B55357-F8F0-48C2-A1C2-5EA997651783}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{95B55357-F8F0-48C2-A1C2-5EA997651783}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
@@ -134,10 +126,6 @@ Global
|
|||||||
{3F4F9059-F869-42D3-B92C-90D27ADFC42D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{3F4F9059-F869-42D3-B92C-90D27ADFC42D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{3F4F9059-F869-42D3-B92C-90D27ADFC42D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{3F4F9059-F869-42D3-B92C-90D27ADFC42D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{3F4F9059-F869-42D3-B92C-90D27ADFC42D}.Release|Any CPU.Build.0 = Release|Any CPU
|
{3F4F9059-F869-42D3-B92C-90D27ADFC42D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{A9870842-FE7A-4760-95DC-9D485DDDA31F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{A9870842-FE7A-4760-95DC-9D485DDDA31F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{A9870842-FE7A-4760-95DC-9D485DDDA31F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{A9870842-FE7A-4760-95DC-9D485DDDA31F}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{444AC6C1-E0E1-45C3-965E-BFA818D70913}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{444AC6C1-E0E1-45C3-965E-BFA818D70913}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{444AC6C1-E0E1-45C3-965E-BFA818D70913}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{444AC6C1-E0E1-45C3-965E-BFA818D70913}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{444AC6C1-E0E1-45C3-965E-BFA818D70913}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{444AC6C1-E0E1-45C3-965E-BFA818D70913}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
@@ -178,7 +166,6 @@ Global
|
|||||||
{AEE8B77E-AB07-423F-9981-8CD829ACB834} = {F5878792-64C8-4ECF-A075-66341FF97127}
|
{AEE8B77E-AB07-423F-9981-8CD829ACB834} = {F5878792-64C8-4ECF-A075-66341FF97127}
|
||||||
{6C3DB1E5-F695-4D70-8F3A-78C2957274BE} = {01CB1A81-8950-471C-BFDF-F135FDDB2C18}
|
{6C3DB1E5-F695-4D70-8F3A-78C2957274BE} = {01CB1A81-8950-471C-BFDF-F135FDDB2C18}
|
||||||
{D7F55010-B3ED-42A5-8D83-E754FFC5F2A2} = {01CB1A81-8950-471C-BFDF-F135FDDB2C18}
|
{D7F55010-B3ED-42A5-8D83-E754FFC5F2A2} = {01CB1A81-8950-471C-BFDF-F135FDDB2C18}
|
||||||
{48278E42-17BB-442B-9877-ACCE0C02C268} = {01CB1A81-8950-471C-BFDF-F135FDDB2C18}
|
|
||||||
{95B55357-F8F0-48C2-A1C2-5EA997651783} = {01CB1A81-8950-471C-BFDF-F135FDDB2C18}
|
{95B55357-F8F0-48C2-A1C2-5EA997651783} = {01CB1A81-8950-471C-BFDF-F135FDDB2C18}
|
||||||
{435D7981-DFDA-46A0-8CD8-CD8C117935D7} = {D781E00D-8563-4102-A0CD-477A679193B5}
|
{435D7981-DFDA-46A0-8CD8-CD8C117935D7} = {D781E00D-8563-4102-A0CD-477A679193B5}
|
||||||
{A0F1C595-96B6-4DBF-8C16-6B99223F8F35} = {8AC8FB6C-033A-4626-820F-ED0F908756B2}
|
{A0F1C595-96B6-4DBF-8C16-6B99223F8F35} = {8AC8FB6C-033A-4626-820F-ED0F908756B2}
|
||||||
@@ -189,7 +176,6 @@ Global
|
|||||||
{7CA2E5FE-E507-4DC6-930C-E18711A9F856} = {8AC8FB6C-033A-4626-820F-ED0F908756B2}
|
{7CA2E5FE-E507-4DC6-930C-E18711A9F856} = {8AC8FB6C-033A-4626-820F-ED0F908756B2}
|
||||||
{83FA86DB-34E4-4C2C-832C-90F491CA10C7} = {8AC8FB6C-033A-4626-820F-ED0F908756B2}
|
{83FA86DB-34E4-4C2C-832C-90F491CA10C7} = {8AC8FB6C-033A-4626-820F-ED0F908756B2}
|
||||||
{3F4F9059-F869-42D3-B92C-90D27ADFC42D} = {8AC8FB6C-033A-4626-820F-ED0F908756B2}
|
{3F4F9059-F869-42D3-B92C-90D27ADFC42D} = {8AC8FB6C-033A-4626-820F-ED0F908756B2}
|
||||||
{A9870842-FE7A-4760-95DC-9D485DDDA31F} = {8AC8FB6C-033A-4626-820F-ED0F908756B2}
|
|
||||||
{444AC6C1-E0E1-45C3-965E-BFA818D70913} = {AA217EB8-E480-456B-BDF3-39419EF2AD85}
|
{444AC6C1-E0E1-45C3-965E-BFA818D70913} = {AA217EB8-E480-456B-BDF3-39419EF2AD85}
|
||||||
{31661196-164A-4F92-86A1-31F13F4E4C83} = {AA217EB8-E480-456B-BDF3-39419EF2AD85}
|
{31661196-164A-4F92-86A1-31F13F4E4C83} = {AA217EB8-E480-456B-BDF3-39419EF2AD85}
|
||||||
{2E81523B-5DBE-4992-A77B-1679758D0688} = {AA217EB8-E480-456B-BDF3-39419EF2AD85}
|
{2E81523B-5DBE-4992-A77B-1679758D0688} = {AA217EB8-E480-456B-BDF3-39419EF2AD85}
|
||||||
|
121
README.md
121
README.md
@@ -1,121 +0,0 @@
|
|||||||
# Phantom Panel
|
|
||||||
|
|
||||||
Phantom Panel is a **work-in-progress** web interface for managing Minecraft servers.
|
|
||||||
|
|
||||||
# Architecture
|
|
||||||
|
|
||||||
Phantom Panel is built on what I'm calling a **Server-Agent architecture**:
|
|
||||||
|
|
||||||
* The **Server** is provides a web interface, persists data in a database, and sends commands to the **Agents**.
|
|
||||||
* One or more **Agents** receive commands from the **Server**, manage the Minecraft server processes, and report on their status.
|
|
||||||
|
|
||||||
This architecture has several goals and benefits:
|
|
||||||
|
|
||||||
1. The Server and Agents can run on separate computers, in separate containers, or a mixture of both.
|
|
||||||
2. The Server and Agents can be updated independently.
|
|
||||||
- The Server can receive new features, bug fixes, and security updates without the need to shutdown every Minecraft server.
|
|
||||||
- Agent updates can be staggered or delayed. For example, if you have Agents in different geographical locations, you could schedule around timezones and update them at times when people are unlikely to be online.
|
|
||||||
3. Agents are lightweight processes which should have minimal impact on the performance of Minecraft servers.
|
|
||||||
|
|
||||||
When an official Server update is released, it will work with older versions of Agents. There is no guarantee it will also work in reverse (updated Agents and an older Server), but if there is an Agent update that is compatible with older Servers, it will be mentioned in the release notes.
|
|
||||||
|
|
||||||
Note that compatibility is only guaranteed when using official releases. If you build the project from a version of the source between two official releases, you have to understand which changes break compatibility.
|
|
||||||
|
|
||||||
# Usage
|
|
||||||
|
|
||||||
This project is **work-in-progress**, and currently has no official releases. Feel free to try it and experiment, but there will be missing features, bugs, and breaking changes.
|
|
||||||
|
|
||||||
For a quick start, I recommend using [Docker](https://www.docker.com/) or another containerization platform. The `Dockerfile` in the root of the repository can build two target images: `phantom-server` and `phantom-agent`.
|
|
||||||
|
|
||||||
Both images put the built application into the `/app` folder. The Agent image also installs Java 8, 16, 17, and 18.
|
|
||||||
|
|
||||||
Files are stored relative to the working directory. In the provided images, the working directory is set to `/data`.
|
|
||||||
|
|
||||||
## Server
|
|
||||||
|
|
||||||
The Server comprises 3 key areas:
|
|
||||||
|
|
||||||
* **Web server** that provides the web interface.
|
|
||||||
* **RPC server** that Agents connect to.
|
|
||||||
* **Database connection** that requires a PostgreSQL database server in order to persist data.
|
|
||||||
|
|
||||||
The configuration for these is set via environment variables.
|
|
||||||
|
|
||||||
### Agent Key
|
|
||||||
|
|
||||||
When the Server starts for the first time, it will generate and an **Agent Key**. The Agent Key contains an encryption certificate and an authorization token, which are needed for the Agents to connect to the Server.
|
|
||||||
|
|
||||||
The Agent Key has two forms:
|
|
||||||
|
|
||||||
* A binary file stored in `/data/secrets/agent.key` that the Agents can read.
|
|
||||||
* A plaintext-encoded version the Server outputs into the logs on every startup, that can be passed to the Agents in an environemnt variable.
|
|
||||||
|
|
||||||
### Storage
|
|
||||||
|
|
||||||
Use volumes to persist the whole `/data` folder.
|
|
||||||
|
|
||||||
### Environment variables
|
|
||||||
|
|
||||||
* **Web Server**
|
|
||||||
- `WEB_SERVER_HOST` is the host. Default: `0.0.0.0`
|
|
||||||
- `WEB_SERVER_PORT` is the port. Default: `9400`
|
|
||||||
- `WEB_BASE_PATH` is the base path of every URL. Must begin with a slash. Default: `/`
|
|
||||||
* **RPC Server**
|
|
||||||
- `RPC_SERVER_HOST` is the host. Default: `0.0.0.0`
|
|
||||||
- `RPC_SERVER_PORT` is the port. Default: `9401`
|
|
||||||
* **PostgreSQL Database Server**
|
|
||||||
- `PG_HOST` is the hostname.
|
|
||||||
- `PG_PORT` is the port.
|
|
||||||
- `PG_USER` is the username.
|
|
||||||
- `PG_PASS` is the password.
|
|
||||||
- `PG_DATABASE` is the database name.
|
|
||||||
|
|
||||||
## Agent
|
|
||||||
|
|
||||||
### Storage
|
|
||||||
|
|
||||||
The `/data` folder will contain two folders:
|
|
||||||
|
|
||||||
* `/data/data` for persistent files
|
|
||||||
* `/data/temp` for volatile files (such as downloaded Minecraft `.jar` files)
|
|
||||||
|
|
||||||
Use volumes to persist either the whole `/data` folder, or just `/data/data` if you don't want to persist the volatile files.
|
|
||||||
|
|
||||||
### Environment variables:
|
|
||||||
|
|
||||||
* **Server Communication**
|
|
||||||
- `SERVER_HOST` is the hostname of the Server.
|
|
||||||
- `SERVER_PORT` is the RPC port of the Server. Default: `9401`
|
|
||||||
- `AGENT_NAME` is the display name of the Agent. Emoji are allowed.
|
|
||||||
- `AGENT_KEY` is the plaintext-encoded version of [Agent Key](#agent-key).
|
|
||||||
- `AGENT_KEY_FILE` is a path to the [Agent Key](#agent-key) binary file.
|
|
||||||
* **Agent Configuration**
|
|
||||||
- `MAX_INSTANCES` is the number of instances that can be created.
|
|
||||||
- `MAX_MEMORY` is the maximum amount of RAM that can be distributed among all instances. Use a positive integer with an optional suffix 'M' for MB, or 'G' for GB. Examples: `4096M`, `16G`
|
|
||||||
* **Minecraft Configuration**
|
|
||||||
- `JAVA_SEARCH_PATH` is a path to a folder which will be searched for Java runtime installations. Linux default: `/usr/lib/jvm`
|
|
||||||
- `ALLOWED_SERVER_PORTS` is a comma-separated list of ports and port ranges that can be used as Minecraft Server ports. Example: `25565,25900,26000-27000`
|
|
||||||
- `ALLOWED_RCON_PORTS` is a comma-separated list of ports and port ranges that can be used as Minecraft RCON ports. Example: `25575,25901,36000-37000`
|
|
||||||
|
|
||||||
# Development
|
|
||||||
|
|
||||||
The repository includes a [Rider](https://www.jetbrains.com/rider/) projects with several run configurations. The `.workdir` folder in the root of the repository is used for storage. Here's how to get started:
|
|
||||||
|
|
||||||
1. You will need a local PostgreSQL instance. If you have [Docker](https://www.docker.com/), you can enter the `Docker` folder in this repository, and run `docker compose up`. Otherwise, you will need to set it up manually with the following configuration:
|
|
||||||
- Host: `localhost`
|
|
||||||
- Port: `9402`
|
|
||||||
- User: `postgres`
|
|
||||||
- Password: `development`
|
|
||||||
- Database: `postgres`
|
|
||||||
2. Install one or more Java versions into the `~/.jdks` folder (`%USERPROFILE%\.jdks` on Windows).
|
|
||||||
3. Open the project in [Rider](https://www.jetbrains.com/rider/) and use one of the provided run configurations:
|
|
||||||
- `Server` starts the Server.
|
|
||||||
- `Agent 1`, `Agent 2`, `Agent 3` start one of the Agents.
|
|
||||||
- `Server + Agent` starts the Server and Agent 1.
|
|
||||||
- `Server + Agent x3` starts the Server and Agent 1, 2, and 3.
|
|
||||||
|
|
||||||
## Bootstrap
|
|
||||||
|
|
||||||
The project uses [Bootstrap 5](https://getbootstrap.com/docs/5.2) with a custom theme and several other customizations. The sources are in the `Phantom.Server.Web.Bootstrap` project.
|
|
||||||
|
|
||||||
If you make any changes to the sources, you will need to use the `Compile Bootstrap` run configuration, then restart the Server to load the new version. This is not done automatically, and it requires [Node](https://nodejs.org/en/) and [npm](https://www.npmjs.com/).
|
|
@@ -1,342 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
using Phantom.Server.Database;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Phantom.Server.Database.Postgres.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(ApplicationDbContext))]
|
|
||||||
[Migration("20221008163849_Identity")]
|
|
||||||
partial class Identity
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "7.0.0-rc.1.22426.7")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ConcurrencyStamp")
|
|
||||||
.IsConcurrencyToken()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedName")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("NormalizedName")
|
|
||||||
.IsUnique()
|
|
||||||
.HasDatabaseName("RoleNameIndex");
|
|
||||||
|
|
||||||
b.ToTable("Roles", "identity");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("ClaimType")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ClaimValue")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("RoleId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("RoleId");
|
|
||||||
|
|
||||||
b.ToTable("RoleClaims", "identity");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("AccessFailedCount")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("ConcurrencyStamp")
|
|
||||||
.IsConcurrencyToken()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Email")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<bool>("EmailConfirmed")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<bool>("LockoutEnabled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedEmail")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedUserName")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("PasswordHash")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("PhoneNumber")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("PhoneNumberConfirmed")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("SecurityStamp")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("TwoFactorEnabled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("UserName")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("NormalizedEmail")
|
|
||||||
.HasDatabaseName("EmailIndex");
|
|
||||||
|
|
||||||
b.HasIndex("NormalizedUserName")
|
|
||||||
.IsUnique()
|
|
||||||
.HasDatabaseName("UserNameIndex");
|
|
||||||
|
|
||||||
b.ToTable("Users", "identity");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("ClaimType")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ClaimValue")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("UserClaims", "identity");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("LoginProvider")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ProviderKey")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ProviderDisplayName")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("LoginProvider", "ProviderKey");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("UserLogins", "identity");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("RoleId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("UserId", "RoleId");
|
|
||||||
|
|
||||||
b.HasIndex("RoleId");
|
|
||||||
|
|
||||||
b.ToTable("UserRoles", "identity");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LoginProvider")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Value")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("UserId", "LoginProvider", "Name");
|
|
||||||
|
|
||||||
b.ToTable("UserTokens", "identity");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Phantom.Server.Database.Entities.AgentEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("AgentGuid")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<int>("MaxInstances")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<ushort>("MaxMemory")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("Version")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.HasKey("AgentGuid");
|
|
||||||
|
|
||||||
b.ToTable("Agents", "agents");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Phantom.Server.Database.Entities.InstanceEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("InstanceGuid")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<Guid>("AgentGuid")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("InstanceName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<Guid>("JavaRuntimeGuid")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<bool>("LaunchAutomatically")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<ushort>("MemoryAllocation")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("MinecraftServerKind")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("MinecraftVersion")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("RconPort")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("ServerPort")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.HasKey("InstanceGuid");
|
|
||||||
|
|
||||||
b.ToTable("Instances", "agents");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("RoleId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("RoleId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,253 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Phantom.Server.Database.Postgres.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class Identity : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.EnsureSchema(
|
|
||||||
name: "identity");
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Roles",
|
|
||||||
schema: "identity",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
|
||||||
NormalizedName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
|
||||||
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Roles", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Users",
|
|
||||||
schema: "identity",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "text", nullable: false),
|
|
||||||
UserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
|
||||||
NormalizedUserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
|
||||||
Email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
|
||||||
NormalizedEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
|
||||||
EmailConfirmed = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
PasswordHash = table.Column<string>(type: "text", nullable: true),
|
|
||||||
SecurityStamp = table.Column<string>(type: "text", nullable: true),
|
|
||||||
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true),
|
|
||||||
PhoneNumber = table.Column<string>(type: "text", nullable: true),
|
|
||||||
PhoneNumberConfirmed = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
TwoFactorEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
LockoutEnd = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
|
||||||
LockoutEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
AccessFailedCount = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Users", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "RoleClaims",
|
|
||||||
schema: "identity",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
|
||||||
RoleId = table.Column<string>(type: "text", nullable: false),
|
|
||||||
ClaimType = table.Column<string>(type: "text", nullable: true),
|
|
||||||
ClaimValue = table.Column<string>(type: "text", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_RoleClaims", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_RoleClaims_Roles_RoleId",
|
|
||||||
column: x => x.RoleId,
|
|
||||||
principalSchema: "identity",
|
|
||||||
principalTable: "Roles",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "UserClaims",
|
|
||||||
schema: "identity",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
|
||||||
UserId = table.Column<string>(type: "text", nullable: false),
|
|
||||||
ClaimType = table.Column<string>(type: "text", nullable: true),
|
|
||||||
ClaimValue = table.Column<string>(type: "text", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_UserClaims", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_UserClaims_Users_UserId",
|
|
||||||
column: x => x.UserId,
|
|
||||||
principalSchema: "identity",
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "UserLogins",
|
|
||||||
schema: "identity",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
LoginProvider = table.Column<string>(type: "text", nullable: false),
|
|
||||||
ProviderKey = table.Column<string>(type: "text", nullable: false),
|
|
||||||
ProviderDisplayName = table.Column<string>(type: "text", nullable: true),
|
|
||||||
UserId = table.Column<string>(type: "text", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_UserLogins", x => new { x.LoginProvider, x.ProviderKey });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_UserLogins_Users_UserId",
|
|
||||||
column: x => x.UserId,
|
|
||||||
principalSchema: "identity",
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "UserRoles",
|
|
||||||
schema: "identity",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
UserId = table.Column<string>(type: "text", nullable: false),
|
|
||||||
RoleId = table.Column<string>(type: "text", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_UserRoles", x => new { x.UserId, x.RoleId });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_UserRoles_Roles_RoleId",
|
|
||||||
column: x => x.RoleId,
|
|
||||||
principalSchema: "identity",
|
|
||||||
principalTable: "Roles",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_UserRoles_Users_UserId",
|
|
||||||
column: x => x.UserId,
|
|
||||||
principalSchema: "identity",
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "UserTokens",
|
|
||||||
schema: "identity",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
UserId = table.Column<string>(type: "text", nullable: false),
|
|
||||||
LoginProvider = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Name = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Value = table.Column<string>(type: "text", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_UserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_UserTokens_Users_UserId",
|
|
||||||
column: x => x.UserId,
|
|
||||||
principalSchema: "identity",
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_RoleClaims_RoleId",
|
|
||||||
schema: "identity",
|
|
||||||
table: "RoleClaims",
|
|
||||||
column: "RoleId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "RoleNameIndex",
|
|
||||||
schema: "identity",
|
|
||||||
table: "Roles",
|
|
||||||
column: "NormalizedName",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_UserClaims_UserId",
|
|
||||||
schema: "identity",
|
|
||||||
table: "UserClaims",
|
|
||||||
column: "UserId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_UserLogins_UserId",
|
|
||||||
schema: "identity",
|
|
||||||
table: "UserLogins",
|
|
||||||
column: "UserId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_UserRoles_RoleId",
|
|
||||||
schema: "identity",
|
|
||||||
table: "UserRoles",
|
|
||||||
column: "RoleId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "EmailIndex",
|
|
||||||
schema: "identity",
|
|
||||||
table: "Users",
|
|
||||||
column: "NormalizedEmail");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "UserNameIndex",
|
|
||||||
schema: "identity",
|
|
||||||
table: "Users",
|
|
||||||
column: "NormalizedUserName",
|
|
||||||
unique: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "RoleClaims",
|
|
||||||
schema: "identity");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "UserClaims",
|
|
||||||
schema: "identity");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "UserLogins",
|
|
||||||
schema: "identity");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "UserRoles",
|
|
||||||
schema: "identity");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "UserTokens",
|
|
||||||
schema: "identity");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Roles",
|
|
||||||
schema: "identity");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Users",
|
|
||||||
schema: "identity");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,346 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
using Phantom.Server.Database;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Phantom.Server.Database.Postgres.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(ApplicationDbContext))]
|
|
||||||
[Migration("20221010193220_InstanceJvmArguments")]
|
|
||||||
partial class InstanceJvmArguments
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "7.0.0-rc.1.22426.7")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ConcurrencyStamp")
|
|
||||||
.IsConcurrencyToken()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedName")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("NormalizedName")
|
|
||||||
.IsUnique()
|
|
||||||
.HasDatabaseName("RoleNameIndex");
|
|
||||||
|
|
||||||
b.ToTable("Roles", "identity");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("ClaimType")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ClaimValue")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("RoleId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("RoleId");
|
|
||||||
|
|
||||||
b.ToTable("RoleClaims", "identity");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("AccessFailedCount")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("ConcurrencyStamp")
|
|
||||||
.IsConcurrencyToken()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Email")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<bool>("EmailConfirmed")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<bool>("LockoutEnabled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedEmail")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedUserName")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("PasswordHash")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("PhoneNumber")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("PhoneNumberConfirmed")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("SecurityStamp")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("TwoFactorEnabled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("UserName")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("NormalizedEmail")
|
|
||||||
.HasDatabaseName("EmailIndex");
|
|
||||||
|
|
||||||
b.HasIndex("NormalizedUserName")
|
|
||||||
.IsUnique()
|
|
||||||
.HasDatabaseName("UserNameIndex");
|
|
||||||
|
|
||||||
b.ToTable("Users", "identity");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("ClaimType")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ClaimValue")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("UserClaims", "identity");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("LoginProvider")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ProviderKey")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ProviderDisplayName")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("LoginProvider", "ProviderKey");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("UserLogins", "identity");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("RoleId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("UserId", "RoleId");
|
|
||||||
|
|
||||||
b.HasIndex("RoleId");
|
|
||||||
|
|
||||||
b.ToTable("UserRoles", "identity");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LoginProvider")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Value")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("UserId", "LoginProvider", "Name");
|
|
||||||
|
|
||||||
b.ToTable("UserTokens", "identity");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Phantom.Server.Database.Entities.AgentEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("AgentGuid")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<int>("MaxInstances")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<ushort>("MaxMemory")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("Version")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.HasKey("AgentGuid");
|
|
||||||
|
|
||||||
b.ToTable("Agents", "agents");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Phantom.Server.Database.Entities.InstanceEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("InstanceGuid")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<Guid>("AgentGuid")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("InstanceName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<Guid>("JavaRuntimeGuid")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("JvmArguments")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("LaunchAutomatically")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<ushort>("MemoryAllocation")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("MinecraftServerKind")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("MinecraftVersion")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("RconPort")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("ServerPort")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.HasKey("InstanceGuid");
|
|
||||||
|
|
||||||
b.ToTable("Instances", "agents");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("RoleId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("RoleId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,31 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Phantom.Server.Database.Postgres.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class InstanceJvmArguments : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AddColumn<string>(
|
|
||||||
name: "JvmArguments",
|
|
||||||
schema: "agents",
|
|
||||||
table: "Instances",
|
|
||||||
type: "text",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: "");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "JvmArguments",
|
|
||||||
schema: "agents",
|
|
||||||
table: "Instances");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,392 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using System.Text.Json;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
using Phantom.Server.Database;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Phantom.Server.Database.Postgres.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(ApplicationDbContext))]
|
|
||||||
[Migration("20221016035515_AuditLog")]
|
|
||||||
partial class AuditLog
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "7.0.0-rc.1.22426.7")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ConcurrencyStamp")
|
|
||||||
.IsConcurrencyToken()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedName")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("NormalizedName")
|
|
||||||
.IsUnique()
|
|
||||||
.HasDatabaseName("RoleNameIndex");
|
|
||||||
|
|
||||||
b.ToTable("Roles", "identity");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("ClaimType")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ClaimValue")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("RoleId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("RoleId");
|
|
||||||
|
|
||||||
b.ToTable("RoleClaims", "identity");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("AccessFailedCount")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("ConcurrencyStamp")
|
|
||||||
.IsConcurrencyToken()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Email")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<bool>("EmailConfirmed")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<bool>("LockoutEnabled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedEmail")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedUserName")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("PasswordHash")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("PhoneNumber")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("PhoneNumberConfirmed")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("SecurityStamp")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("TwoFactorEnabled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("UserName")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("NormalizedEmail")
|
|
||||||
.HasDatabaseName("EmailIndex");
|
|
||||||
|
|
||||||
b.HasIndex("NormalizedUserName")
|
|
||||||
.IsUnique()
|
|
||||||
.HasDatabaseName("UserNameIndex");
|
|
||||||
|
|
||||||
b.ToTable("Users", "identity");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("ClaimType")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ClaimValue")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("UserClaims", "identity");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("LoginProvider")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ProviderKey")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ProviderDisplayName")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("LoginProvider", "ProviderKey");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("UserLogins", "identity");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("RoleId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("UserId", "RoleId");
|
|
||||||
|
|
||||||
b.HasIndex("RoleId");
|
|
||||||
|
|
||||||
b.ToTable("UserRoles", "identity");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LoginProvider")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Value")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("UserId", "LoginProvider", "Name");
|
|
||||||
|
|
||||||
b.ToTable("UserTokens", "identity");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Phantom.Server.Database.Entities.AgentEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("AgentGuid")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<int>("MaxInstances")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<ushort>("MaxMemory")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("Version")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.HasKey("AgentGuid");
|
|
||||||
|
|
||||||
b.ToTable("Agents", "agents");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Phantom.Server.Database.Entities.AuditEventEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<long>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
|
||||||
|
|
||||||
b.Property<JsonDocument>("Data")
|
|
||||||
.HasColumnType("jsonb");
|
|
||||||
|
|
||||||
b.Property<string>("EventType")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("SubjectId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("SubjectType")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UtcTime")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("AuditEvents", "system");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Phantom.Server.Database.Entities.InstanceEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("InstanceGuid")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<Guid>("AgentGuid")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("InstanceName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<Guid>("JavaRuntimeGuid")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("JvmArguments")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("LaunchAutomatically")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<ushort>("MemoryAllocation")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("MinecraftServerKind")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("MinecraftVersion")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("RconPort")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("ServerPort")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.HasKey("InstanceGuid");
|
|
||||||
|
|
||||||
b.ToTable("Instances", "agents");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("RoleId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("RoleId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Phantom.Server.Database.Entities.AuditEventEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId");
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user