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

Compare commits

..

6 Commits

330 changed files with 4670 additions and 2589 deletions

View File

@ -9,10 +9,10 @@
<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="CONTROLLER_HOST" value="localhost" />
<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_HOST" value="localhost" />
</envs> </envs>
<option name="USE_EXTERNAL_CONSOLE" value="0" /> <option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" /> <option name="USE_MONO" value="0" />

View File

@ -9,10 +9,10 @@
<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="CONTROLLER_HOST" value="localhost" />
<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_HOST" value="localhost" />
</envs> </envs>
<option name="USE_EXTERNAL_CONSOLE" value="0" /> <option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" /> <option name="USE_MONO" value="0" />

View File

@ -9,10 +9,10 @@
<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="CONTROLLER_HOST" value="localhost" />
<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_HOST" value="localhost" />
</envs> </envs>
<option name="USE_EXTERNAL_CONSOLE" value="0" /> <option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" /> <option name="USE_MONO" value="0" />

View File

@ -1,9 +1,10 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Server + Agent x3" type="CompoundRunConfigurationType"> <configuration default="false" name="Controller + Web + Agent x3" type="CompoundRunConfigurationType">
<toRun name="Agent 1" type="DotNetProject" /> <toRun name="Agent 1" type="DotNetProject" />
<toRun name="Agent 2" type="DotNetProject" /> <toRun name="Agent 2" type="DotNetProject" />
<toRun name="Agent 3" type="DotNetProject" /> <toRun name="Agent 3" type="DotNetProject" />
<toRun name="Server" type="DotNetProject" /> <toRun name="Controller" type="DotNetProject" />
<toRun name="Web" type="DotNetProject" />
<method v="2" /> <method v="2" />
</configuration> </configuration>
</component> </component>

View File

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Controller + Web + Agent" type="CompoundRunConfigurationType">
<toRun name="Agent 1" type="DotNetProject" />
<toRun name="Controller" type="DotNetProject" />
<toRun name="Web" type="DotNetProject" />
<method v="2" />
</configuration>
</component>

View File

@ -1,18 +1,17 @@
<component name="ProjectRunConfigurationManager"> <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="EXE_PATH" value="$PROJECT_DIR$/.artifacts/bin/Phantom.Controller/debug/Phantom.Controller.exe" />
<option name="PROGRAM_PARAMETERS" value="" /> <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" /> <option name="PASS_PARENT_ENVS" value="1" />
<envs> <envs>
<env name="ASPNETCORE_ENVIRONMENT" value="Development" /> <env name="AGENT_RPC_SERVER_HOST" value="localhost" />
<env name="PG_DATABASE" value="postgres" /> <env name="PG_DATABASE" value="postgres" />
<env name="PG_HOST" value="localhost" /> <env name="PG_HOST" value="localhost" />
<env name="PG_PASS" value="development" /> <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="PG_USER" value="postgres" />
<env name="RPC_SERVER_HOST" value="localhost" /> <env name="WEB_RPC_SERVER_HOST" value="localhost" />
<env name="WEB_SERVER_HOST" value="localhost" />
</envs> </envs>
<option name="USE_EXTERNAL_CONSOLE" value="0" /> <option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" /> <option name="USE_MONO" value="0" />

View File

@ -1,7 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Server + Agent" type="CompoundRunConfigurationType">
<toRun name="Agent 1" type="DotNetProject" />
<toRun name="Server" type="DotNetProject" />
<method v="2" />
</configuration>
</component>

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

@ -0,0 +1,26 @@
<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="CONTROLLER_HOST" value="localhost" />
<env name="WEB_KEY" value="BMNHM9RRPMCBBY29D9XHS6KBKZSRY7F5XFN27YZX96XXWJC2NM2D6YRHM9PZN9JGQGCSJ6FMB2GGZ" />
<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

@ -0,0 +1,21 @@
using Phantom.Common.Logging;
using Phantom.Common.Messages.Agent;
using Phantom.Utils.Rpc;
using Serilog;
namespace Phantom.Agent.Rpc;
public sealed class ControllerConnection {
private static readonly ILogger Logger = PhantomLogger.Create(nameof(ControllerConnection));
private readonly RpcConnectionToServer<IMessageToControllerListener> connection;
public ControllerConnection(RpcConnectionToServer<IMessageToControllerListener> connection) {
this.connection = connection;
Logger.Information("Connection ready.");
}
public Task Send<TMessage>(TMessage message) where TMessage : IMessageToController {
return connection.Send(message);
}
}

View File

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

View File

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

View File

@ -0,0 +1,38 @@
using NetMQ;
using NetMQ.Sockets;
using Phantom.Common.Data.Agent;
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.Sockets;
using Phantom.Utils.Tasks;
using Serilog;
namespace Phantom.Agent.Rpc;
public sealed class RpcClientRuntime : RpcClientRuntime<IMessageToAgentListener, IMessageToControllerListener, ReplyMessage> {
public static Task Launch(RpcClientSocket<IMessageToAgentListener, IMessageToControllerListener, ReplyMessage> socket, AgentInfo agentInfo, IMessageToAgentListener messageListener, SemaphoreSlim disconnectSemaphore, CancellationToken receiveCancellationToken) {
return new RpcClientRuntime(socket, messageListener, disconnectSemaphore, receiveCancellationToken).Launch();
}
private RpcClientRuntime(RpcClientSocket<IMessageToAgentListener, IMessageToControllerListener, ReplyMessage> socket, IMessageToAgentListener messageListener, SemaphoreSlim disconnectSemaphore, CancellationToken receiveCancellationToken) : base(socket, messageListener, disconnectSemaphore, receiveCancellationToken) {}
protected override void RunWithConnection(ClientSocket socket, RpcConnectionToServer<IMessageToControllerListener> connection, ILogger logger, TaskManager taskManager) {
var keepAliveLoop = new KeepAliveLoop(connection);
try {
base.RunWithConnection(socket, connection, logger, taskManager);
} finally {
keepAliveLoop.Cancel();
}
}
protected override async Task Disconnect(ClientSocket socket, ILogger logger) {
var unregisterMessageBytes = AgentMessageRegistries.ToController.Write(new UnregisterAgentMessage()).ToArray();
try {
await socket.SendAsync(unregisterMessageBytes).AsTask().WaitAsync(TimeSpan.FromSeconds(5), CancellationToken.None);
} catch (TimeoutException) {
logger.Error("Timed out communicating agent shutdown with the controller.");
}
}
}

View File

@ -1,107 +0,0 @@
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.Utils.Rpc;
using Phantom.Utils.Rpc.Message;
using Phantom.Utils.Tasks;
using Serilog;
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) {
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();
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 SemaphoreSlim disconnectSemaphore;
private readonly CancellationToken receiveCancellationToken;
private RpcLauncher(RpcConfiguration config, ClientSocket socket, Guid agentGuid, Func<RpcServerConnection, IMessageToAgentListener> messageListenerFactory, SemaphoreSlim disconnectSemaphore, CancellationToken receiveCancellationToken) : base(config, socket) {
this.config = config;
this.agentGuid = agentGuid;
this.messageListenerFactory = messageListenerFactory;
this.disconnectSemaphore = disconnectSemaphore;
this.receiveCancellationToken = receiveCancellationToken;
}
protected override void Connect(ClientSocket socket) {
var logger = config.RuntimeLogger;
var url = config.TcpUrl;
logger.Information("Starting ZeroMQ client and connecting to {Url}...", url);
socket.Connect(url);
logger.Information("ZeroMQ client ready.");
}
protected override void Run(ClientSocket socket, MessageReplyTracker replyTracker, TaskManager taskManager) {
var connection = new RpcServerConnection(socket, replyTracker);
ServerMessaging.SetCurrentConnection(connection);
var logger = config.RuntimeLogger;
var handler = new MessageToAgentHandler(messageListenerFactory(connection), logger, taskManager, receiveCancellationToken);
var keepAliveLoop = new KeepAliveLoop(connection);
try {
while (!receiveCancellationToken.IsCancellationRequested) {
var data = socket.Receive(receiveCancellationToken);
LogMessageType(logger, data);
if (data.Length > 0) {
MessageRegistries.ToAgent.Handle(data, handler);
}
}
} catch (OperationCanceledException) {
// Ignore.
} finally {
logger.Debug("ZeroMQ client stopped receiving messages.");
disconnectSemaphore.Wait(CancellationToken.None);
keepAliveLoop.Cancel();
}
}
private static void LogMessageType(ILogger logger, ReadOnlyMemory<byte> data) {
if (!logger.IsEnabled(LogEventLevel.Verbose)) {
return;
}
if (data.Length > 0 && MessageRegistries.ToAgent.TryGetType(data, out var type)) {
logger.Verbose("Received {MessageType} ({Bytes} B) from server.", type.Name, data.Length);
}
else {
logger.Verbose("Received {Bytes} B message from server.", data.Length);
}
}
protected override async Task Disconnect() {
var unregisterTimeoutTask = Task.Delay(TimeSpan.FromSeconds(5), CancellationToken.None);
var finishedTask = await Task.WhenAny(ServerMessaging.Send(new UnregisterAgentMessage(agentGuid)), unregisterTimeoutTask);
if (finishedTask == unregisterTimeoutTask) {
config.RuntimeLogger.Error("Timed out communicating agent shutdown with the server.");
}
}
private sealed class MessageToAgentHandler : MessageHandler<IMessageToAgentListener> {
public MessageToAgentHandler(IMessageToAgentListener listener, ILogger logger, TaskManager taskManager, CancellationToken cancellationToken) : base(listener, logger, taskManager, cancellationToken) {}
protected override Task SendReply(uint sequenceId, byte[] serializedReply) {
return ServerMessaging.Send(new ReplyMessage(sequenceId, serializedReply));
}
}
}

View File

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

View File

@ -1,34 +0,0 @@
using Phantom.Common.Logging;
using Phantom.Common.Messages;
using Serilog;
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 readonly object SetCurrentConnectionLock = new ();
internal static void SetCurrentConnection(RpcServerConnection connection) {
lock (SetCurrentConnectionLock) {
if (CurrentConnection != null) {
throw new InvalidOperationException("Server connection can only be set once.");
}
CurrentConnection = connection;
}
Logger.Information("Server connection ready.");
}
public static Task Send<TMessage>(TMessage message) where TMessage : IMessageToServer {
return CurrentConnectionOrThrow.Send(message);
}
public static Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken) where TMessage : IMessageToServer<TReply> where TReply : class {
return CurrentConnectionOrThrow.Send<TMessage, TReply>(message, waitForReplyTime, waitForReplyCancellationToken);
}
}

View File

@ -1,4 +1,5 @@
using Phantom.Agent.Minecraft.Java; using Phantom.Agent.Minecraft.Java;
using Phantom.Agent.Rpc;
using Phantom.Agent.Services.Backups; using Phantom.Agent.Services.Backups;
using Phantom.Agent.Services.Instances; using Phantom.Agent.Services.Instances;
using Phantom.Common.Data.Agent; using Phantom.Common.Data.Agent;
@ -18,12 +19,12 @@ public sealed class AgentServices {
internal JavaRuntimeRepository JavaRuntimeRepository { get; } internal JavaRuntimeRepository JavaRuntimeRepository { get; }
internal InstanceSessionManager InstanceSessionManager { get; } internal InstanceSessionManager InstanceSessionManager { get; }
public AgentServices(AgentInfo agentInfo, AgentFolders agentFolders, AgentServiceConfiguration serviceConfiguration) { public AgentServices(AgentInfo agentInfo, AgentFolders agentFolders, AgentServiceConfiguration serviceConfiguration, ControllerConnection controllerConnection) {
this.AgentFolders = agentFolders; this.AgentFolders = agentFolders;
this.TaskManager = new TaskManager(PhantomLogger.Create<TaskManager, AgentServices>()); this.TaskManager = new TaskManager(PhantomLogger.Create<TaskManager, AgentServices>());
this.BackupManager = new BackupManager(agentFolders, serviceConfiguration.MaxConcurrentCompressionTasks); this.BackupManager = new BackupManager(agentFolders, serviceConfiguration.MaxConcurrentCompressionTasks);
this.JavaRuntimeRepository = new JavaRuntimeRepository(); this.JavaRuntimeRepository = new JavaRuntimeRepository();
this.InstanceSessionManager = new InstanceSessionManager(agentInfo, agentFolders, JavaRuntimeRepository, TaskManager, BackupManager); this.InstanceSessionManager = new InstanceSessionManager(controllerConnection, agentInfo, agentFolders, JavaRuntimeRepository, TaskManager, BackupManager);
} }
public async Task Initialize() { public async Task Initialize() {

View File

@ -1,12 +1,11 @@
using Phantom.Agent.Minecraft.Launcher; using Phantom.Agent.Minecraft.Launcher;
using Phantom.Agent.Rpc;
using Phantom.Agent.Services.Instances.Procedures; using Phantom.Agent.Services.Instances.Procedures;
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.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.Agent.ToController;
using Serilog; using Serilog;
namespace Phantom.Agent.Services.Instances; namespace Phantom.Agent.Services.Instances;
@ -57,7 +56,7 @@ sealed class Instance : IAsyncDisposable {
public void ReportLastStatus() { public void ReportLastStatus() {
TryUpdateStatus("Report last status of instance " + shortName, async () => { TryUpdateStatus("Report last status of instance " + shortName, async () => {
await ServerMessaging.Send(new ReportInstanceStatusMessage(Configuration.InstanceGuid, currentStatus)); await Services.ControllerConnection.Send(new ReportInstanceStatusMessage(Configuration.InstanceGuid, currentStatus));
}); });
} }
@ -65,14 +64,14 @@ sealed class Instance : IAsyncDisposable {
TryUpdateStatus("Report status of instance " + shortName + " as " + status.GetType().Name, async () => { TryUpdateStatus("Report status of instance " + shortName + " as " + status.GetType().Name, async () => {
if (status != currentStatus) { if (status != currentStatus) {
currentStatus = status; currentStatus = status;
await ServerMessaging.Send(new ReportInstanceStatusMessage(Configuration.InstanceGuid, status)); await Services.ControllerConnection.Send(new ReportInstanceStatusMessage(Configuration.InstanceGuid, status));
} }
}); });
} }
private void ReportEvent(IInstanceEvent instanceEvent) { private void ReportEvent(IInstanceEvent instanceEvent) {
var message = new ReportInstanceEventMessage(Guid.NewGuid(), DateTime.UtcNow, Configuration.InstanceGuid, instanceEvent); var message = new ReportInstanceEventMessage(Guid.NewGuid(), DateTime.UtcNow, Configuration.InstanceGuid, instanceEvent);
Services.TaskManager.Run("Report event for instance " + shortName, async () => await ServerMessaging.Send(message)); Services.TaskManager.Run("Report event for instance " + shortName, async () => await Services.ControllerConnection.Send(message));
} }
internal void TransitionState(IInstanceState newState) { internal void TransitionState(IInstanceState newState) {

View File

@ -2,7 +2,7 @@ using System.Collections.Immutable;
using System.Threading.Channels; using System.Threading.Channels;
using Phantom.Agent.Rpc; using Phantom.Agent.Rpc;
using Phantom.Common.Logging; using Phantom.Common.Logging;
using Phantom.Common.Messages.ToServer; using Phantom.Common.Messages.Agent.ToController;
using Phantom.Utils.Tasks; using Phantom.Utils.Tasks;
namespace Phantom.Agent.Services.Instances; namespace Phantom.Agent.Services.Instances;
@ -16,12 +16,14 @@ sealed class InstanceLogSender : CancellableBackgroundTask {
private static readonly TimeSpan SendDelay = TimeSpan.FromMilliseconds(200); private static readonly TimeSpan SendDelay = TimeSpan.FromMilliseconds(200);
private readonly ControllerConnection controllerConnection;
private readonly Guid instanceGuid; private readonly Guid instanceGuid;
private readonly Channel<string> outputChannel; private readonly Channel<string> outputChannel;
private int droppedLinesSinceLastSend; private int droppedLinesSinceLastSend;
public InstanceLogSender(TaskManager taskManager, Guid instanceGuid, string loggerName) : base(PhantomLogger.Create<InstanceLogSender>(loggerName), taskManager, "Instance log sender for " + loggerName) { public InstanceLogSender(ControllerConnection controllerConnection, TaskManager taskManager, Guid instanceGuid, string loggerName) : base(PhantomLogger.Create<InstanceLogSender>(loggerName), taskManager, "Instance log sender for " + loggerName) {
this.controllerConnection = controllerConnection;
this.instanceGuid = instanceGuid; this.instanceGuid = instanceGuid;
this.outputChannel = Channel.CreateBounded<string>(BufferOptions, OnLineDropped); this.outputChannel = Channel.CreateBounded<string>(BufferOptions, OnLineDropped);
Start(); Start();
@ -61,7 +63,7 @@ sealed class InstanceLogSender : CancellableBackgroundTask {
private async Task SendOutputToServer(ImmutableArray<string> lines) { private async Task SendOutputToServer(ImmutableArray<string> lines) {
if (!lines.IsEmpty) { if (!lines.IsEmpty) {
await ServerMessaging.Send(new InstanceOutputMessage(instanceGuid, lines)); await controllerConnection.Send(new InstanceOutputMessage(instanceGuid, lines));
} }
} }

View File

@ -1,7 +1,8 @@
using Phantom.Agent.Minecraft.Launcher; using Phantom.Agent.Minecraft.Launcher;
using Phantom.Agent.Rpc;
using Phantom.Agent.Services.Backups; using Phantom.Agent.Services.Backups;
using Phantom.Utils.Tasks; using Phantom.Utils.Tasks;
namespace Phantom.Agent.Services.Instances; namespace Phantom.Agent.Services.Instances;
sealed record InstanceServices(TaskManager TaskManager, PortManager PortManager, BackupManager BackupManager, LaunchServices LaunchServices); sealed record InstanceServices(ControllerConnection ControllerConnection, TaskManager TaskManager, PortManager PortManager, BackupManager BackupManager, LaunchServices LaunchServices);

View File

@ -14,7 +14,7 @@ using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Minecraft; 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.Agent.ToController;
using Phantom.Utils.IO; using Phantom.Utils.IO;
using Phantom.Utils.Tasks; using Phantom.Utils.Tasks;
using Serilog; using Serilog;
@ -24,6 +24,7 @@ namespace Phantom.Agent.Services.Instances;
sealed class InstanceSessionManager : IAsyncDisposable { sealed class InstanceSessionManager : IAsyncDisposable {
private static readonly ILogger Logger = PhantomLogger.Create<InstanceSessionManager>(); private static readonly ILogger Logger = PhantomLogger.Create<InstanceSessionManager>();
private readonly ControllerConnection controllerConnection;
private readonly AgentInfo agentInfo; private readonly AgentInfo agentInfo;
private readonly string basePath; private readonly string basePath;
@ -36,7 +37,8 @@ sealed class InstanceSessionManager : IAsyncDisposable {
private uint instanceLoggerSequenceId = 0; private uint instanceLoggerSequenceId = 0;
public InstanceSessionManager(AgentInfo agentInfo, AgentFolders agentFolders, JavaRuntimeRepository javaRuntimeRepository, TaskManager taskManager, BackupManager backupManager) { public InstanceSessionManager(ControllerConnection controllerConnection, AgentInfo agentInfo, AgentFolders agentFolders, JavaRuntimeRepository javaRuntimeRepository, TaskManager taskManager, BackupManager backupManager) {
this.controllerConnection = controllerConnection;
this.agentInfo = agentInfo; this.agentInfo = agentInfo;
this.basePath = agentFolders.InstancesFolderPath; this.basePath = agentFolders.InstancesFolderPath;
this.shutdownCancellationToken = shutdownCancellationTokenSource.Token; this.shutdownCancellationToken = shutdownCancellationTokenSource.Token;
@ -45,7 +47,7 @@ sealed class InstanceSessionManager : IAsyncDisposable {
var launchServices = new LaunchServices(minecraftServerExecutables, javaRuntimeRepository); var launchServices = new LaunchServices(minecraftServerExecutables, javaRuntimeRepository);
var portManager = new PortManager(agentInfo.AllowedServerPorts, agentInfo.AllowedRconPorts); var portManager = new PortManager(agentInfo.AllowedServerPorts, agentInfo.AllowedRconPorts);
this.instanceServices = new InstanceServices(taskManager, portManager, backupManager, launchServices); this.instanceServices = new InstanceServices(controllerConnection, taskManager, portManager, backupManager, launchServices);
} }
private async Task<InstanceActionResult<T>> AcquireSemaphoreAndRun<T>(Func<Task<InstanceActionResult<T>>> func) { private async Task<InstanceActionResult<T>> AcquireSemaphoreAndRun<T>(Func<Task<InstanceActionResult<T>>> func) {
@ -146,7 +148,7 @@ sealed class InstanceSessionManager : IAsyncDisposable {
var runningInstances = GetRunningInstancesInternal(); var runningInstances = GetRunningInstancesInternal();
var runningInstanceCount = runningInstances.Length; var runningInstanceCount = runningInstances.Length;
var runningInstanceMemory = runningInstances.Aggregate(RamAllocationUnits.Zero, static (total, instance) => total + instance.Configuration.MemoryAllocation); var runningInstanceMemory = runningInstances.Aggregate(RamAllocationUnits.Zero, static (total, instance) => total + instance.Configuration.MemoryAllocation);
await ServerMessaging.Send(new ReportAgentStatusMessage(runningInstanceCount, runningInstanceMemory)); await controllerConnection.Send(new ReportAgentStatusMessage(runningInstanceCount, runningInstanceMemory));
} finally { } finally {
semaphore.Release(); semaphore.Release();
} }

View File

@ -27,7 +27,7 @@ sealed class InstanceRunningState : IInstanceState, IDisposable {
this.context = context; this.context = context;
this.Process = process; this.Process = process;
this.logSender = new InstanceLogSender(context.Services.TaskManager, configuration.InstanceGuid, context.ShortName); this.logSender = new InstanceLogSender(context.Services.ControllerConnection, context.Services.TaskManager, configuration.InstanceGuid, context.ShortName);
this.backupScheduler = new BackupScheduler(context.Services.TaskManager, context.Services.BackupManager, process, context, configuration.ServerPort); this.backupScheduler = new BackupScheduler(context.Services.TaskManager, context.Services.BackupManager, process, context, configuration.ServerPort);
this.backupScheduler.BackupCompleted += OnScheduledBackupCompleted; this.backupScheduler.BackupCompleted += OnScheduledBackupCompleted;

View File

@ -6,7 +6,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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.Minecraft\Phantom.Agent.Minecraft.csproj" />
<ProjectReference Include="..\Phantom.Agent.Rpc\Phantom.Agent.Rpc.csproj" /> <ProjectReference Include="..\Phantom.Agent.Rpc\Phantom.Agent.Rpc.csproj" />
</ItemGroup> </ItemGroup>

View File

@ -1,11 +1,11 @@
using Phantom.Agent.Rpc; using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Replies; using Phantom.Common.Data.Replies;
using Phantom.Common.Logging; using Phantom.Common.Logging;
using Phantom.Common.Messages; using Phantom.Common.Messages.Agent;
using Phantom.Common.Messages.BiDirectional; using Phantom.Common.Messages.Agent.BiDirectional;
using Phantom.Common.Messages.ToAgent; using Phantom.Common.Messages.Agent.ToAgent;
using Phantom.Common.Messages.ToServer; using Phantom.Common.Messages.Agent.ToController;
using Phantom.Utils.Rpc;
using Phantom.Utils.Rpc.Message; using Phantom.Utils.Rpc.Message;
using Serilog; using Serilog;
@ -14,11 +14,11 @@ namespace Phantom.Agent.Services.Rpc;
public sealed class MessageListener : IMessageToAgentListener { public sealed class MessageListener : IMessageToAgentListener {
private static ILogger Logger { get; } = PhantomLogger.Create<MessageListener>(); private static ILogger Logger { get; } = PhantomLogger.Create<MessageListener>();
private readonly RpcServerConnection connection; private readonly RpcConnectionToServer<IMessageToControllerListener> connection;
private readonly AgentServices agent; private readonly AgentServices agent;
private readonly CancellationTokenSource shutdownTokenSource; private readonly CancellationTokenSource shutdownTokenSource;
public MessageListener(RpcServerConnection connection, AgentServices agent, CancellationTokenSource shutdownTokenSource) { public MessageListener(RpcConnectionToServer<IMessageToControllerListener> connection, AgentServices agent, CancellationTokenSource shutdownTokenSource) {
this.connection = connection; this.connection = connection;
this.agent = agent; this.agent = agent;
this.shutdownTokenSource = shutdownTokenSource; this.shutdownTokenSource = shutdownTokenSource;
@ -40,7 +40,7 @@ public sealed class MessageListener : IMessageToAgentListener {
} }
} }
await ServerMessaging.Send(new AdvertiseJavaRuntimesMessage(agent.JavaRuntimeRepository.All)); await connection.Send(new AdvertiseJavaRuntimesMessage(agent.JavaRuntimeRepository.All));
await agent.InstanceSessionManager.RefreshAgentStatus(); await agent.InstanceSessionManager.RefreshAgentStatus();
return NoReply.Instance; return NoReply.Instance;

View File

@ -1,5 +1,5 @@
using NetMQ; using NetMQ;
using Phantom.Common.Data.Agent; using Phantom.Common.Data;
using Phantom.Common.Logging; using Phantom.Common.Logging;
using Phantom.Utils.Cryptography; using Phantom.Utils.Cryptography;
using Phantom.Utils.IO; using Phantom.Utils.IO;
@ -10,7 +10,7 @@ namespace Phantom.Agent;
static class AgentKey { static class AgentKey {
private static ILogger Logger { get; } = PhantomLogger.Create(nameof(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) { if (agentKeyFilePath != null) {
return LoadFromFile(agentKeyFilePath); 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)) { if (!File.Exists(agentKeyFilePath)) {
Logger.Fatal("Missing agent key file: {AgentKeyFilePath}", agentKeyFilePath); Logger.Fatal("Missing agent key file: {AgentKeyFilePath}", agentKeyFilePath);
return null; return null;
@ -41,7 +41,7 @@ static class AgentKey {
} }
} }
private static (NetMQCertificate, AgentAuthToken)? LoadFromToken(string agentKey) { private static (NetMQCertificate, AuthToken)? LoadFromToken(string agentKey) {
try { try {
return LoadFromBytes(TokenGenerator.DecodeBytes(agentKey)); return LoadFromBytes(TokenGenerator.DecodeBytes(agentKey));
} catch (Exception) { } catch (Exception) {
@ -50,11 +50,11 @@ static class AgentKey {
} }
} }
private static (NetMQCertificate, AgentAuthToken)? LoadFromBytes(byte[] agentKey) { private static (NetMQCertificate, AuthToken)? LoadFromBytes(byte[] agentKey) {
var (publicKey, agentToken) = AgentKeyData.FromBytes(agentKey); var (publicKey, agentToken) = ConnectionCommonKey.FromBytes(agentKey);
var serverCertificate = NetMQCertificate.FromPublicKey(publicKey); var controllerCertificate = NetMQCertificate.FromPublicKey(publicKey);
Logger.Information("Loaded agent key."); Logger.Information("Loaded agent key.");
return (serverCertificate, agentToken); return (controllerCertificate, agentToken);
} }
} }

View File

@ -1,11 +1,15 @@
using System.Reflection; using System.Reflection;
using NetMQ;
using Phantom.Agent; using Phantom.Agent;
using Phantom.Agent.Rpc; using Phantom.Agent.Rpc;
using Phantom.Agent.Services; using Phantom.Agent.Services;
using Phantom.Agent.Services.Rpc; using Phantom.Agent.Services.Rpc;
using Phantom.Common.Data.Agent; using Phantom.Common.Data.Agent;
using Phantom.Common.Logging; using Phantom.Common.Logging;
using Phantom.Common.Messages.Agent;
using Phantom.Common.Messages.Agent.ToController;
using Phantom.Utils.Rpc; using Phantom.Utils.Rpc;
using Phantom.Utils.Rpc.Sockets;
using Phantom.Utils.Runtime; using Phantom.Utils.Runtime;
using Phantom.Utils.Tasks; using Phantom.Utils.Tasks;
@ -26,7 +30,7 @@ try {
PhantomLogger.Root.InformationHeading("Initializing Phantom Panel agent..."); PhantomLogger.Root.InformationHeading("Initializing Phantom Panel agent...");
PhantomLogger.Root.Information("Agent version: {Version}", fullVersion); 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); var agentKey = await AgentKey.Load(agentKeyToken, agentKeyFilePath);
if (agentKey == null) { if (agentKey == null) {
@ -43,21 +47,20 @@ try {
return 1; 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 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) {
return new MessageListener(connection, agentServices, shutdownCancellationTokenSource);
}
PhantomLogger.Root.InformationHeading("Launching Phantom Panel agent..."); PhantomLogger.Root.InformationHeading("Launching Phantom Panel agent...");
var rpcConfiguration = new RpcConfiguration(PhantomLogger.Create("Rpc"), PhantomLogger.Create<TaskManager>("Rpc"), controllerHost, controllerPort, controllerCertificate);
var rpcSocket = RpcClientSocket.Connect(rpcConfiguration, AgentMessageRegistries.Definitions, new RegisterAgentMessage(agentToken, agentInfo));
var agentServices = new AgentServices(agentInfo, folders, new AgentServiceConfiguration(maxConcurrentBackupCompressionTasks), new ControllerConnection(rpcSocket.Connection));
await agentServices.Initialize(); await agentServices.Initialize();
var rpcDisconnectSemaphore = new SemaphoreSlim(0, 1); var rpcDisconnectSemaphore = new SemaphoreSlim(0, 1);
var rpcConfiguration = new RpcConfiguration(PhantomLogger.Create("Rpc"), PhantomLogger.Create<TaskManager>("Rpc"), serverHost, serverPort, serverCertificate); var rpcMessageListener = new MessageListener(rpcSocket.Connection, agentServices, shutdownCancellationTokenSource);
var rpcTask = RpcLauncher.Launch(rpcConfiguration, agentToken, agentInfo, MessageListenerFactory, rpcDisconnectSemaphore, shutdownCancellationToken); var rpcTask = RpcClientRuntime.Launch(rpcSocket, agentInfo, rpcMessageListener, rpcDisconnectSemaphore, shutdownCancellationToken);
try { try {
await rpcTask.WaitAsync(shutdownCancellationToken); await rpcTask.WaitAsync(shutdownCancellationToken);
} finally { } finally {
@ -67,6 +70,8 @@ try {
rpcDisconnectSemaphore.Release(); rpcDisconnectSemaphore.Release();
await rpcTask; await rpcTask;
rpcDisconnectSemaphore.Dispose(); rpcDisconnectSemaphore.Dispose();
NetMQConfig.Cleanup();
} }
return 0; return 0;

View File

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

View File

@ -0,0 +1,22 @@
using MemoryPack;
using Phantom.Common.Data.Agent;
namespace Phantom.Common.Data.Web.Agent;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record AgentWithStats(
[property: MemoryPackOrder(0)] Guid Guid,
[property: MemoryPackOrder(1)] string Name,
[property: MemoryPackOrder(2)] ushort ProtocolVersion,
[property: MemoryPackOrder(3)] string BuildVersion,
[property: MemoryPackOrder(4)] ushort MaxInstances,
[property: MemoryPackOrder(5)] RamAllocationUnits MaxMemory,
[property: MemoryPackOrder(6)] AllowedPorts? AllowedServerPorts,
[property: MemoryPackOrder(7)] AllowedPorts? AllowedRconPorts,
[property: MemoryPackOrder(8)] AgentStats? Stats,
[property: MemoryPackOrder(9)] DateTimeOffset? LastPing,
[property: MemoryPackOrder(10)] bool IsOnline
) {
[MemoryPackIgnore]
public RamAllocationUnits? AvailableMemory => MaxMemory - Stats?.RunningInstanceMemory;
}

View File

@ -1,4 +1,4 @@
namespace Phantom.Controller.Database.Enums; namespace Phantom.Common.Data.Web.AuditLog;
public enum AuditLogEventType { public enum AuditLogEventType {
AdministratorUserCreated, AdministratorUserCreated,
@ -6,6 +6,7 @@ public enum AuditLogEventType {
UserLoggedIn, UserLoggedIn,
UserLoggedOut, UserLoggedOut,
UserCreated, UserCreated,
UserPasswordChanged,
UserRolesChanged, UserRolesChanged,
UserDeleted, UserDeleted,
InstanceCreated, InstanceCreated,
@ -15,13 +16,14 @@ public enum AuditLogEventType {
InstanceCommandExecuted InstanceCommandExecuted
} }
static class AuditLogEventTypeExtensions { public static class AuditLogEventTypeExtensions {
private static readonly Dictionary<AuditLogEventType, AuditLogSubjectType> SubjectTypes = new () { private static readonly Dictionary<AuditLogEventType, AuditLogSubjectType> SubjectTypes = new () {
{ AuditLogEventType.AdministratorUserCreated, AuditLogSubjectType.User }, { AuditLogEventType.AdministratorUserCreated, AuditLogSubjectType.User },
{ AuditLogEventType.AdministratorUserModified, AuditLogSubjectType.User }, { AuditLogEventType.AdministratorUserModified, AuditLogSubjectType.User },
{ AuditLogEventType.UserLoggedIn, AuditLogSubjectType.User }, { AuditLogEventType.UserLoggedIn, AuditLogSubjectType.User },
{ AuditLogEventType.UserLoggedOut, AuditLogSubjectType.User }, { AuditLogEventType.UserLoggedOut, AuditLogSubjectType.User },
{ AuditLogEventType.UserCreated, AuditLogSubjectType.User }, { AuditLogEventType.UserCreated, AuditLogSubjectType.User },
{ AuditLogEventType.UserPasswordChanged, AuditLogSubjectType.User },
{ AuditLogEventType.UserRolesChanged, AuditLogSubjectType.User }, { AuditLogEventType.UserRolesChanged, AuditLogSubjectType.User },
{ AuditLogEventType.UserDeleted, AuditLogSubjectType.User }, { AuditLogEventType.UserDeleted, AuditLogSubjectType.User },
{ AuditLogEventType.InstanceCreated, AuditLogSubjectType.Instance }, { AuditLogEventType.InstanceCreated, AuditLogSubjectType.Instance },
@ -39,7 +41,7 @@ static class AuditLogEventTypeExtensions {
} }
} }
internal static AuditLogSubjectType GetSubjectType(this AuditLogEventType type) { public static AuditLogSubjectType GetSubjectType(this AuditLogEventType type) {
return SubjectTypes[type]; return SubjectTypes[type];
} }
} }

View File

@ -0,0 +1,14 @@
using MemoryPack;
namespace Phantom.Common.Data.Web.AuditLog;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record AuditLogItem(
[property: MemoryPackOrder(0)] DateTime UtcTime,
[property: MemoryPackOrder(1)] Guid? UserGuid,
[property: MemoryPackOrder(2)] string? UserName,
[property: MemoryPackOrder(3)] AuditLogEventType EventType,
[property: MemoryPackOrder(4)] AuditLogSubjectType SubjectType,
[property: MemoryPackOrder(5)] string? SubjectId,
[property: MemoryPackOrder(6)] string? JsonData
);

View File

@ -1,4 +1,4 @@
namespace Phantom.Controller.Database.Enums; namespace Phantom.Common.Data.Web.AuditLog;
public enum AuditLogSubjectType { public enum AuditLogSubjectType {
User, User,

View File

@ -1,4 +1,4 @@
namespace Phantom.Controller.Database.Enums; namespace Phantom.Common.Data.Web.EventLog;
public enum EventLogEventType { public enum EventLogEventType {
InstanceLaunchSucceded, InstanceLaunchSucceded,
@ -10,7 +10,7 @@ public enum EventLogEventType {
InstanceBackupFailed, InstanceBackupFailed,
} }
static class EventLogEventTypeExtensions { public static class EventLogEventTypeExtensions {
private static readonly Dictionary<EventLogEventType, EventLogSubjectType> SubjectTypes = new () { private static readonly Dictionary<EventLogEventType, EventLogSubjectType> SubjectTypes = new () {
{ EventLogEventType.InstanceLaunchSucceded, EventLogSubjectType.Instance }, { EventLogEventType.InstanceLaunchSucceded, EventLogSubjectType.Instance },
{ EventLogEventType.InstanceLaunchFailed, EventLogSubjectType.Instance }, { EventLogEventType.InstanceLaunchFailed, EventLogSubjectType.Instance },

View File

@ -0,0 +1,13 @@
using MemoryPack;
namespace Phantom.Common.Data.Web.EventLog;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record EventLogItem(
[property: MemoryPackOrder(0)] DateTime UtcTime,
[property: MemoryPackOrder(1)] Guid? AgentGuid,
[property: MemoryPackOrder(2)] EventLogEventType EventType,
[property: MemoryPackOrder(3)] EventLogSubjectType SubjectType,
[property: MemoryPackOrder(4)] string SubjectId,
[property: MemoryPackOrder(5)] string? JsonData
);

View File

@ -0,0 +1,5 @@
namespace Phantom.Common.Data.Web.EventLog;
public enum EventLogSubjectType {
Instance
}

View File

@ -0,0 +1,23 @@
namespace Phantom.Common.Data.Web.Instance;
public enum CreateOrUpdateInstanceResult : byte {
UnknownError,
Success,
InstanceNameMustNotBeEmpty,
InstanceMemoryMustNotBeZero,
MinecraftVersionDownloadInfoNotFound,
AgentNotFound
}
public static class CreateOrUpdateInstanceResultExtensions {
public static string ToSentence(this CreateOrUpdateInstanceResult reason) {
return reason switch {
CreateOrUpdateInstanceResult.Success => "Success.",
CreateOrUpdateInstanceResult.InstanceNameMustNotBeEmpty => "Instance name must not be empty.",
CreateOrUpdateInstanceResult.InstanceMemoryMustNotBeZero => "Memory must not be 0 MB.",
CreateOrUpdateInstanceResult.MinecraftVersionDownloadInfoNotFound => "Could not find download information for the selected Minecraft version.",
CreateOrUpdateInstanceResult.AgentNotFound => "Agent not found.",
_ => "Unknown error."
};
}
}

View File

@ -0,0 +1,15 @@
using MemoryPack;
using Phantom.Common.Data.Instance;
namespace Phantom.Common.Data.Web.Instance;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record Instance(
[property: MemoryPackOrder(0)] InstanceConfiguration Configuration,
[property: MemoryPackOrder(1)] IInstanceStatus Status,
[property: MemoryPackOrder(2)] bool LaunchAutomatically
) {
public static Instance Offline(InstanceConfiguration configuration, bool launchAutomatically = false) {
return new Instance(configuration, InstanceStatus.Offline, launchAutomatically);
}
}

View File

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

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next.StrongName" />
<PackageReference Include="MemoryPack" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Phantom.Common.Data\Phantom.Common.Data.csproj" />
</ItemGroup>
</Project>

View File

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

View File

@ -0,0 +1,28 @@
using System.Collections.Immutable;
using MemoryPack;
using Phantom.Common.Data.Web.Users.AddUserErrors;
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 partial record AddUserError {
internal AddUserError() {}
}
}
namespace Phantom.Common.Data.Web.Users.AddUserErrors {
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record NameIsInvalid([property: MemoryPackOrder(0)] UsernameRequirementViolation Violation) : AddUserError;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record PasswordIsInvalid([property: MemoryPackOrder(0)] ImmutableArray<PasswordRequirementViolation> Violations) : AddUserError;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record NameAlreadyExists : AddUserError;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record UnknownError : AddUserError;
}

View File

@ -0,0 +1,10 @@
using System.Collections.Immutable;
using MemoryPack;
namespace Phantom.Common.Data.Web.Users;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record ChangeUserRolesResult(
[property: MemoryPackOrder(0)] ImmutableHashSet<Guid> AddedToRoleGuids,
[property: MemoryPackOrder(1)] ImmutableHashSet<Guid> RemovedFromRoleGuids
);

View File

@ -0,0 +1,31 @@
using MemoryPack;
using Phantom.Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults;
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 {
internal CreateOrUpdateAdministratorUserResult() {}
}
}
namespace Phantom.Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults {
[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

@ -0,0 +1,23 @@
using MemoryPack;
using Phantom.Common.Data.Web.Users.CreateUserResults;
namespace Phantom.Common.Data.Web.Users {
[MemoryPackable]
[MemoryPackUnion(0, typeof(Success))]
[MemoryPackUnion(1, typeof(CreationFailed))]
[MemoryPackUnion(2, typeof(UnknownError))]
public abstract partial record CreateUserResult {
internal CreateUserResult() {}
}
}
namespace Phantom.Common.Data.Web.Users.CreateUserResults {
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record Success([property: MemoryPackOrder(0)] UserInfo User) : CreateUserResult;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record CreationFailed([property: MemoryPackOrder(0)] AddUserError Error) : CreateUserResult;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record UnknownError : CreateUserResult;
}

View File

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

View File

@ -0,0 +1,11 @@
using System.Collections.Immutable;
using MemoryPack;
namespace Phantom.Common.Data.Web.Users;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record LogInSuccess (
[property: MemoryPackOrder(0)] Guid UserGuid,
[property: MemoryPackOrder(1)] PermissionSet Permissions,
[property: MemoryPackOrder(2)] ImmutableArray<byte> Token
);

View File

@ -0,0 +1,27 @@
using MemoryPack;
using Phantom.Common.Data.Web.Users.PasswordRequirementViolations;
namespace Phantom.Common.Data.Web.Users {
[MemoryPackable]
[MemoryPackUnion(0, typeof(TooShort))]
[MemoryPackUnion(1, typeof(MustContainLowercaseLetter))]
[MemoryPackUnion(2, typeof(MustContainUppercaseLetter))]
[MemoryPackUnion(3, typeof(MustContainDigit))]
public abstract partial record PasswordRequirementViolation {
internal PasswordRequirementViolation() {}
}
}
namespace Phantom.Common.Data.Web.Users.PasswordRequirementViolations {
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record TooShort([property: MemoryPackOrder(0)] int MinimumLength) : PasswordRequirementViolation;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record MustContainLowercaseLetter : PasswordRequirementViolation;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record MustContainUppercaseLetter : PasswordRequirementViolation;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record MustContainDigit : PasswordRequirementViolation;
}

View File

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

View File

@ -0,0 +1,29 @@
using System.Collections.Immutable;
using MemoryPack;
namespace Phantom.Common.Data.Web.Users;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial class PermissionSet {
public static PermissionSet None { get; } = new (ImmutableHashSet<string>.Empty);
[MemoryPackOrder(0)]
[MemoryPackInclude]
private readonly ImmutableHashSet<string> permissionIds;
public PermissionSet(ImmutableHashSet<string> permissionIds) {
this.permissionIds = permissionIds;
}
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,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

@ -0,0 +1,24 @@
using System.Collections.Immutable;
using MemoryPack;
using Phantom.Common.Data.Web.Users.SetUserPasswordErrors;
namespace Phantom.Common.Data.Web.Users {
[MemoryPackable]
[MemoryPackUnion(0, typeof(UserNotFound))]
[MemoryPackUnion(1, typeof(PasswordIsInvalid))]
[MemoryPackUnion(2, typeof(UnknownError))]
public abstract partial record SetUserPasswordError {
internal SetUserPasswordError() {}
}
}
namespace Phantom.Common.Data.Web.Users.SetUserPasswordErrors {
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record UserNotFound : SetUserPasswordError;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record PasswordIsInvalid([property: MemoryPackOrder(0)] ImmutableArray<PasswordRequirementViolation> Violations) : SetUserPasswordError;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial 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,19 @@
using MemoryPack;
using Phantom.Common.Data.Web.Users.UsernameRequirementViolations;
namespace Phantom.Common.Data.Web.Users {
[MemoryPackable]
[MemoryPackUnion(0, typeof(IsEmpty))]
[MemoryPackUnion(1, typeof(TooLong))]
public abstract partial record UsernameRequirementViolation {
internal UsernameRequirementViolation() {}
}
}
namespace Phantom.Common.Data.Web.Users.UsernameRequirementViolations {
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record IsEmpty : UsernameRequirementViolation;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record TooLong([property: MemoryPackOrder(0)] 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

@ -0,0 +1,9 @@
using MemoryPack;
namespace Phantom.Common.Data.Agent;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record AgentStats(
[property: MemoryPackOrder(0)] int RunningInstanceCount,
[property: MemoryPackOrder(1)] RamAllocationUnits RunningInstanceMemory
);

View File

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

View File

@ -0,0 +1,18 @@
namespace Phantom.Common.Data;
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

@ -1,7 +1,10 @@
namespace Phantom.Common.Data.Minecraft; using MemoryPack;
public sealed record MinecraftVersion( namespace Phantom.Common.Data.Minecraft;
string Id,
MinecraftVersionType Type, [MemoryPackable(GenerateType.VersionTolerant)]
string MetadataUrl public sealed partial record MinecraftVersion(
[property: MemoryPackOrder(0)] string Id,
[property: MemoryPackOrder(1)] MinecraftVersionType Type,
[property: MemoryPackOrder(2)] string MetadataUrl
); );

View File

@ -27,7 +27,7 @@ public static class PhantomLogger {
} }
public static ILogger Create(string name1, string name2) { public static ILogger Create(string name1, string name2) {
return Create(name1 + ":" + name2); return Create(ConcatNames(name1, name2));
} }
public static ILogger Create<T>() { public static ILogger Create<T>() {
@ -38,10 +38,18 @@ public static class PhantomLogger {
return Create(typeof(T).Name, 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>() { public static ILogger Create<T1, T2>() {
return Create(typeof(T1).Name, typeof(T2).Name); return Create(typeof(T1).Name, typeof(T2).Name);
} }
private static string ConcatNames(string name1, string name2) {
return name1 + ":" + name2;
}
public static void Dispose() { public static void Dispose() {
Root.Dispose(); Root.Dispose();
Base.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, ReplyMessage> 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, ReplyMessage> {
public MessageRegistry<IMessageToAgentListener> ToClient => ToAgent;
public MessageRegistry<IMessageToControllerListener> ToServer => ToController;
public bool IsRegistrationMessage(Type messageType) {
return messageType == typeof(RegisterAgentMessage);
}
public ReplyMessage CreateReplyMessage(uint sequenceId, byte[] serializedReply) {
return new ReplyMessage(sequenceId, serializedReply);
}
}
}

View File

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

View File

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

View File

@ -1,9 +1,9 @@
using Phantom.Common.Data.Replies; using Phantom.Common.Data.Replies;
using Phantom.Common.Messages.BiDirectional; using Phantom.Common.Messages.Agent.BiDirectional;
using Phantom.Common.Messages.ToAgent; using Phantom.Common.Messages.Agent.ToAgent;
using Phantom.Utils.Rpc.Message; using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages; namespace Phantom.Common.Messages.Agent;
public interface IMessageToAgentListener { public interface IMessageToAgentListener {
Task<NoReply> HandleRegisterAgentSuccess(RegisterAgentSuccessMessage message); 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.Agent.BiDirectional;
using Phantom.Common.Messages.ToServer; using Phantom.Common.Messages.Agent.ToController;
using Phantom.Utils.Rpc.Message; using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages; namespace Phantom.Common.Messages.Agent;
public interface IMessageToServerListener { public interface IMessageToControllerListener {
bool IsDisposed { get; }
Task<NoReply> HandleRegisterAgent(RegisterAgentMessage message); Task<NoReply> HandleRegisterAgent(RegisterAgentMessage message);
Task<NoReply> HandleUnregisterAgent(UnregisterAgentMessage message); Task<NoReply> HandleUnregisterAgent(UnregisterAgentMessage message);
Task<NoReply> HandleAgentIsAlive(AgentIsAliveMessage message); Task<NoReply> HandleAgentIsAlive(AgentIsAliveMessage message);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@ -0,0 +1,15 @@
using System.Collections.Immutable;
using MemoryPack;
using Phantom.Utils.Rpc.Message;
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
) : IMessageToController {
public Task<NoReply> Accept(IMessageToControllerListener listener) {
return listener.HandleInstanceOutput(this);
}
}

View File

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

View File

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

View File

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

View File

@ -2,14 +2,14 @@
using Phantom.Common.Data.Instance; using Phantom.Common.Data.Instance;
using Phantom.Utils.Rpc.Message; using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.ToServer; namespace Phantom.Common.Messages.Agent.ToController;
[MemoryPackable(GenerateType.VersionTolerant)] [MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record ReportInstanceStatusMessage( public sealed partial record ReportInstanceStatusMessage(
[property: MemoryPackOrder(0)] Guid InstanceGuid, [property: MemoryPackOrder(0)] Guid InstanceGuid,
[property: MemoryPackOrder(1)] IInstanceStatus InstanceStatus [property: MemoryPackOrder(1)] IInstanceStatus InstanceStatus
) : IMessageToServer { ) : IMessageToController {
public Task<NoReply> Accept(IMessageToServerListener listener) { public Task<NoReply> Accept(IMessageToControllerListener listener) {
return listener.HandleReportInstanceStatus(this); return listener.HandleReportInstanceStatus(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 UnregisterAgentMessage : 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,35 @@
using System.Collections.Immutable;
using Phantom.Common.Data.Java;
using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.AuditLog;
using Phantom.Common.Data.Web.EventLog;
using Phantom.Common.Data.Web.Instance;
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<NoReply> HandleRegisterWeb(RegisterWebMessage message);
Task<NoReply> HandleUnregisterWeb(UnregisterWebMessage message);
Task<LogInSuccess?> HandleLogIn(LogInMessage message);
Task<CreateOrUpdateAdministratorUserResult> HandleCreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message);
Task<CreateUserResult> HandleCreateUser(CreateUserMessage message);
Task<ImmutableArray<UserInfo>> HandleGetUsers(GetUsersMessage message);
Task<ImmutableArray<RoleInfo>> HandleGetRoles(GetRolesMessage message);
Task<ImmutableDictionary<Guid, ImmutableArray<Guid>>> HandleGetUserRoles(GetUserRolesMessage message);
Task<ChangeUserRolesResult> HandleChangeUserRoles(ChangeUserRolesMessage message);
Task<DeleteUserResult> HandleDeleteUser(DeleteUserMessage message);
Task<InstanceActionResult<CreateOrUpdateInstanceResult>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message);
Task<InstanceActionResult<LaunchInstanceResult>> HandleLaunchInstance(LaunchInstanceMessage message);
Task<InstanceActionResult<StopInstanceResult>> HandleStopInstance(StopInstanceMessage message);
Task<InstanceActionResult<SendCommandToInstanceResult>> HandleSendCommandToInstance(SendCommandToInstanceMessage message);
Task<ImmutableArray<MinecraftVersion>> HandleGetMinecraftVersions(GetMinecraftVersionsMessage message);
Task<ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>> HandleGetAgentJavaRuntimes(GetAgentJavaRuntimesMessage message);
Task<ImmutableArray<AuditLogItem>> HandleGetAuditLog(GetAuditLogMessage message);
Task<ImmutableArray<EventLogItem>> HandleGetEventLog(GetEventLogMessage 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,13 @@
using Phantom.Common.Messages.Web.BiDirectional;
using Phantom.Common.Messages.Web.ToWeb;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.Web;
public interface IMessageToWebListener {
Task<NoReply> HandleRegisterWebResult(RegisterWebResultMessage message);
Task<NoReply> HandleRefreshAgents(RefreshAgentsMessage message);
Task<NoReply> HandleRefreshInstances(RefreshInstancesMessage message);
Task<NoReply> HandleInstanceOutput(InstanceOutputMessage message);
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,17 @@
using System.Collections.Immutable;
using MemoryPack;
using Phantom.Common.Data.Web.Users;
namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record ChangeUserRolesMessage(
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
[property: MemoryPackOrder(1)] Guid SubjectUserGuid,
[property: MemoryPackOrder(2)] ImmutableHashSet<Guid> AddToRoleGuids,
[property: MemoryPackOrder(3)] ImmutableHashSet<Guid> RemoveFromRoleGuids
) : IMessageToController<ChangeUserRolesResult> {
public Task<ChangeUserRolesResult> Accept(IMessageToControllerListener listener) {
return listener.HandleChangeUserRoles(this);
}
}

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 CreateOrUpdateAdministratorUserMessage(
[property: MemoryPackOrder(0)] string Username,
[property: MemoryPackOrder(1)] string Password
) : IMessageToController<CreateOrUpdateAdministratorUserResult> {
public Task<CreateOrUpdateAdministratorUserResult> Accept(IMessageToControllerListener listener) {
return listener.HandleCreateOrUpdateAdministratorUser(this);
}
}

View File

@ -0,0 +1,16 @@
using MemoryPack;
using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Instance;
namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record CreateOrUpdateInstanceMessage(
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
[property: MemoryPackOrder(1)] InstanceConfiguration Configuration
) : IMessageToController<InstanceActionResult<CreateOrUpdateInstanceResult>> {
public Task<InstanceActionResult<CreateOrUpdateInstanceResult>> Accept(IMessageToControllerListener listener) {
return listener.HandleCreateOrUpdateInstance(this);
}
}

View File

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

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 DeleteUserMessage(
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
[property: MemoryPackOrder(1)] Guid SubjectUserGuid
) : IMessageToController<DeleteUserResult> {
public Task<DeleteUserResult> Accept(IMessageToControllerListener listener) {
return listener.HandleDeleteUser(this);
}
}

View File

@ -0,0 +1,12 @@
using System.Collections.Immutable;
using MemoryPack;
using Phantom.Common.Data.Java;
namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record GetAgentJavaRuntimesMessage : IMessageToController<ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>> {
public Task<ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>> Accept(IMessageToControllerListener listener) {
return listener.HandleGetAgentJavaRuntimes(this);
}
}

View File

@ -0,0 +1,14 @@
using System.Collections.Immutable;
using MemoryPack;
using Phantom.Common.Data.Web.AuditLog;
namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record GetAuditLogMessage(
[property: MemoryPackOrder(0)] int Count
) : IMessageToController<ImmutableArray<AuditLogItem>> {
public Task<ImmutableArray<AuditLogItem>> Accept(IMessageToControllerListener listener) {
return listener.HandleGetAuditLog(this);
}
}

View File

@ -0,0 +1,14 @@
using System.Collections.Immutable;
using MemoryPack;
using Phantom.Common.Data.Web.EventLog;
namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record GetEventLogMessage(
[property: MemoryPackOrder(0)] int Count
) : IMessageToController<ImmutableArray<EventLogItem>> {
public Task<ImmutableArray<EventLogItem>> Accept(IMessageToControllerListener listener) {
return listener.HandleGetEventLog(this);
}
}

View File

@ -0,0 +1,12 @@
using System.Collections.Immutable;
using MemoryPack;
using Phantom.Common.Data.Minecraft;
namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record GetMinecraftVersionsMessage : IMessageToController<ImmutableArray<MinecraftVersion>> {
public Task<ImmutableArray<MinecraftVersion>> Accept(IMessageToControllerListener listener) {
return listener.HandleGetMinecraftVersions(this);
}
}

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