1
0
mirror of https://github.com/chylex/Minecraft-Phantom-Panel.git synced 2024-11-25 16:42:54 +01:00

Compare commits

...

7 Commits

Author SHA1 Message Date
a89a8738f9
WIP 2023-10-22 07:41:46 +02:00
0d7f10d7ee
WIP 2023-10-22 07:41:46 +02:00
3afa724672
WIP 2023-10-22 07:41:46 +02:00
de040036e7
WIP 2023-10-22 07:41:46 +02:00
53edd80486
WIP 2023-10-22 07:41:46 +02:00
7b728a30ff
Extract classes for client to server RPC connection 2023-10-22 07:41:46 +02:00
e20d2232ed
Fully separate Controller and Web into their own services - Controller compiling and setup 2023-10-21 17:55:54 +02:00
179 changed files with 2078 additions and 1608 deletions

View File

@ -9,10 +9,10 @@
<env name="AGENT_NAME" value="Agent 1" />
<env name="ALLOWED_RCON_PORTS" value="25575,27000,27001" />
<env name="ALLOWED_SERVER_PORTS" value="25565,26000,26001" />
<env name="CONTROLLER_HOST" value="localhost" />
<env name="JAVA_SEARCH_PATH" value="~/.jdks" />
<env name="MAX_INSTANCES" value="3" />
<env name="MAX_MEMORY" value="12G" />
<env name="SERVER_HOST" value="localhost" />
</envs>
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />

View File

@ -9,10 +9,10 @@
<env name="AGENT_NAME" value="Agent 2" />
<env name="ALLOWED_RCON_PORTS" value="27002-27006" />
<env name="ALLOWED_SERVER_PORTS" value="26002-26006" />
<env name="CONTROLLER_HOST" value="localhost" />
<env name="JAVA_SEARCH_PATH" value="~/.jdks" />
<env name="MAX_INSTANCES" value="5" />
<env name="MAX_MEMORY" value="10G" />
<env name="SERVER_HOST" value="localhost" />
</envs>
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />

View File

@ -9,10 +9,10 @@
<env name="AGENT_NAME" value="Agent 3" />
<env name="ALLOWED_RCON_PORTS" value="27007" />
<env name="ALLOWED_SERVER_PORTS" value="26007" />
<env name="CONTROLLER_HOST" value="localhost" />
<env name="JAVA_SEARCH_PATH" value="~/.jdks" />
<env name="MAX_INSTANCES" value="1" />
<env name="MAX_MEMORY" value="2560M" />
<env name="SERVER_HOST" value="localhost" />
</envs>
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />

View File

@ -1,18 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Server" type="DotNetProject" factoryName=".NET Project">
<configuration default="false" name="Controller" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/.artifacts/bin/Phantom.Controller/debug/Phantom.Controller.exe" />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Server" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Controller" />
<option name="PASS_PARENT_ENVS" value="1" />
<envs>
<env name="ASPNETCORE_ENVIRONMENT" value="Development" />
<env name="AGENT_RPC_SERVER_HOST" value="localhost" />
<env name="PG_DATABASE" value="postgres" />
<env name="PG_HOST" value="localhost" />
<env name="PG_PASS" value="development" />
<env name="PG_PORT" value="9402" />
<env name="PG_PORT" value="9403" />
<env name="PG_USER" value="postgres" />
<env name="RPC_SERVER_HOST" value="localhost" />
<env name="WEB_SERVER_HOST" value="localhost" />
<env name="WEB_RPC_SERVER_HOST" value="localhost" />
</envs>
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />

24
.run/Web.run.xml Normal file
View File

@ -0,0 +1,24 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Web" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/.artifacts/bin/Phantom.Web/debug/Phantom.Web.exe" />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Web" />
<option name="PASS_PARENT_ENVS" value="1" />
<envs>
<env name="ASPNETCORE_ENVIRONMENT" value="Development" />
<env name="WEB_SERVER_HOST" value="localhost" />
</envs>
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Web/Phantom.Web/Phantom.Web.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net8.0" />
<method v="2">
<option name="Build" />
</method>
</configuration>
</component>

1
.workdir/Controller/.gitignore vendored Normal file
View File

@ -0,0 +1 @@


View File

@ -0,0 +1,2 @@
±™h?־<>ֹBx
<02> f-<2D>¢יא<01>“ש"8”כיJ<4A>Jn/וda

View File

@ -0,0 +1 @@
./gϋΏNρ°t<C2B0>$Ν!Β(ƒρ#η~ΖΞ}<14><:

View File

@ -1,5 +1,7 @@
using Phantom.Common.Logging;
using Phantom.Common.Messages.ToServer;
using Phantom.Common.Messages.Agent;
using Phantom.Common.Messages.Agent.ToController;
using Phantom.Utils.Rpc;
using Serilog;
namespace Phantom.Agent.Rpc;
@ -9,10 +11,10 @@ sealed class KeepAliveLoop {
private static readonly TimeSpan KeepAliveInterval = TimeSpan.FromSeconds(10);
private readonly RpcServerConnection connection;
private readonly RpcConnectionToServer<IMessageToControllerListener> connection;
private readonly CancellationTokenSource cancellationTokenSource = new ();
public KeepAliveLoop(RpcServerConnection connection) {
public KeepAliveLoop(RpcConnectionToServer<IMessageToControllerListener> connection) {
this.connection = connection;
Task.Run(Run);
}

View File

@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Common\Phantom.Common.Messages\Phantom.Common.Messages.csproj" />
<ProjectReference Include="..\..\Common\Phantom.Common.Messages.Agent\Phantom.Common.Messages.Agent.csproj" />
</ItemGroup>
</Project>

View File

@ -1,9 +1,9 @@
using NetMQ;
using NetMQ.Sockets;
using Phantom.Common.Data.Agent;
using Phantom.Common.Messages;
using Phantom.Common.Messages.BiDirectional;
using Phantom.Common.Messages.ToServer;
using Phantom.Common.Messages.Agent;
using Phantom.Common.Messages.Agent.BiDirectional;
using Phantom.Common.Messages.Agent.ToController;
using Phantom.Utils.Rpc;
using Phantom.Utils.Rpc.Message;
using Phantom.Utils.Tasks;
@ -13,25 +13,25 @@ using Serilog.Events;
namespace Phantom.Agent.Rpc;
public sealed class RpcLauncher : RpcRuntime<ClientSocket> {
public static Task Launch(RpcConfiguration config, AgentAuthToken authToken, AgentInfo agentInfo, Func<RpcServerConnection, IMessageToAgentListener> listenerFactory, SemaphoreSlim disconnectSemaphore, CancellationToken receiveCancellationToken) {
public static Task Launch(RpcConfiguration config, AuthToken authToken, AgentInfo agentInfo, Func<RpcConnectionToServer<IMessageToControllerListener>, IMessageToAgentListener> listenerFactory, SemaphoreSlim disconnectSemaphore, CancellationToken receiveCancellationToken) {
var socket = new ClientSocket();
var options = socket.Options;
options.CurveServerCertificate = config.ServerCertificate;
options.CurveCertificate = new NetMQCertificate();
options.HelloMessage = MessageRegistries.ToServer.Write(new RegisterAgentMessage(authToken, agentInfo)).ToArray();
options.HelloMessage = AgentMessageRegistries.ToController.Write(new RegisterAgentMessage(authToken, agentInfo)).ToArray();
return new RpcLauncher(config, socket, agentInfo.Guid, listenerFactory, disconnectSemaphore, receiveCancellationToken).Launch();
}
private readonly RpcConfiguration config;
private readonly Guid agentGuid;
private readonly Func<RpcServerConnection, IMessageToAgentListener> messageListenerFactory;
private readonly Func<RpcConnectionToServer<IMessageToControllerListener>, IMessageToAgentListener> messageListenerFactory;
private readonly SemaphoreSlim disconnectSemaphore;
private readonly CancellationToken receiveCancellationToken;
private RpcLauncher(RpcConfiguration config, ClientSocket socket, Guid agentGuid, Func<RpcServerConnection, IMessageToAgentListener> messageListenerFactory, SemaphoreSlim disconnectSemaphore, CancellationToken receiveCancellationToken) : base(config, socket) {
private RpcLauncher(RpcConfiguration config, ClientSocket socket, Guid agentGuid, Func<RpcConnectionToServer<IMessageToControllerListener>, IMessageToAgentListener> messageListenerFactory, SemaphoreSlim disconnectSemaphore, CancellationToken receiveCancellationToken) : base(config, socket) {
this.config = config;
this.agentGuid = agentGuid;
this.messageListenerFactory = messageListenerFactory;
@ -49,7 +49,7 @@ public sealed class RpcLauncher : RpcRuntime<ClientSocket> {
}
protected override void Run(ClientSocket socket, MessageReplyTracker replyTracker, TaskManager taskManager) {
var connection = new RpcServerConnection(socket, replyTracker);
var connection = new RpcConnectionToServer<IMessageToControllerListener>(socket, AgentMessageRegistries.ToController, replyTracker);
ServerMessaging.SetCurrentConnection(connection);
var logger = config.RuntimeLogger;
@ -63,7 +63,7 @@ public sealed class RpcLauncher : RpcRuntime<ClientSocket> {
LogMessageType(logger, data);
if (data.Length > 0) {
MessageRegistries.ToAgent.Handle(data, handler);
AgentMessageRegistries.ToAgent.Handle(data, handler);
}
}
} catch (OperationCanceledException) {
@ -81,11 +81,11 @@ public sealed class RpcLauncher : RpcRuntime<ClientSocket> {
return;
}
if (data.Length > 0 && MessageRegistries.ToAgent.TryGetType(data, out var type)) {
logger.Verbose("Received {MessageType} ({Bytes} B) from server.", type.Name, data.Length);
if (data.Length > 0 && AgentMessageRegistries.ToAgent.TryGetType(data, out var type)) {
logger.Verbose("Received {MessageType} ({Bytes} B) from controller.", type.Name, data.Length);
}
else {
logger.Verbose("Received {Bytes} B message from server.", data.Length);
logger.Verbose("Received {Bytes} B message from controller.", data.Length);
}
}
@ -93,7 +93,7 @@ public sealed class RpcLauncher : RpcRuntime<ClientSocket> {
var unregisterTimeoutTask = Task.Delay(TimeSpan.FromSeconds(5), CancellationToken.None);
var finishedTask = await Task.WhenAny(ServerMessaging.Send(new UnregisterAgentMessage(agentGuid)), unregisterTimeoutTask);
if (finishedTask == unregisterTimeoutTask) {
config.RuntimeLogger.Error("Timed out communicating agent shutdown with the server.");
config.RuntimeLogger.Error("Timed out communicating agent shutdown with the controller.");
}
}

View File

@ -1,41 +0,0 @@
using NetMQ;
using NetMQ.Sockets;
using Phantom.Common.Messages;
using Phantom.Common.Messages.BiDirectional;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Agent.Rpc;
public sealed class RpcServerConnection {
private readonly ClientSocket socket;
private readonly MessageReplyTracker replyTracker;
internal RpcServerConnection(ClientSocket socket, MessageReplyTracker replyTracker) {
this.socket = socket;
this.replyTracker = replyTracker;
}
internal async Task Send<TMessage>(TMessage message) where TMessage : IMessageToServer {
var bytes = MessageRegistries.ToServer.Write(message).ToArray();
if (bytes.Length > 0) {
await socket.SendAsync(bytes);
}
}
internal async Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken) where TMessage : IMessageToServer<TReply> where TReply : class {
var sequenceId = replyTracker.RegisterReply();
var bytes = MessageRegistries.ToServer.Write<TMessage, TReply>(sequenceId, message).ToArray();
if (bytes.Length == 0) {
replyTracker.ForgetReply(sequenceId);
return null;
}
await socket.SendAsync(bytes);
return await replyTracker.WaitForReply<TReply>(sequenceId, waitForReplyTime, waitForReplyCancellationToken);
}
public void Receive(ReplyMessage message) {
replyTracker.ReceiveReply(message.SequenceId, message.SerializedReply);
}
}

View File

@ -1,5 +1,6 @@
using Phantom.Common.Logging;
using Phantom.Common.Messages;
using Phantom.Common.Messages.Agent;
using Phantom.Utils.Rpc;
using Serilog;
namespace Phantom.Agent.Rpc;
@ -7,12 +8,12 @@ namespace Phantom.Agent.Rpc;
public static class ServerMessaging {
private static readonly ILogger Logger = PhantomLogger.Create(nameof(ServerMessaging));
private static RpcServerConnection? CurrentConnection { get; set; }
private static RpcServerConnection CurrentConnectionOrThrow => CurrentConnection ?? throw new InvalidOperationException("Server connection not ready.");
private static RpcConnectionToServer<IMessageToControllerListener>? CurrentConnection { get; set; }
private static RpcConnectionToServer<IMessageToControllerListener> CurrentConnectionOrThrow => CurrentConnection ?? throw new InvalidOperationException("Server connection not ready.");
private static readonly object SetCurrentConnectionLock = new ();
internal static void SetCurrentConnection(RpcServerConnection connection) {
internal static void SetCurrentConnection(RpcConnectionToServer<IMessageToControllerListener> connection) {
lock (SetCurrentConnectionLock) {
if (CurrentConnection != null) {
throw new InvalidOperationException("Server connection can only be set once.");
@ -24,11 +25,11 @@ public static class ServerMessaging {
Logger.Information("Server connection ready.");
}
public static Task Send<TMessage>(TMessage message) where TMessage : IMessageToServer {
public static Task Send<TMessage>(TMessage message) where TMessage : IMessageToController {
return CurrentConnectionOrThrow.Send(message);
}
public static Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken) where TMessage : IMessageToServer<TReply> where TReply : class {
public static Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken) where TMessage : IMessageToController<TReply> where TReply : class {
return CurrentConnectionOrThrow.Send<TMessage, TReply>(message, waitForReplyTime, waitForReplyCancellationToken);
}
}

View File

@ -6,7 +6,7 @@ using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies;
using Phantom.Common.Logging;
using Phantom.Common.Messages.ToServer;
using Phantom.Common.Messages.Agent.ToController;
using Serilog;
namespace Phantom.Agent.Services.Instances;

View File

@ -2,7 +2,7 @@ using System.Collections.Immutable;
using System.Threading.Channels;
using Phantom.Agent.Rpc;
using Phantom.Common.Logging;
using Phantom.Common.Messages.ToServer;
using Phantom.Common.Messages.Agent.ToController;
using Phantom.Utils.Tasks;
namespace Phantom.Agent.Services.Instances;

View File

@ -14,7 +14,7 @@ using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies;
using Phantom.Common.Logging;
using Phantom.Common.Messages.ToServer;
using Phantom.Common.Messages.Agent.ToController;
using Phantom.Utils.IO;
using Phantom.Utils.Tasks;
using Serilog;

View File

@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Common\Phantom.Common.Messages\Phantom.Common.Messages.csproj" />
<ProjectReference Include="..\..\Common\Phantom.Common.Messages.Agent\Phantom.Common.Messages.Agent.csproj" />
<ProjectReference Include="..\Phantom.Agent.Minecraft\Phantom.Agent.Minecraft.csproj" />
<ProjectReference Include="..\Phantom.Agent.Rpc\Phantom.Agent.Rpc.csproj" />
</ItemGroup>

View File

@ -2,10 +2,11 @@
using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Replies;
using Phantom.Common.Logging;
using Phantom.Common.Messages;
using Phantom.Common.Messages.BiDirectional;
using Phantom.Common.Messages.ToAgent;
using Phantom.Common.Messages.ToServer;
using Phantom.Common.Messages.Agent;
using Phantom.Common.Messages.Agent.BiDirectional;
using Phantom.Common.Messages.Agent.ToAgent;
using Phantom.Common.Messages.Agent.ToController;
using Phantom.Utils.Rpc;
using Phantom.Utils.Rpc.Message;
using Serilog;
@ -14,11 +15,11 @@ namespace Phantom.Agent.Services.Rpc;
public sealed class MessageListener : IMessageToAgentListener {
private static ILogger Logger { get; } = PhantomLogger.Create<MessageListener>();
private readonly RpcServerConnection connection;
private readonly RpcConnectionToServer<IMessageToControllerListener> connection;
private readonly AgentServices agent;
private readonly CancellationTokenSource shutdownTokenSource;
public MessageListener(RpcServerConnection connection, AgentServices agent, CancellationTokenSource shutdownTokenSource) {
public MessageListener(RpcConnectionToServer<IMessageToControllerListener> connection, AgentServices agent, CancellationTokenSource shutdownTokenSource) {
this.connection = connection;
this.agent = agent;
this.shutdownTokenSource = shutdownTokenSource;

View File

@ -10,7 +10,7 @@ namespace Phantom.Agent;
static class AgentKey {
private static ILogger Logger { get; } = PhantomLogger.Create(nameof(AgentKey));
public static Task<(NetMQCertificate, AgentAuthToken)?> Load(string? agentKeyToken, string? agentKeyFilePath) {
public static Task<(NetMQCertificate, AuthToken)?> Load(string? agentKeyToken, string? agentKeyFilePath) {
if (agentKeyFilePath != null) {
return LoadFromFile(agentKeyFilePath);
}
@ -22,7 +22,7 @@ static class AgentKey {
}
}
private static async Task<(NetMQCertificate, AgentAuthToken)?> LoadFromFile(string agentKeyFilePath) {
private static async Task<(NetMQCertificate, AuthToken)?> LoadFromFile(string agentKeyFilePath) {
if (!File.Exists(agentKeyFilePath)) {
Logger.Fatal("Missing agent key file: {AgentKeyFilePath}", agentKeyFilePath);
return null;
@ -41,7 +41,7 @@ static class AgentKey {
}
}
private static (NetMQCertificate, AgentAuthToken)? LoadFromToken(string agentKey) {
private static (NetMQCertificate, AuthToken)? LoadFromToken(string agentKey) {
try {
return LoadFromBytes(TokenGenerator.DecodeBytes(agentKey));
} catch (Exception) {
@ -50,11 +50,11 @@ static class AgentKey {
}
}
private static (NetMQCertificate, AgentAuthToken)? LoadFromBytes(byte[] agentKey) {
var (publicKey, agentToken) = AgentKeyData.FromBytes(agentKey);
var serverCertificate = NetMQCertificate.FromPublicKey(publicKey);
private static (NetMQCertificate, AuthToken)? LoadFromBytes(byte[] agentKey) {
var (publicKey, agentToken) = ConnectionCommonKey.FromBytes(agentKey);
var controllerCertificate = NetMQCertificate.FromPublicKey(publicKey);
Logger.Information("Loaded agent key.");
return (serverCertificate, agentToken);
return (controllerCertificate, agentToken);
}
}

View File

@ -5,6 +5,7 @@ using Phantom.Agent.Services;
using Phantom.Agent.Services.Rpc;
using Phantom.Common.Data.Agent;
using Phantom.Common.Logging;
using Phantom.Common.Messages.Agent;
using Phantom.Utils.Rpc;
using Phantom.Utils.Runtime;
using Phantom.Utils.Tasks;
@ -26,7 +27,7 @@ try {
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, maxConcurrentBackupCompressionTasks) = Variables.LoadOrStop();
var (controllerHost, controllerPort, javaSearchPath, agentKeyToken, agentKeyFilePath, agentName, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts, maxConcurrentBackupCompressionTasks) = Variables.LoadOrStop();
var agentKey = await AgentKey.Load(agentKeyToken, agentKeyFilePath);
if (agentKey == null) {
@ -43,11 +44,11 @@ try {
return 1;
}
var (serverCertificate, agentToken) = agentKey.Value;
var (controllerCertificate, agentToken) = agentKey.Value;
var agentInfo = new AgentInfo(agentGuid.Value, agentName, ProtocolVersion, fullVersion, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts);
var agentServices = new AgentServices(agentInfo, folders, new AgentServiceConfiguration(maxConcurrentBackupCompressionTasks));
MessageListener MessageListenerFactory(RpcServerConnection connection) {
MessageListener MessageListenerFactory(RpcConnectionToServer<IMessageToControllerListener> connection) {
return new MessageListener(connection, agentServices, shutdownCancellationTokenSource);
}
@ -56,7 +57,7 @@ try {
await agentServices.Initialize();
var rpcDisconnectSemaphore = new SemaphoreSlim(0, 1);
var rpcConfiguration = new RpcConfiguration(PhantomLogger.Create("Rpc"), PhantomLogger.Create<TaskManager>("Rpc"), serverHost, serverPort, serverCertificate);
var rpcConfiguration = new RpcConfiguration(PhantomLogger.Create("Rpc"), PhantomLogger.Create<TaskManager>("Rpc"), controllerHost, controllerPort, controllerCertificate);
var rpcTask = RpcLauncher.Launch(rpcConfiguration, agentToken, agentInfo, MessageListenerFactory, rpcDisconnectSemaphore, shutdownCancellationToken);
try {
await rpcTask.WaitAsync(shutdownCancellationToken);

View File

@ -6,8 +6,8 @@ using Phantom.Utils.Runtime;
namespace Phantom.Agent;
sealed record Variables(
string ServerHost,
ushort ServerPort,
string ControllerHost,
ushort ControllerPort,
string JavaSearchPath,
string? AgentKeyToken,
string? AgentKeyFilePath,
@ -23,8 +23,8 @@ sealed record Variables(
var javaSearchPath = EnvironmentVariables.GetString("JAVA_SEARCH_PATH").WithDefaultGetter(GetDefaultJavaSearchPath);
return new Variables(
EnvironmentVariables.GetString("SERVER_HOST").Require,
EnvironmentVariables.GetPortNumber("SERVER_PORT").WithDefault(9401),
EnvironmentVariables.GetString("CONTROLLER_HOST").Require,
EnvironmentVariables.GetPortNumber("CONTROLLER_PORT").WithDefault(9401),
javaSearchPath,
agentKeyToken,
agentKeyFilePath,

View File

@ -6,6 +6,7 @@ public enum AuditLogEventType {
UserLoggedIn,
UserLoggedOut,
UserCreated,
UserPasswordChanged,
UserRolesChanged,
UserDeleted,
InstanceCreated,
@ -22,6 +23,7 @@ static class AuditLogEventTypeExtensions {
{ AuditLogEventType.UserLoggedIn, AuditLogSubjectType.User },
{ AuditLogEventType.UserLoggedOut, AuditLogSubjectType.User },
{ AuditLogEventType.UserCreated, AuditLogSubjectType.User },
{ AuditLogEventType.UserPasswordChanged, AuditLogSubjectType.User },
{ AuditLogEventType.UserRolesChanged, AuditLogSubjectType.User },
{ AuditLogEventType.UserDeleted, AuditLogSubjectType.User },
{ AuditLogEventType.InstanceCreated, AuditLogSubjectType.Instance },

View File

@ -1,6 +1,6 @@
using System.Collections.Immutable;
namespace Phantom.Controller.Minecraft;
namespace Phantom.Common.Data.Web.Minecraft;
public static class JvmArgumentsHelper {
public static ImmutableArray<string> Split(string arguments) {

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next.StrongName" />
<PackageReference Include="MemoryPack" />
</ItemGroup>
</Project>

View File

@ -1,4 +1,4 @@
namespace Phantom.Controller.Services.Users;
namespace Phantom.Controller.Services.Users.Roles;
public enum AddRoleError : byte {
NameIsEmpty,

View File

@ -0,0 +1,25 @@
using System.Collections.Immutable;
using MemoryPack;
namespace Phantom.Common.Data.Web.Users;
[MemoryPackable]
[MemoryPackUnion(0, typeof(NameIsInvalid))]
[MemoryPackUnion(1, typeof(PasswordIsInvalid))]
[MemoryPackUnion(2, typeof(NameAlreadyExists))]
[MemoryPackUnion(3, typeof(UnknownError))]
public abstract record AddUserError {
private AddUserError() {}
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed record NameIsInvalid([property: MemoryPackOrder(0)] UsernameRequirementViolation Violation) : AddUserError;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed record PasswordIsInvalid([property: MemoryPackOrder(0)] ImmutableArray<PasswordRequirementViolation> Violations) : AddUserError;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed record NameAlreadyExists : AddUserError;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed record UnknownError : AddUserError;
}

View File

@ -0,0 +1,28 @@
using MemoryPack;
namespace Phantom.Common.Data.Web.Users;
[MemoryPackable]
[MemoryPackUnion(0, typeof(Success))]
[MemoryPackUnion(1, typeof(CreationFailed))]
[MemoryPackUnion(2, typeof(UpdatingFailed))]
[MemoryPackUnion(3, typeof(AddingToRoleFailed))]
[MemoryPackUnion(4, typeof(UnknownError))]
public abstract partial record CreateOrUpdateAdministratorUserResult {
private CreateOrUpdateAdministratorUserResult() {}
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record Success([property: MemoryPackOrder(0)] UserInfo User) : CreateOrUpdateAdministratorUserResult;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record CreationFailed([property: MemoryPackOrder(0)] AddUserError Error) : CreateOrUpdateAdministratorUserResult;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record UpdatingFailed([property: MemoryPackOrder(0)] SetUserPasswordError Error) : CreateOrUpdateAdministratorUserResult;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record AddingToRoleFailed : CreateOrUpdateAdministratorUserResult;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record UnknownError : CreateOrUpdateAdministratorUserResult;
}

View File

@ -1,4 +1,4 @@
namespace Phantom.Controller.Services.Users;
namespace Phantom.Common.Data.Web.Users;
public enum DeleteUserResult : byte {
Deleted,

View File

@ -0,0 +1,25 @@
using System.Collections.Immutable;
namespace Phantom.Common.Data.Web.Users.Permissions;
public sealed class IdentityPermissions {
public static IdentityPermissions None { get; } = new (ImmutableHashSet<string>.Empty);
private readonly ImmutableHashSet<string> permissionIds;
public IdentityPermissions(ImmutableHashSet<string> permissionIdsQuery) {
this.permissionIds = permissionIdsQuery;
}
public bool Check(Permission? permission) {
while (permission != null) {
if (!permissionIds.Contains(permission.Id)) {
return false;
}
permission = permission.Parent;
}
return true;
}
}

View File

@ -0,0 +1,24 @@
using MemoryPack;
namespace Phantom.Common.Data.Web.Users;
[MemoryPackable]
[MemoryPackUnion(0, typeof(TooShort))]
[MemoryPackUnion(1, typeof(LowercaseLetterRequired))]
[MemoryPackUnion(2, typeof(UppercaseLetterRequired))]
[MemoryPackUnion(3, typeof(DigitRequired))]
public abstract record PasswordRequirementViolation {
private PasswordRequirementViolation() {}
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed record TooShort([property: MemoryPackOrder(0)] int MinimumLength) : PasswordRequirementViolation;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed record LowercaseLetterRequired : PasswordRequirementViolation;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed record UppercaseLetterRequired : PasswordRequirementViolation;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed record DigitRequired : PasswordRequirementViolation;
}

View File

@ -1,4 +1,4 @@
namespace Phantom.Web.Identity.Data;
namespace Phantom.Common.Data.Web.Users.Permissions;
public sealed record Permission(string Id, Permission? Parent) {
private static readonly List<Permission> AllPermissions = new ();

View File

@ -0,0 +1,9 @@
using MemoryPack;
namespace Phantom.Common.Data.Web.Users;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record RoleInfo(
[property: MemoryPackOrder(0)] Guid Guid,
[property: MemoryPackOrder(1)] string Name
);

View File

@ -1,13 +1,21 @@
using System.Collections.Immutable;
using MemoryPack;
namespace Phantom.Controller.Services.Users;
namespace Phantom.Common.Data.Web.Users;
[MemoryPackable]
[MemoryPackUnion(0, typeof(UserNotFound))]
[MemoryPackUnion(1, typeof(PasswordIsInvalid))]
[MemoryPackUnion(2, typeof(UnknownError))]
public abstract record SetUserPasswordError {
private SetUserPasswordError() {}
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed record UserNotFound : SetUserPasswordError;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed record PasswordIsInvalid(ImmutableArray<PasswordRequirementViolation> Violations) : SetUserPasswordError;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed record UnknownError : SetUserPasswordError;
}

View File

@ -0,0 +1,9 @@
using MemoryPack;
namespace Phantom.Common.Data.Web.Users;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record UserInfo(
[property: MemoryPackOrder(0)] Guid Guid,
[property: MemoryPackOrder(1)] string Name
);

View File

@ -0,0 +1,12 @@
namespace Phantom.Common.Data.Web.Users;
public static class UserPasswords {
public static string Hash(string password) {
return BCrypt.Net.BCrypt.HashPassword(password, workFactor: 12);
}
public static bool Verify(string password, string hash) {
// TODO rehash
return BCrypt.Net.BCrypt.Verify(password, hash);
}
}

View File

@ -0,0 +1,16 @@
using MemoryPack;
namespace Phantom.Common.Data.Web.Users;
[MemoryPackable]
[MemoryPackUnion(0, typeof(IsEmpty))]
[MemoryPackUnion(1, typeof(TooLong))]
public abstract record UsernameRequirementViolation {
private UsernameRequirementViolation() {}
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed record IsEmpty : UsernameRequirementViolation;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed record TooLong(int MaxLength) : UsernameRequirementViolation;
}

View File

@ -1,18 +0,0 @@
namespace Phantom.Common.Data.Agent;
public static class AgentKeyData {
private const byte TokenLength = AgentAuthToken.Length;
public static byte[] ToBytes(byte[] publicKey, AgentAuthToken agentToken) {
Span<byte> agentKey = stackalloc byte[TokenLength + publicKey.Length];
agentToken.WriteTo(agentKey[..TokenLength]);
publicKey.CopyTo(agentKey[TokenLength..]);
return agentKey.ToArray();
}
public static (byte[] PublicKey, AgentAuthToken AgentToken) FromBytes(byte[] agentKey) {
var token = new AgentAuthToken(agentKey[..TokenLength]);
var publicKey = agentKey[TokenLength..];
return (publicKey, token);
}
}

View File

@ -6,14 +6,14 @@ namespace Phantom.Common.Data.Agent;
[MemoryPackable(GenerateType.VersionTolerant)]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public sealed partial class AgentAuthToken {
public sealed partial class AuthToken {
internal const int Length = 12;
[MemoryPackOrder(0)]
[MemoryPackInclude]
private readonly byte[] bytes;
internal AgentAuthToken(byte[]? bytes) {
internal AuthToken(byte[]? bytes) {
if (bytes == null) {
throw new ArgumentNullException(nameof(bytes));
}
@ -25,7 +25,7 @@ public sealed partial class AgentAuthToken {
this.bytes = bytes;
}
public bool FixedTimeEquals(AgentAuthToken providedAuthToken) {
public bool FixedTimeEquals(AuthToken providedAuthToken) {
return CryptographicOperations.FixedTimeEquals(bytes, providedAuthToken.bytes);
}
@ -33,7 +33,7 @@ public sealed partial class AgentAuthToken {
bytes.CopyTo(span);
}
public static AgentAuthToken Generate() {
return new AgentAuthToken(RandomNumberGenerator.GetBytes(Length));
public static AuthToken Generate() {
return new AuthToken(RandomNumberGenerator.GetBytes(Length));
}
}

View File

@ -0,0 +1,18 @@
namespace Phantom.Common.Data.Agent;
public readonly record struct ConnectionCommonKey(byte[] CertificatePublicKey, AuthToken AuthToken) {
private const byte TokenLength = AuthToken.Length;
public byte[] ToBytes() {
Span<byte> result = stackalloc byte[TokenLength + CertificatePublicKey.Length];
AuthToken.WriteTo(result[..TokenLength]);
CertificatePublicKey.CopyTo(result[TokenLength..]);
return result.ToArray();
}
public static ConnectionCommonKey FromBytes(byte[] agentKey) {
var authToken = new AuthToken(agentKey[..TokenLength]);
var certificatePublicKey = agentKey[TokenLength..];
return new ConnectionCommonKey(certificatePublicKey, authToken);
}
}

View File

@ -27,7 +27,7 @@ public static class PhantomLogger {
}
public static ILogger Create(string name1, string name2) {
return Create(name1 + ":" + name2);
return Create(ConcatNames(name1, name2));
}
public static ILogger Create<T>() {
@ -37,11 +37,19 @@ public static class PhantomLogger {
public static ILogger Create<T>(string name) {
return Create(typeof(T).Name, name);
}
public static ILogger Create<T>(string name1, string name2) {
return Create(typeof(T).Name, ConcatNames(name1, name2));
}
public static ILogger Create<T1, T2>() {
return Create(typeof(T1).Name, typeof(T2).Name);
}
private static string ConcatNames(string name1, string name2) {
return name1 + ":" + name2;
}
public static void Dispose() {
Root.Dispose();
Base.Dispose();

View File

@ -0,0 +1,48 @@
using Phantom.Common.Data.Replies;
using Phantom.Common.Logging;
using Phantom.Common.Messages.Agent.BiDirectional;
using Phantom.Common.Messages.Agent.ToAgent;
using Phantom.Common.Messages.Agent.ToController;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.Agent;
public static class AgentMessageRegistries {
public static MessageRegistry<IMessageToAgentListener> ToAgent { get; } = new (PhantomLogger.Create("MessageRegistry", nameof(ToAgent)));
public static MessageRegistry<IMessageToControllerListener> ToController { get; } = new (PhantomLogger.Create("MessageRegistry", nameof(ToController)));
public static IMessageDefinitions<IMessageToAgentListener, IMessageToControllerListener> Definitions { get; } = new MessageDefinitions();
static AgentMessageRegistries() {
ToAgent.Add<RegisterAgentSuccessMessage>(0);
ToAgent.Add<RegisterAgentFailureMessage>(1);
ToAgent.Add<ConfigureInstanceMessage, InstanceActionResult<ConfigureInstanceResult>>(2);
ToAgent.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(3);
ToAgent.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(4);
ToAgent.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(5);
ToAgent.Add<ReplyMessage>(127);
ToController.Add<RegisterAgentMessage>(0);
ToController.Add<UnregisterAgentMessage>(1);
ToController.Add<AgentIsAliveMessage>(2);
ToController.Add<AdvertiseJavaRuntimesMessage>(3);
ToController.Add<ReportInstanceStatusMessage>(4);
ToController.Add<InstanceOutputMessage>(5);
ToController.Add<ReportAgentStatusMessage>(6);
ToController.Add<ReportInstanceEventMessage>(7);
ToController.Add<ReplyMessage>(127);
}
private sealed class MessageDefinitions : IMessageDefinitions<IMessageToAgentListener, IMessageToControllerListener> {
public MessageRegistry<IMessageToAgentListener> Outgoing => ToAgent;
public MessageRegistry<IMessageToControllerListener> Incoming => ToController;
public bool IsRegistrationMessage(Type messageType) {
return messageType == typeof(RegisterAgentMessage);
}
public IMessage<IMessageToAgentListener, NoReply> CreateReplyMessage( uint sequenceId, byte[] serializedReply) {
return new ReplyMessage(sequenceId, serializedReply);
}
}
}

View File

@ -1,14 +1,14 @@
using MemoryPack;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.BiDirectional;
namespace Phantom.Common.Messages.Agent.BiDirectional;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record ReplyMessage(
[property: MemoryPackOrder(0)] uint SequenceId,
[property: MemoryPackOrder(1)] byte[] SerializedReply
) : IMessageToServer, IMessageToAgent {
public Task<NoReply> Accept(IMessageToServerListener listener) {
) : IMessageToController, IMessageToAgent, IReply {
public Task<NoReply> Accept(IMessageToControllerListener listener) {
return listener.HandleReply(this);
}

View File

@ -1,6 +1,6 @@
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages;
namespace Phantom.Common.Messages.Agent;
public interface IMessageToAgent<TReply> : IMessage<IMessageToAgentListener, TReply> {}

View File

@ -1,9 +1,9 @@
using Phantom.Common.Data.Replies;
using Phantom.Common.Messages.BiDirectional;
using Phantom.Common.Messages.ToAgent;
using Phantom.Common.Messages.Agent.BiDirectional;
using Phantom.Common.Messages.Agent.ToAgent;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages;
namespace Phantom.Common.Messages.Agent;
public interface IMessageToAgentListener {
Task<NoReply> HandleRegisterAgentSuccess(RegisterAgentSuccessMessage message);

View File

@ -0,0 +1,7 @@
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.Agent;
public interface IMessageToController<TReply> : IMessage<IMessageToControllerListener, TReply> {}
public interface IMessageToController : IMessageToController<NoReply> {}

View File

@ -1,11 +1,10 @@
using Phantom.Common.Messages.BiDirectional;
using Phantom.Common.Messages.ToServer;
using Phantom.Common.Messages.Agent.BiDirectional;
using Phantom.Common.Messages.Agent.ToController;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages;
namespace Phantom.Common.Messages.Agent;
public interface IMessageToServerListener {
bool IsDisposed { get; }
public interface IMessageToControllerListener {
Task<NoReply> HandleRegisterAgent(RegisterAgentMessage message);
Task<NoReply> HandleUnregisterAgent(UnregisterAgentMessage message);
Task<NoReply> HandleAgentIsAlive(AgentIsAliveMessage message);

View File

@ -2,7 +2,7 @@
using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Replies;
namespace Phantom.Common.Messages.ToAgent;
namespace Phantom.Common.Messages.Agent.ToAgent;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record ConfigureInstanceMessage(

View File

@ -1,7 +1,7 @@
using MemoryPack;
using Phantom.Common.Data.Replies;
namespace Phantom.Common.Messages.ToAgent;
namespace Phantom.Common.Messages.Agent.ToAgent;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record LaunchInstanceMessage(

View File

@ -2,7 +2,7 @@
using Phantom.Common.Data.Replies;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.ToAgent;
namespace Phantom.Common.Messages.Agent.ToAgent;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record RegisterAgentFailureMessage(

View File

@ -2,7 +2,7 @@
using MemoryPack;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.ToAgent;
namespace Phantom.Common.Messages.Agent.ToAgent;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record RegisterAgentSuccessMessage(

View File

@ -1,7 +1,7 @@
using MemoryPack;
using Phantom.Common.Data.Replies;
namespace Phantom.Common.Messages.ToAgent;
namespace Phantom.Common.Messages.Agent.ToAgent;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record SendCommandToInstanceMessage(

View File

@ -2,7 +2,7 @@
using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies;
namespace Phantom.Common.Messages.ToAgent;
namespace Phantom.Common.Messages.Agent.ToAgent;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record StopInstanceMessage(

View File

@ -3,13 +3,13 @@ using MemoryPack;
using Phantom.Common.Data.Java;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.ToServer;
namespace Phantom.Common.Messages.Agent.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record AdvertiseJavaRuntimesMessage(
[property: MemoryPackOrder(0)] ImmutableArray<TaggedJavaRuntime> Runtimes
) : IMessageToServer {
public Task<NoReply> Accept(IMessageToServerListener listener) {
) : IMessageToController {
public Task<NoReply> Accept(IMessageToControllerListener listener) {
return listener.HandleAdvertiseJavaRuntimes(this);
}
}

View File

@ -0,0 +1,11 @@
using MemoryPack;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.Agent.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record AgentIsAliveMessage : IMessageToController {
public Task<NoReply> Accept(IMessageToControllerListener listener) {
return listener.HandleAgentIsAlive(this);
}
}

View File

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

View File

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

View File

@ -2,14 +2,14 @@
using Phantom.Common.Data;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.ToServer;
namespace Phantom.Common.Messages.Agent.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record ReportAgentStatusMessage(
[property: MemoryPackOrder(0)] int RunningInstanceCount,
[property: MemoryPackOrder(1)] RamAllocationUnits RunningInstanceMemory
) : IMessageToServer {
public Task<NoReply> Accept(IMessageToServerListener listener) {
) : IMessageToController {
public Task<NoReply> Accept(IMessageToControllerListener listener) {
return listener.HandleReportAgentStatus(this);
}
}

View File

@ -2,7 +2,7 @@
using Phantom.Common.Data.Instance;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.ToServer;
namespace Phantom.Common.Messages.Agent.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record ReportInstanceEventMessage(
@ -10,8 +10,8 @@ public sealed partial record ReportInstanceEventMessage(
[property: MemoryPackOrder(1)] DateTime UtcTime,
[property: MemoryPackOrder(2)] Guid InstanceGuid,
[property: MemoryPackOrder(3)] IInstanceEvent Event
) : IMessageToServer {
public Task<NoReply> Accept(IMessageToServerListener listener) {
) : IMessageToController {
public Task<NoReply> Accept(IMessageToControllerListener listener) {
return listener.HandleReportInstanceEvent(this);
}
}

View File

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

View File

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

View File

@ -0,0 +1,18 @@
using MemoryPack;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.Web.BiDirectional;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record ReplyMessage(
[property: MemoryPackOrder(0)] uint SequenceId,
[property: MemoryPackOrder(1)] byte[] SerializedReply
) : IMessageToController, IMessageToWeb, IReply {
public Task<NoReply> Accept(IMessageToControllerListener listener) {
return listener.HandleReply(this);
}
public Task<NoReply> Accept(IMessageToWebListener listener) {
return listener.HandleReply(this);
}
}

View File

@ -0,0 +1,7 @@
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.Web;
public interface IMessageToController<TReply> : IMessage<IMessageToControllerListener, TReply> {}
public interface IMessageToController : IMessageToController<NoReply> {}

View File

@ -0,0 +1,11 @@
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Messages.Web.BiDirectional;
using Phantom.Common.Messages.Web.ToController;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.Web;
public interface IMessageToControllerListener {
Task<CreateOrUpdateAdministratorUserResult> CreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUser message);
Task<NoReply> HandleReply(ReplyMessage message);
}

View File

@ -0,0 +1,7 @@
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.Web;
public interface IMessageToWeb<TReply> : IMessage<IMessageToWebListener, TReply> {}
public interface IMessageToWeb : IMessageToWeb<NoReply> {}

View File

@ -0,0 +1,8 @@
using Phantom.Common.Messages.Web.BiDirectional;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.Web;
public interface IMessageToWebListener {
Task<NoReply> HandleReply(ReplyMessage message);
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Phantom.Common.Logging\Phantom.Common.Logging.csproj" />
<ProjectReference Include="..\Phantom.Common.Data\Phantom.Common.Data.csproj" />
<ProjectReference Include="..\Phantom.Common.Data.Web\Phantom.Common.Data.Web.csproj" />
<ProjectReference Include="..\..\Utils\Phantom.Utils.Rpc\Phantom.Utils.Rpc.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,14 @@
using MemoryPack;
using Phantom.Common.Data.Web.Users;
namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record CreateOrUpdateAdministratorUser(
[property: MemoryPackOrder(0)] string Username,
[property: MemoryPackOrder(1)] string Password
) : IMessageToController<CreateOrUpdateAdministratorUserResult> {
public Task<CreateOrUpdateAdministratorUserResult> Accept(IMessageToControllerListener listener) {
return listener.CreateOrUpdateAdministratorUser(this);
}
}

View File

@ -0,0 +1,34 @@
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Logging;
using Phantom.Common.Messages.Web.BiDirectional;
using Phantom.Common.Messages.Web.ToController;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.Web;
public static class WebMessageRegistries {
public static MessageRegistry<IMessageToControllerListener> ToController { get; } = new (PhantomLogger.Create("MessageRegistry", nameof(ToController)));
public static MessageRegistry<IMessageToWebListener> ToWeb { get; } = new (PhantomLogger.Create("MessageRegistry", nameof(ToWeb)));
public static IMessageDefinitions<IMessageToWebListener, IMessageToControllerListener> Definitions { get; } = new MessageDefinitions();
static WebMessageRegistries() {
ToController.Add<CreateOrUpdateAdministratorUser, CreateOrUpdateAdministratorUserResult>(1);
ToController.Add<ReplyMessage>(127);
ToWeb.Add<ReplyMessage>(127);
}
private sealed class MessageDefinitions : IMessageDefinitions<IMessageToWebListener, IMessageToControllerListener> {
public MessageRegistry<IMessageToWebListener> Outgoing => ToWeb;
public MessageRegistry<IMessageToControllerListener> Incoming => ToController;
public bool IsRegistrationMessage(Type messageType) {
return false;
}
public IMessage<IMessageToWebListener, NoReply> CreateReplyMessage( uint sequenceId, byte[] serializedReply) {
return new ReplyMessage(sequenceId, serializedReply);
}
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,32 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
namespace Phantom.Controller.Database.Postgres;
public sealed class ApplicationDbContextFactory : IDbContextProvider {
private readonly PooledDbContextFactory<ApplicationDbContext> factory;
public ApplicationDbContextFactory(string connectionString) {
this.factory = new PooledDbContextFactory<ApplicationDbContext>(CreateOptions(connectionString), poolSize: 32);
}
public ApplicationDbContext Eager() {
return factory.CreateDbContext();
}
public ILazyDbContext Lazy() {
return new LazyDbContext(this);
}
private static DbContextOptions<ApplicationDbContext> CreateOptions(string connectionString) {
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
builder.UseNpgsql(connectionString, ConfigureOptions);
return builder.Options;
}
private static void ConfigureOptions(NpgsqlDbContextOptionsBuilder options) {
options.CommandTimeout(10);
options.MigrationsAssembly(typeof(ApplicationDbContextDesignFactory).Assembly.FullName);
}
}

View File

@ -0,0 +1,16 @@
namespace Phantom.Controller.Database.Postgres;
sealed class LazyDbContext : ILazyDbContext {
public ApplicationDbContext Ctx => cachedContext ??= contextFactory.Eager();
private readonly ApplicationDbContextFactory contextFactory;
private ApplicationDbContext? cachedContext;
internal LazyDbContext(ApplicationDbContextFactory contextFactory) {
this.contextFactory = contextFactory;
}
public ValueTask DisposeAsync() {
return cachedContext?.DisposeAsync() ?? ValueTask.CompletedTask;
}
}

View File

@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore;
using Phantom.Common.Logging;
using Phantom.Utils.Tasks;
using Serilog;
namespace Phantom.Controller.Database;
public static class DatabaseMigrator {
private static readonly ILogger Logger = PhantomLogger.Create(nameof(DatabaseMigrator));
public static async Task Run(IDbContextProvider dbProvider, CancellationToken cancellationToken) {
await using var ctx = dbProvider.Eager();
Logger.Information("Connecting to database...");
var retryConnection = new Throttler(TimeSpan.FromSeconds(10));
while (!await ctx.Database.CanConnectAsync(cancellationToken)) {
Logger.Warning("Cannot connect to database, retrying...");
await retryConnection.Wait();
}
Logger.Information("Running migrations...");
await ctx.Database.MigrateAsync(CancellationToken.None);
}
}

View File

@ -1,30 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
namespace Phantom.Controller.Database;
public sealed class DatabaseProvider {
private readonly IServiceScopeFactory serviceScopeFactory;
public DatabaseProvider(IServiceScopeFactory serviceScopeFactory) {
this.serviceScopeFactory = serviceScopeFactory;
}
public Scope CreateScope() {
return new Scope(serviceScopeFactory.CreateScope());
}
public readonly struct Scope : IDisposable {
private readonly IServiceScope scope;
public ApplicationDbContext Ctx { get; }
internal Scope(IServiceScope scope) {
this.scope = scope;
this.Ctx = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
}
public void Dispose() {
scope.Dispose();
}
}
}

View File

@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Phantom.Common.Data.Web.Users;
namespace Phantom.Controller.Database.Entities;
@ -11,9 +12,13 @@ public sealed class UserEntity {
public string Name { get; set; }
public string PasswordHash { get; set; }
public UserEntity(Guid userGuid, string name) {
public UserEntity(Guid userGuid, string name, string passwordHash) {
UserGuid = userGuid;
Name = name;
PasswordHash = null!;
PasswordHash = passwordHash;
}
public UserInfo ToUserInfo() {
return new UserInfo(UserGuid, Name);
}
}

View File

@ -0,0 +1,6 @@
namespace Phantom.Controller.Database;
public interface IDbContextProvider {
ApplicationDbContext Eager();
ILazyDbContext Lazy();
}

View File

@ -0,0 +1,5 @@
namespace Phantom.Controller.Database;
public interface ILazyDbContext : IAsyncDisposable {
ApplicationDbContext Ctx { get; }
}

View File

@ -15,6 +15,8 @@
<ItemGroup>
<ProjectReference Include="..\..\Common\Phantom.Common.Data\Phantom.Common.Data.csproj" />
<ProjectReference Include="..\..\Common\Phantom.Common.Data.Web\Phantom.Common.Data.Web.csproj" />
<ProjectReference Include="..\..\Common\Phantom.Common.Logging\Phantom.Common.Logging.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,66 @@
using Phantom.Controller.Database.Entities;
using Phantom.Controller.Database.Enums;
namespace Phantom.Controller.Database.Repositories;
sealed partial class AuditLogRepository {
public void AddUserLoggedInEvent(UserEntity user) {
AddItem(AuditLogEventType.UserLoggedIn, user.UserGuid.ToString());
}
public void AddUserLoggedOutEvent(Guid userGuid) {
AddItem(AuditLogEventType.UserLoggedOut, userGuid.ToString());
}
public void AddUserCreatedEvent(UserEntity user) {
AddItem(AuditLogEventType.UserCreated, user.UserGuid.ToString());
}
public void AddUserPasswordChangedEvent(UserEntity user) {
AddItem(AuditLogEventType.UserCreated, user.UserGuid.ToString());
}
public void AddUserRolesChangedEvent(UserEntity user, List<string> addedToRoles, List<string> removedFromRoles) {
var extra = new Dictionary<string, object?>();
if (addedToRoles.Count > 0) {
extra["addedToRoles"] = addedToRoles;
}
if (removedFromRoles.Count > 0) {
extra["removedFromRoles"] = removedFromRoles;
}
AddItem(AuditLogEventType.UserRolesChanged, user.UserGuid.ToString(), extra);
}
public void AddUserDeletedEvent(UserEntity user) {
AddItem(AuditLogEventType.UserDeleted, user.UserGuid.ToString(), new Dictionary<string, object?> {
{ "username", user.Name }
});
}
public void AddInstanceCreatedEvent(Guid instanceGuid) {
AddItem(AuditLogEventType.InstanceCreated, instanceGuid.ToString());
}
public void AddInstanceEditedEvent(Guid instanceGuid) {
AddItem(AuditLogEventType.InstanceEdited, instanceGuid.ToString());
}
public void AddInstanceLaunchedEvent(Guid instanceGuid) {
AddItem(AuditLogEventType.InstanceLaunched, instanceGuid.ToString());
}
public void AddInstanceCommandExecutedEvent(Guid instanceGuid, string command) {
AddItem(AuditLogEventType.InstanceCommandExecuted, instanceGuid.ToString(), new Dictionary<string, object?> {
{ "command", command }
});
}
public void AddInstanceStoppedEvent(Guid instanceGuid, int stopInSeconds) {
AddItem(AuditLogEventType.InstanceStopped, instanceGuid.ToString(), new Dictionary<string, object?> {
{ "stop_in_seconds", stopInSeconds.ToString() }
});
}
}

View File

@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore;
using Phantom.Controller.Database.Entities;
using Phantom.Controller.Database.Enums;
using Phantom.Controller.Services.Audit;
namespace Phantom.Controller.Database.Repositories;
public sealed partial class AuditLogRepository {
private readonly ILazyDbContext db;
private readonly Guid? currentUserGuid;
public AuditLogRepository(ILazyDbContext db, Guid? currentUserGuid) {
this.db = db;
this.currentUserGuid = currentUserGuid;
}
private void AddItem(AuditLogEventType eventType, string subjectId, Dictionary<string, object?>? extra = null) {
db.Ctx.AuditLog.Add(new AuditLogEntity(currentUserGuid, eventType, subjectId, extra));
}
public Task<AuditLogItem[]> GetItems(int count, CancellationToken cancellationToken) {
return db.Ctx
.AuditLog
.Include(static entity => entity.User)
.AsQueryable()
.OrderByDescending(static entity => entity.UtcTime)
.Take(count)
.Select(static entity => new AuditLogItem(entity.UtcTime, entity.UserGuid, entity.User == null ? null : entity.User.Name, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data))
.ToArrayAsync(cancellationToken);
}
}

View File

@ -0,0 +1,48 @@
using System.Collections.Immutable;
using Microsoft.EntityFrameworkCore;
using Phantom.Controller.Database;
using Phantom.Controller.Database.Entities;
using Phantom.Controller.Services.Users.Roles;
using Phantom.Utils.Collections;
using Phantom.Utils.Tasks;
namespace Phantom.Controller.Services.Users;
public sealed class RoleRepository {
private const int MaxRoleNameLength = 40;
private readonly ILazyDbContext db;
public RoleRepository(ILazyDbContext db) {
this.db = db;
}
public Task<List<RoleEntity>> GetAll() {
return db.Ctx.Roles.ToListAsync();
}
public Task<ImmutableHashSet<string>> GetAllNames() {
return db.Ctx.Roles.Select(static role => role.Name).AsAsyncEnumerable().ToImmutableSetAsync();
}
public ValueTask<RoleEntity?> GetByGuid(Guid guid) {
return db.Ctx.Roles.FindAsync(guid);
}
public async Task<Result<RoleEntity, AddRoleError>> Create(string name) {
if (string.IsNullOrWhiteSpace(name)) {
return AddRoleError.NameIsEmpty;
}
else if (name.Length > MaxRoleNameLength) {
return AddRoleError.NameIsTooLong;
}
if (await db.Ctx.Roles.AnyAsync(role => role.Name == name)) {
return AddRoleError.NameAlreadyExists;
}
var role = new RoleEntity(Guid.NewGuid(), name);
db.Ctx.Roles.Add(role);
return role;
}
}

View File

@ -0,0 +1,112 @@
using System.Collections.Immutable;
using Microsoft.EntityFrameworkCore;
using Phantom.Common.Data.Web.Users;
using Phantom.Controller.Database.Entities;
using Phantom.Utils.Collections;
using Phantom.Utils.Tasks;
namespace Phantom.Controller.Database.Repositories;
public sealed class UserRepository {
private const int MaxUserNameLength = 40;
private const int MinimumPasswordLength = 16;
private static UsernameRequirementViolation? CheckUsernameRequirements(string username) {
if (string.IsNullOrWhiteSpace(username)) {
return new UsernameRequirementViolation.IsEmpty();
}
else if (username.Length > MaxUserNameLength) {
return new UsernameRequirementViolation.TooLong(MaxUserNameLength);
}
else {
return null;
}
}
private static ImmutableArray<PasswordRequirementViolation> CheckPasswordRequirements(string password) {
var violations = ImmutableArray.CreateBuilder<PasswordRequirementViolation>();
if (password.Length < MinimumPasswordLength) {
violations.Add(new PasswordRequirementViolation.TooShort(MinimumPasswordLength));
}
if (!password.Any(char.IsLower)) {
violations.Add(new PasswordRequirementViolation.LowercaseLetterRequired());
}
if (!password.Any(char.IsUpper)) {
violations.Add(new PasswordRequirementViolation.UppercaseLetterRequired());
}
if (!password.Any(char.IsDigit)) {
violations.Add(new PasswordRequirementViolation.DigitRequired());
}
return violations.ToImmutable();
}
private readonly ILazyDbContext db;
private AuditLogRepository? auditLog;
private AuditLogRepository AuditLogRepository => this.auditLog ??= new AuditLogRepository(db, null);
public UserRepository(ILazyDbContext db) {
this.db = db;
}
public Task<ImmutableArray<UserEntity>> GetAll() {
return db.Ctx.Users.AsAsyncEnumerable().ToImmutableArrayAsync();
}
public Task<Dictionary<Guid, T>> GetAllByGuid<T>(Func<UserEntity, T> valueSelector, CancellationToken cancellationToken = default) {
return db.Ctx.Users.ToDictionaryAsync(static user => user.UserGuid, valueSelector, cancellationToken);
}
public async Task<UserEntity?> GetByGuid(Guid guid) {
return await db.Ctx.Users.FindAsync(guid);
}
public Task<UserEntity?> GetByName(string username) {
return db.Ctx.Users.FirstOrDefaultAsync(user => user.Name == username);
}
public async Task<Result<UserEntity, AddUserError>> CreateUser(string username, string password) {
var usernameRequirementViolation = CheckUsernameRequirements(username);
if (usernameRequirementViolation != null) {
return new AddUserError.NameIsInvalid(usernameRequirementViolation);
}
var passwordRequirementViolations = CheckPasswordRequirements(password);
if (!passwordRequirementViolations.IsEmpty) {
return new AddUserError.PasswordIsInvalid(passwordRequirementViolations);
}
if (await db.Ctx.Users.AnyAsync(user => user.Name == username)) {
return new AddUserError.NameAlreadyExists();
}
var user = new UserEntity(Guid.NewGuid(), username, UserPasswords.Hash(password));
db.Ctx.Users.Add(user);
AuditLogRepository.AddUserCreatedEvent(user);
return user;
}
public Result<SetUserPasswordError> SetUserPassword(UserEntity user, string password) {
var requirementViolations = CheckPasswordRequirements(password);
if (!requirementViolations.IsEmpty) {
return new SetUserPasswordError.PasswordIsInvalid(requirementViolations);
}
user.PasswordHash = UserPasswords.Hash(password);
AuditLogRepository.AddUserPasswordChangedEvent(user);
return Result.Ok<SetUserPasswordError>();
}
public void DeleteUser(UserEntity user) {
db.Ctx.Users.Remove(user);
AuditLogRepository.AddUserDeletedEvent(user);
}
}

View File

@ -0,0 +1,56 @@
using System.Collections.Immutable;
using Microsoft.EntityFrameworkCore;
using Phantom.Controller.Database.Entities;
using Phantom.Utils.Collections;
namespace Phantom.Controller.Database.Repositories;
public sealed class UserRoleRepository {
private readonly ILazyDbContext db;
public UserRoleRepository(ILazyDbContext db) {
this.db = db;
}
public Task<Dictionary<Guid, ImmutableArray<RoleEntity>>> GetAllByUserGuid() {
return db.Ctx.UserRoles
.Include(static ur => ur.Role)
.GroupBy(static ur => ur.UserGuid, static ur => ur.Role)
.ToDictionaryAsync(static group => group.Key, static group => group.ToImmutableArray());
}
public Task<ImmutableArray<RoleEntity>> GetUserRoles(UserEntity user) {
return db.Ctx.UserRoles
.Include(static ur => ur.Role)
.Where(ur => ur.UserGuid == user.UserGuid)
.Select(static ur => ur.Role)
.AsAsyncEnumerable()
.ToImmutableArrayAsync();
}
public Task<ImmutableHashSet<Guid>> GetUserRoleGuids(UserEntity user) {
return db.Ctx.UserRoles
.Where(ur => ur.UserGuid == user.UserGuid)
.Select(static ur => ur.RoleGuid)
.AsAsyncEnumerable()
.ToImmutableSetAsync();
}
public async Task Add(UserEntity user, RoleEntity role) {
var userRole = await db.Ctx.UserRoles.FindAsync(user.UserGuid, role.RoleGuid);
if (userRole == null) {
db.Ctx.UserRoles.Add(new UserRoleEntity(user.UserGuid, role.RoleGuid));
}
}
public async Task<UserRoleEntity?> Remove(UserEntity user, RoleEntity role) {
var userRole = await db.Ctx.UserRoles.FindAsync(user.UserGuid, role.RoleGuid);
if (userRole == null) {
return null;
}
else {
db.Ctx.UserRoles.Remove(userRole);
return userRole;
}
}
}

View File

@ -6,7 +6,8 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Common\Phantom.Common.Messages\Phantom.Common.Messages.csproj" />
<ProjectReference Include="..\..\Common\Phantom.Common.Messages.Agent\Phantom.Common.Messages.Agent.csproj" />
<ProjectReference Include="..\..\Common\Phantom.Common.Messages.Web\Phantom.Common.Messages.Web.csproj" />
</ItemGroup>
</Project>

View File

@ -1,28 +1,29 @@
using NetMQ;
using NetMQ.Sockets;
using Phantom.Common.Messages;
using Phantom.Common.Messages.BiDirectional;
using Phantom.Common.Messages.Agent.BiDirectional;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Controller.Rpc;
public sealed class RpcClientConnection {
public sealed class RpcClientConnection<TListener> {
private readonly ServerSocket socket;
private readonly uint routingId;
private readonly MessageRegistry<TListener> messageRegistry;
private readonly MessageReplyTracker messageReplyTracker;
internal event EventHandler<RpcClientConnectionClosedEventArgs>? Closed;
private bool isClosed;
internal RpcClientConnection(ServerSocket socket, uint routingId, MessageReplyTracker messageReplyTracker) {
internal RpcClientConnection(ServerSocket socket, uint routingId, MessageRegistry<TListener> messageRegistry, MessageReplyTracker messageReplyTracker) {
this.socket = socket;
this.routingId = routingId;
this.messageRegistry = messageRegistry;
this.messageReplyTracker = messageReplyTracker;
}
public bool IsSame(RpcClientConnection other) {
return this.routingId == other.routingId;
public bool IsSame(RpcClientConnection<TListener> other) {
return this.routingId == other.routingId && this.socket == other.socket;
}
public void Close() {
@ -34,25 +35,25 @@ public sealed class RpcClientConnection {
}
}
public async Task Send<TMessage>(TMessage message) where TMessage : IMessageToAgent {
public async Task Send<TMessage>(TMessage message) where TMessage : IMessage<TListener, NoReply> {
if (isClosed) {
return;
}
var bytes = MessageRegistries.ToAgent.Write(message).ToArray();
var bytes = messageRegistry.Write(message).ToArray();
if (bytes.Length > 0) {
await socket.SendAsync(routingId, bytes);
}
}
public async Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken) where TMessage : IMessageToAgent<TReply> where TReply : class {
public async Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken) where TMessage : IMessage<TListener, TReply> where TReply : class {
if (isClosed) {
return null;
}
var sequenceId = messageReplyTracker.RegisterReply();
var bytes = MessageRegistries.ToAgent.Write<TMessage, TReply>(sequenceId, message).ToArray();
var bytes = messageRegistry.Write<TMessage, TReply>(sequenceId, message).ToArray();
if (bytes.Length == 0) {
messageReplyTracker.ForgetReply(sequenceId);
return null;

View File

@ -1,122 +0,0 @@
using NetMQ.Sockets;
using Phantom.Common.Messages;
using Phantom.Common.Messages.BiDirectional;
using Phantom.Common.Messages.ToServer;
using Phantom.Utils.Rpc;
using Phantom.Utils.Rpc.Message;
using Phantom.Utils.Tasks;
using Serilog;
using Serilog.Events;
namespace Phantom.Controller.Rpc;
public sealed class RpcLauncher : RpcRuntime<ServerSocket> {
public static Task Launch(RpcConfiguration config, Func<RpcClientConnection, IMessageToServerListener> listenerFactory, CancellationToken cancellationToken) {
var socket = new ServerSocket();
var options = socket.Options;
options.CurveServer = true;
options.CurveCertificate = config.ServerCertificate;
return new RpcLauncher(config, socket, listenerFactory, cancellationToken).Launch();
}
private readonly RpcConfiguration config;
private readonly Func<RpcClientConnection, IMessageToServerListener> listenerFactory;
private readonly CancellationToken cancellationToken;
private RpcLauncher(RpcConfiguration config, ServerSocket socket, Func<RpcClientConnection, IMessageToServerListener> listenerFactory, CancellationToken cancellationToken) : base(config, socket) {
this.config = config;
this.listenerFactory = listenerFactory;
this.cancellationToken = cancellationToken;
}
protected override void Connect(ServerSocket socket) {
var logger = config.RuntimeLogger;
var url = config.TcpUrl;
logger.Information("Starting ZeroMQ server on {Url}...", url);
socket.Bind(url);
logger.Information("ZeroMQ server initialized, listening for agent connections on port {Port}.", config.Port);
}
protected override void Run(ServerSocket socket, MessageReplyTracker replyTracker, TaskManager taskManager) {
var logger = config.RuntimeLogger;
var clients = new Dictionary<ulong, Client>();
void OnConnectionClosed(object? sender, RpcClientConnectionClosedEventArgs e) {
clients.Remove(e.RoutingId);
logger.Debug("Closed connection to {RoutingId}.", e.RoutingId);
}
while (!cancellationToken.IsCancellationRequested) {
var (routingId, data) = socket.Receive(cancellationToken);
if (data.Length == 0) {
LogMessageType(logger, routingId, data);
continue;
}
if (!clients.TryGetValue(routingId, out var client)) {
if (!CheckIsAgentRegistrationMessage(data, logger, routingId)) {
continue;
}
var connection = new RpcClientConnection(socket, routingId, replyTracker);
connection.Closed += OnConnectionClosed;
client = new Client(connection, listenerFactory, logger, taskManager, cancellationToken);
clients[routingId] = client;
}
LogMessageType(logger, routingId, data);
MessageRegistries.ToServer.Handle(data, client);
client.CloseIfDisposed();
}
foreach (var client in clients.Values) {
client.Connection.Closed -= OnConnectionClosed;
}
}
private static void LogMessageType(ILogger logger, uint routingId, ReadOnlyMemory<byte> data) {
if (!logger.IsEnabled(LogEventLevel.Verbose)) {
return;
}
if (data.Length > 0 && MessageRegistries.ToServer.TryGetType(data, out var type)) {
logger.Verbose("Received {MessageType} ({Bytes} B) from {RoutingId}.", type.Name, data.Length, routingId);
}
else {
logger.Verbose("Received {Bytes} B message from {RoutingId}.", data.Length, routingId);
}
}
private static bool CheckIsAgentRegistrationMessage(ReadOnlyMemory<byte> data, ILogger logger, uint routingId) {
if (MessageRegistries.ToServer.TryGetType(data, out var type) && type == typeof(RegisterAgentMessage)) {
return true;
}
logger.Warning("Received {MessageType} from a non-registered agent {RoutingId}.", type?.Name ?? "unknown message", routingId);
return false;
}
private sealed class Client : MessageHandler<IMessageToServerListener> {
public RpcClientConnection Connection { get; }
public Client(RpcClientConnection connection, Func<RpcClientConnection, IMessageToServerListener> listenerFactory, ILogger logger, TaskManager taskManager, CancellationToken cancellationToken) : base(listenerFactory(connection), logger, taskManager, cancellationToken) {
Connection = connection;
}
protected override Task SendReply(uint sequenceId, byte[] serializedReply) {
return Connection.Send(new ReplyMessage(sequenceId, serializedReply));
}
public void CloseIfDisposed() {
if (Listener.IsDisposed) {
Connection.Close();
}
}
}
}

View File

@ -0,0 +1,126 @@
using NetMQ.Sockets;
using Phantom.Utils.Rpc;
using Phantom.Utils.Rpc.Message;
using Phantom.Utils.Tasks;
using Serilog;
using Serilog.Events;
namespace Phantom.Controller.Rpc;
public static class RpcRuntime {
public static Task Launch<TOutgoingListener, TIncomingListener>(RpcConfiguration config, IMessageDefinitions<TOutgoingListener, TIncomingListener> messageDefinitions, Func<RpcClientConnection<TOutgoingListener>, TIncomingListener> listenerFactory, CancellationToken cancellationToken) {
return RpcRuntime<TOutgoingListener, TIncomingListener>.Launch(config, messageDefinitions, listenerFactory, cancellationToken);
}
}
internal sealed class RpcRuntime<TOutgoingListener, TIncomingListener> : RpcRuntime<ServerSocket> {
internal static Task Launch(RpcConfiguration config, IMessageDefinitions<TOutgoingListener, TIncomingListener> messageDefinitions, Func<RpcClientConnection<TOutgoingListener>, TIncomingListener> listenerFactory, CancellationToken cancellationToken) {
return new RpcRuntime<TOutgoingListener, TIncomingListener>(config, messageDefinitions, listenerFactory, cancellationToken).Launch();
}
private static ServerSocket CreateSocket(RpcConfiguration config) {
var socket = new ServerSocket();
var options = socket.Options;
options.CurveServer = true;
options.CurveCertificate = config.ServerCertificate;
return socket;
}
private readonly RpcConfiguration config;
private readonly IMessageDefinitions<TOutgoingListener, TIncomingListener> messageDefinitions;
private readonly Func<RpcClientConnection<TOutgoingListener>, TIncomingListener> listenerFactory;
private readonly CancellationToken cancellationToken;
private RpcRuntime(RpcConfiguration config, IMessageDefinitions<TOutgoingListener, TIncomingListener> messageDefinitions, Func<RpcClientConnection<TOutgoingListener>, TIncomingListener> listenerFactory, CancellationToken cancellationToken) : base(config, CreateSocket(config)) {
this.config = config;
this.messageDefinitions = messageDefinitions;
this.listenerFactory = listenerFactory;
this.cancellationToken = cancellationToken;
}
protected override void Connect(ServerSocket socket) {
var logger = config.RuntimeLogger;
var url = config.TcpUrl;
logger.Information("Starting ZeroMQ server on {Url}...", url);
socket.Bind(url);
logger.Information("ZeroMQ server initialized, listening for connections on port {Port}.", config.Port);
}
protected override void Run(ServerSocket socket, MessageReplyTracker replyTracker, TaskManager taskManager) {
var logger = config.RuntimeLogger;
var clients = new Dictionary<ulong, Client>();
void OnConnectionClosed(object? sender, RpcClientConnectionClosedEventArgs e) {
clients.Remove(e.RoutingId);
logger.Debug("Closed connection to {RoutingId}.", e.RoutingId);
}
while (!cancellationToken.IsCancellationRequested) {
var (routingId, data) = socket.Receive(cancellationToken);
if (data.Length == 0) {
LogMessageType(logger, routingId, data);
continue;
}
if (!clients.TryGetValue(routingId, out var client)) {
if (!CheckIsRegistrationMessage(data, logger, routingId)) {
continue;
}
var connection = new RpcClientConnection<TOutgoingListener>(socket, routingId, messageDefinitions.Outgoing, replyTracker);
connection.Closed += OnConnectionClosed;
client = new Client(connection, messageDefinitions, listenerFactory(connection), logger, taskManager, cancellationToken);
clients[routingId] = client;
}
LogMessageType(logger, routingId, data);
messageDefinitions.Incoming.Handle(data, client);
}
foreach (var client in clients.Values) {
client.Connection.Closed -= OnConnectionClosed;
}
}
private void LogMessageType(ILogger logger, uint routingId, ReadOnlyMemory<byte> data) {
if (!logger.IsEnabled(LogEventLevel.Verbose)) {
return;
}
if (data.Length > 0 && messageDefinitions.Incoming.TryGetType(data, out var type)) {
logger.Verbose("Received {MessageType} ({Bytes} B) from {RoutingId}.", type.Name, data.Length, routingId);
}
else {
logger.Verbose("Received {Bytes} B message from {RoutingId}.", data.Length, routingId);
}
}
private bool CheckIsRegistrationMessage(ReadOnlyMemory<byte> data, ILogger logger, uint routingId) {
if (messageDefinitions.Incoming.TryGetType(data, out var type) && messageDefinitions.IsRegistrationMessage(type)) {
return true;
}
logger.Warning("Received {MessageType} from {RoutingId} who is not registered.", type?.Name ?? "unknown message", routingId);
return false;
}
private sealed class Client : MessageHandler<TIncomingListener> {
public RpcClientConnection<TOutgoingListener> Connection { get; }
private readonly IMessageDefinitions<TOutgoingListener, TIncomingListener> messageDefinitions;
public Client(RpcClientConnection<TOutgoingListener> connection, IMessageDefinitions<TOutgoingListener, TIncomingListener> messageDefinitions, TIncomingListener listener, ILogger logger, TaskManager taskManager, CancellationToken cancellationToken) : base(listener, logger, taskManager, cancellationToken) {
this.Connection = connection;
this.messageDefinitions = messageDefinitions;
}
protected override Task SendReply(uint sequenceId, byte[] serializedReply) {
return Connection.Send(messageDefinitions.CreateReplyMessage(sequenceId, serializedReply));
}
}
}

View File

@ -1,16 +1,16 @@
using Phantom.Common.Messages;
using Phantom.Common.Messages.Agent;
using Phantom.Controller.Rpc;
namespace Phantom.Controller.Services.Agents;
sealed class AgentConnection {
private readonly RpcClientConnection connection;
private readonly RpcClientConnection<IMessageToAgentListener> connection;
internal AgentConnection(RpcClientConnection connection) {
internal AgentConnection(RpcClientConnection<IMessageToAgentListener> connection) {
this.connection = connection;
}
public bool IsSame(RpcClientConnection connection) {
public bool IsSame(RpcClientConnection<IMessageToAgentListener> connection) {
return this.connection.IsSame(connection);
}

View File

@ -3,15 +3,15 @@ using Phantom.Common.Data;
using Phantom.Common.Data.Agent;
using Phantom.Common.Data.Replies;
using Phantom.Common.Logging;
using Phantom.Common.Messages;
using Phantom.Common.Messages.ToAgent;
using Phantom.Common.Messages.Agent;
using Phantom.Common.Messages.Agent.ToAgent;
using Phantom.Controller.Database;
using Phantom.Controller.Rpc;
using Phantom.Controller.Services.Instances;
using Phantom.Utils.Collections;
using Phantom.Utils.Events;
using Phantom.Utils.Tasks;
using ILogger = Serilog.ILogger;
using Serilog;
namespace Phantom.Controller.Services.Agents;
@ -26,20 +26,20 @@ public sealed class AgentManager {
public EventSubscribers<ImmutableArray<Agent>> AgentsChanged => agents.Subs;
private readonly CancellationToken cancellationToken;
private readonly AgentAuthToken authToken;
private readonly DatabaseProvider databaseProvider;
private readonly AuthToken authToken;
private readonly IDbContextProvider dbProvider;
public AgentManager(ServiceConfiguration configuration, AgentAuthToken authToken, DatabaseProvider databaseProvider, TaskManager taskManager) {
this.cancellationToken = configuration.CancellationToken;
public AgentManager(AuthToken authToken, IDbContextProvider dbProvider, TaskManager taskManager, CancellationToken cancellationToken) {
this.authToken = authToken;
this.databaseProvider = databaseProvider;
this.dbProvider = dbProvider;
this.cancellationToken = cancellationToken;
taskManager.Run("Refresh agent status loop", RefreshAgentStatus);
}
public async Task Initialize() {
using var scope = databaseProvider.CreateScope();
internal async Task Initialize() {
await using var ctx = dbProvider.Eager();
await foreach (var entity in scope.Ctx.Agents.AsAsyncEnumerable().WithCancellation(cancellationToken)) {
await foreach (var entity in ctx.Agents.AsAsyncEnumerable().WithCancellation(cancellationToken)) {
var agent = new Agent(entity.AgentGuid, entity.Name, entity.ProtocolVersion, entity.BuildVersion, entity.MaxInstances, entity.MaxMemory);
if (!agents.ByGuid.AddOrReplaceIf(agent.Guid, agent, static oldAgent => oldAgent.IsOffline)) {
// TODO
@ -52,7 +52,7 @@ public sealed class AgentManager {
return agents.ByGuid.ToImmutable();
}
internal async Task<bool> RegisterAgent(AgentAuthToken authToken, AgentInfo agentInfo, InstanceManager instanceManager, RpcClientConnection connection) {
internal async Task<bool> RegisterAgent(AuthToken authToken, AgentInfo agentInfo, InstanceManager instanceManager, RpcClientConnection<IMessageToAgentListener> connection) {
if (!this.authToken.FixedTimeEquals(authToken)) {
await connection.Send(new RegisterAgentFailureMessage(RegisterAgentFailure.InvalidToken));
return false;
@ -68,8 +68,8 @@ public sealed class AgentManager {
oldAgent.Connection?.Close();
}
using (var scope = databaseProvider.CreateScope()) {
var entity = scope.Ctx.AgentUpsert.Fetch(agent.Guid);
await using (var ctx = dbProvider.Eager()) {
var entity = ctx.AgentUpsert.Fetch(agent.Guid);
entity.Name = agent.Name;
entity.ProtocolVersion = agent.ProtocolVersion;
@ -77,7 +77,7 @@ public sealed class AgentManager {
entity.MaxInstances = agent.MaxInstances;
entity.MaxMemory = agent.MaxMemory;
await scope.Ctx.SaveChangesAsync(cancellationToken);
await ctx.SaveChangesAsync(cancellationToken);
}
Logger.Information("Registered agent \"{Name}\" (GUID {Guid}).", agent.Name, agent.Guid);
@ -88,7 +88,7 @@ public sealed class AgentManager {
return true;
}
internal bool UnregisterAgent(Guid agentGuid, RpcClientConnection connection) {
internal bool UnregisterAgent(Guid agentGuid, RpcClientConnection<IMessageToAgentListener> connection) {
if (agents.ByGuid.TryReplaceIf(agentGuid, static oldAgent => oldAgent.AsOffline(), oldAgent => oldAgent.Connection?.IsSame(connection) == true)) {
Logger.Information("Unregistered agent with GUID {Guid}.", agentGuid);
return true;

View File

@ -1,70 +0,0 @@
using Phantom.Controller.Database.Entities;
using Phantom.Controller.Database.Enums;
namespace Phantom.Controller.Services.Audit;
public sealed partial class AuditLog {
public Task AddAdministratorUserCreatedEvent(UserEntity administratorUser) {
return AddItem(AuditLogEventType.AdministratorUserCreated, administratorUser.UserGuid.ToString());
}
public Task AddAdministratorUserModifiedEvent(UserEntity administratorUser) {
return AddItem(AuditLogEventType.AdministratorUserModified, administratorUser.UserGuid.ToString());
}
public void AddUserLoggedInEvent(UserEntity user) {
AddItem(user.UserGuid, AuditLogEventType.UserLoggedIn, user.UserGuid.ToString());
}
public void AddUserLoggedOutEvent(Guid userGuid) {
AddItem(userGuid, AuditLogEventType.UserLoggedOut, userGuid.ToString());
}
public Task AddUserCreatedEvent(UserEntity user) {
return AddItem(AuditLogEventType.UserCreated, user.UserGuid.ToString());
}
public Task AddUserRolesChangedEvent(UserEntity user, List<string> addedToRoles, List<string> removedFromRoles) {
var extra = new Dictionary<string, object?>();
if (addedToRoles.Count > 0) {
extra["addedToRoles"] = addedToRoles;
}
if (removedFromRoles.Count > 0) {
extra["removedFromRoles"] = removedFromRoles;
}
return AddItem(AuditLogEventType.UserRolesChanged, user.UserGuid.ToString(), extra);
}
public Task AddUserDeletedEvent(UserEntity user) {
return AddItem(AuditLogEventType.UserDeleted, user.UserGuid.ToString(), new Dictionary<string, object?> {
{ "username", user.Name }
});
}
public Task AddInstanceCreatedEvent(Guid instanceGuid) {
return AddItem(AuditLogEventType.InstanceCreated, instanceGuid.ToString());
}
public Task AddInstanceEditedEvent(Guid instanceGuid) {
return AddItem(AuditLogEventType.InstanceEdited, instanceGuid.ToString());
}
public Task AddInstanceLaunchedEvent(Guid instanceGuid) {
return AddItem(AuditLogEventType.InstanceLaunched, instanceGuid.ToString());
}
public Task AddInstanceCommandExecutedEvent(Guid instanceGuid, string command) {
return AddItem(AuditLogEventType.InstanceCommandExecuted, instanceGuid.ToString(), new Dictionary<string, object?> {
{ "command", command }
});
}
public Task AddInstanceStoppedEvent(Guid instanceGuid, int stopInSeconds) {
return AddItem(AuditLogEventType.InstanceStopped, instanceGuid.ToString(), new Dictionary<string, object?> {
{ "stop_in_seconds", stopInSeconds.ToString() }
});
}
}

View File

@ -1,54 +0,0 @@
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.EntityFrameworkCore;
using Phantom.Controller.Database;
using Phantom.Controller.Database.Entities;
using Phantom.Controller.Database.Enums;
using Phantom.Controller.Services.Users;
using Phantom.Utils.Tasks;
namespace Phantom.Controller.Services.Audit;
public sealed partial class AuditLog {
private readonly CancellationToken cancellationToken;
private readonly DatabaseProvider databaseProvider;
private readonly AuthenticationStateProvider authenticationStateProvider;
private readonly TaskManager taskManager;
public AuditLog(ServiceConfiguration serviceConfiguration, DatabaseProvider databaseProvider, AuthenticationStateProvider authenticationStateProvider, TaskManager taskManager) {
this.cancellationToken = serviceConfiguration.CancellationToken;
this.databaseProvider = databaseProvider;
this.authenticationStateProvider = authenticationStateProvider;
this.taskManager = taskManager;
}
private async Task<Guid?> GetCurrentAuthenticatedUserId() {
var authenticationState = await authenticationStateProvider.GetAuthenticationStateAsync();
return UserManager.GetAuthenticatedUserId(authenticationState.User);
}
private async Task AddEntityToDatabase(AuditLogEntity logEntity) {
using var scope = databaseProvider.CreateScope();
scope.Ctx.AuditLog.Add(logEntity);
await scope.Ctx.SaveChangesAsync(cancellationToken);
}
private void AddItem(Guid? userGuid, AuditLogEventType eventType, string subjectId, Dictionary<string, object?>? extra = null) {
var logEntity = new AuditLogEntity(userGuid, eventType, subjectId, extra);
taskManager.Run("Store audit log item to database", () => AddEntityToDatabase(logEntity));
}
private async Task AddItem(AuditLogEventType eventType, string subjectId, Dictionary<string, object?>? extra = null) {
AddItem(await GetCurrentAuthenticatedUserId(), eventType, subjectId, extra);
}
public async Task<AuditLogItem[]> GetItems(int count, CancellationToken cancellationToken) {
using var scope = databaseProvider.CreateScope();
return await scope.Ctx.AuditLog
.Include(static entity => entity.User)
.AsQueryable()
.OrderByDescending(static entity => entity.UtcTime)
.Take(count)
.Select(static entity => new AuditLogItem(entity.UtcTime, entity.UserGuid, entity.User == null ? null : entity.User.Name, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data))
.ToArrayAsync(cancellationToken);
}
}

View File

@ -0,0 +1,68 @@
using Phantom.Common.Data.Agent;
using Phantom.Common.Logging;
using Phantom.Common.Messages.Agent;
using Phantom.Common.Messages.Web;
using Phantom.Controller.Database;
using Phantom.Controller.Minecraft;
using Phantom.Controller.Rpc;
using Phantom.Controller.Services.Agents;
using Phantom.Controller.Services.Events;
using Phantom.Controller.Services.Instances;
using Phantom.Controller.Services.Rpc;
using Phantom.Controller.Services.Users;
using Phantom.Controller.Services.Users.Permissions;
using Phantom.Utils.Tasks;
namespace Phantom.Controller.Services;
public sealed class ControllerServices {
private TaskManager TaskManager { get; }
private MinecraftVersions MinecraftVersions { get; }
private AgentManager AgentManager { get; }
private AgentJavaRuntimesManager AgentJavaRuntimesManager { get; }
private EventLog EventLog { get; }
private InstanceManager InstanceManager { get; }
private InstanceLogManager InstanceLogManager { get; }
private UserManager UserManager { get; }
private RoleManager RoleManager { get; }
private PermissionManager PermissionManager { get; }
private readonly IDbContextProvider dbProvider;
private readonly CancellationToken cancellationToken;
public ControllerServices(IDbContextProvider dbProvider, AuthToken agentAuthToken, CancellationToken shutdownCancellationToken) {
this.TaskManager = new TaskManager(PhantomLogger.Create<TaskManager, ControllerServices>());
this.MinecraftVersions = new MinecraftVersions();
this.AgentManager = new AgentManager(agentAuthToken, dbProvider, TaskManager, shutdownCancellationToken);
this.AgentJavaRuntimesManager = new AgentJavaRuntimesManager();
this.EventLog = new EventLog(dbProvider, TaskManager, shutdownCancellationToken);
this.InstanceManager = new InstanceManager(AgentManager, MinecraftVersions, dbProvider, shutdownCancellationToken);
this.InstanceLogManager = new InstanceLogManager();
this.UserManager = new UserManager(dbProvider);
this.RoleManager = new RoleManager(dbProvider);
this.PermissionManager = new PermissionManager(dbProvider);
this.dbProvider = dbProvider;
this.cancellationToken = shutdownCancellationToken;
}
public AgentMessageListener CreateAgentMessageListener(RpcClientConnection<IMessageToAgentListener> connection) {
return new AgentMessageListener(connection, AgentManager, AgentJavaRuntimesManager, InstanceManager, InstanceLogManager, EventLog, cancellationToken);
}
public WebMessageListener CreateWebMessageListener(RpcClientConnection<IMessageToWebListener> connection) {
return new WebMessageListener(connection, UserManager, RoleManager);
}
public async Task Initialize() {
await DatabaseMigrator.Run(dbProvider, cancellationToken);
await PermissionManager.Initialize();
await RoleManager.Initialize();
await AgentManager.Initialize();
await InstanceManager.Initialize();
}
}

View File

@ -1,26 +1,28 @@
using Microsoft.EntityFrameworkCore;
using System.Collections.Immutable;
using Microsoft.EntityFrameworkCore;
using Phantom.Controller.Database;
using Phantom.Controller.Database.Entities;
using Phantom.Controller.Database.Enums;
using Phantom.Utils.Collections;
using Phantom.Utils.Tasks;
namespace Phantom.Controller.Services.Events;
public sealed partial class EventLog {
private readonly CancellationToken cancellationToken;
private readonly DatabaseProvider databaseProvider;
private readonly IDbContextProvider dbProvider;
private readonly TaskManager taskManager;
private readonly CancellationToken cancellationToken;
public EventLog(ServiceConfiguration serviceConfiguration, DatabaseProvider databaseProvider, TaskManager taskManager) {
this.cancellationToken = serviceConfiguration.CancellationToken;
this.databaseProvider = databaseProvider;
public EventLog(IDbContextProvider dbProvider, TaskManager taskManager, CancellationToken cancellationToken) {
this.dbProvider = dbProvider;
this.taskManager = taskManager;
this.cancellationToken = cancellationToken;
}
private async Task AddEntityToDatabase(EventLogEntity logEntity) {
using var scope = databaseProvider.CreateScope();
scope.Ctx.EventLog.Add(logEntity);
await scope.Ctx.SaveChangesAsync(cancellationToken);
await using var ctx = dbProvider.Eager();
ctx.EventLog.Add(logEntity);
await ctx.SaveChangesAsync(cancellationToken);
}
private void AddItem(Guid eventGuid, DateTime utcTime, Guid? agentGuid, EventLogEventType eventType, string subjectId, Dictionary<string, object?>? extra = null) {
@ -28,13 +30,14 @@ public sealed partial class EventLog {
taskManager.Run("Store event log item to database", () => AddEntityToDatabase(logEntity));
}
public async Task<EventLogItem[]> GetItems(int count, CancellationToken cancellationToken) {
using var scope = databaseProvider.CreateScope();
return await scope.Ctx.EventLog
.AsQueryable()
.OrderByDescending(static entity => entity.UtcTime)
.Take(count)
.Select(static entity => new EventLogItem(entity.UtcTime, entity.AgentGuid, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data))
.ToArrayAsync(cancellationToken);
public async Task<ImmutableArray<EventLogItem>> GetItems(int count, CancellationToken cancellationToken) {
await using var ctx = dbProvider.Eager();
return await ctx.EventLog
.AsQueryable()
.OrderByDescending(static entity => entity.UtcTime)
.Take(count)
.Select(static entity => new EventLogItem(entity.UtcTime, entity.AgentGuid, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data))
.AsAsyncEnumerable()
.ToImmutableArrayAsync(cancellationToken);
}
}

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