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

Compare commits

..

3 Commits

Author SHA1 Message Date
d9c187994b
WIP 2023-11-03 23:32:40 +01:00
3a18d2067f
WIP 2023-11-01 10:39:23 +01:00
149bb6e0f1
Reimplement Web service 2023-10-31 22:10:09 +01:00
22 changed files with 113 additions and 224 deletions

View File

@ -17,6 +17,5 @@ public sealed partial record AgentWithStats(
[property: MemoryPackOrder(9)] DateTimeOffset? LastPing,
[property: MemoryPackOrder(10)] bool IsOnline
) {
[MemoryPackIgnore]
public RamAllocationUnits? AvailableMemory => MaxMemory - Stats?.RunningInstanceMemory;
}

View File

@ -1,23 +0,0 @@
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

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

View File

@ -1,7 +1,4 @@
using System.Collections.Immutable;
using Phantom.Common.Data.Java;
using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Instance;
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Messages.Web.BiDirectional;
@ -15,8 +12,5 @@ public interface IMessageToControllerListener {
Task<LogInSuccess?> HandleLogIn(LogInMessage message);
Task<CreateOrUpdateAdministratorUserResult> HandleCreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message);
Task<InstanceActionResult<CreateOrUpdateInstanceResult>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message);
Task<InstanceActionResult<LaunchInstanceResult>> HandleLaunchInstance(LaunchInstanceMessage message);
Task<ImmutableArray<MinecraftVersion>> HandleGetMinecraftVersions(GetMinecraftVersionsMessage message);
Task<ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>> HandleGetAgentJavaRuntimes(GetAgentJavaRuntimes message);
Task<NoReply> HandleReply(ReplyMessage message);
}

View File

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

View File

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

View File

@ -1,14 +0,0 @@
using MemoryPack;
using Phantom.Common.Data.Replies;
namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record LaunchInstanceMessage(
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
[property: MemoryPackOrder(1)] Guid InstanceGuid
) : IMessageToController<InstanceActionResult<LaunchInstanceResult>> {
public Task<InstanceActionResult<LaunchInstanceResult>> Accept(IMessageToControllerListener listener) {
return listener.HandleLaunchInstance(this);
}
}

View File

@ -1,7 +1,4 @@
using System.Collections.Immutable;
using Phantom.Common.Data.Java;
using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Instance;
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Logging;
@ -23,9 +20,6 @@ public static class WebMessageRegistries {
ToController.Add<LogInMessage, LogInSuccess?>(1);
ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(2);
ToController.Add<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(3);
ToController.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(4);
ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(5);
ToController.Add<GetAgentJavaRuntimes, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(6);
ToController.Add<ReplyMessage>(127);
ToWeb.Add<RegisterWebResultMessage>(0);

View File

@ -60,7 +60,7 @@ public sealed class ControllerServices {
}
public WebMessageListener CreateWebMessageListener(RpcConnectionToClient<IMessageToWebListener> connection) {
return new WebMessageListener(connection, webAuthToken, UserManager, UserLoginManager, AgentManager, AgentJavaRuntimesManager, InstanceManager, MinecraftVersions, TaskManager);
return new WebMessageListener(connection, webAuthToken, UserManager, UserLoginManager, AgentManager, InstanceManager, TaskManager);
}
public async Task Initialize() {

View File

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

View File

@ -63,7 +63,7 @@ sealed class InstanceManager {
}
[SuppressMessage("ReSharper", "ConvertIfStatementToConditionalTernaryExpression")]
public async Task<InstanceActionResult<CreateOrUpdateInstanceResult>> CreateOrUpdateInstance(Guid auditLogUserGuid, InstanceConfiguration configuration) {
public async Task<InstanceActionResult<CreateOrUpdateInstanceResult>> CreateOrUpdateInstance(InstanceConfiguration configuration, Guid auditLogUserGuid) {
var agent = agentManager.GetAgent(configuration.AgentGuid);
if (agent == null) {
return InstanceActionResult.Concrete(CreateOrUpdateInstanceResult.AgentNotFound);
@ -176,38 +176,34 @@ sealed class InstanceManager {
return instances.ByGuid.TryGetValue(instanceGuid, out var instance) ? await SendInstanceActionMessage<TMessage, TReply>(instance, message) : InstanceActionResult.General<TReply>(InstanceActionGeneralResult.InstanceDoesNotExist);
}
public async Task<InstanceActionResult<LaunchInstanceResult>> LaunchInstance(Guid auditLogUserGuid, Guid instanceGuid) {
public async Task<InstanceActionResult<LaunchInstanceResult>> LaunchInstance(Guid instanceGuid) {
var result = await SendInstanceActionMessage<LaunchInstanceMessage, LaunchInstanceResult>(instanceGuid, new LaunchInstanceMessage(instanceGuid));
if (result.Is(LaunchInstanceResult.LaunchInitiated)) {
await HandleInstanceManuallyLaunchedOrStopped(instanceGuid, true, auditLogUserGuid, auditLogRepository => auditLogRepository.AddInstanceLaunchedEvent(instanceGuid));
await SetInstanceShouldLaunchAutomatically(instanceGuid, true);
}
return result;
}
public async Task<InstanceActionResult<StopInstanceResult>> StopInstance(Guid auditLogUserGuid, Guid instanceGuid, MinecraftStopStrategy stopStrategy) {
public async Task<InstanceActionResult<StopInstanceResult>> StopInstance(Guid instanceGuid, MinecraftStopStrategy stopStrategy) {
var result = await SendInstanceActionMessage<StopInstanceMessage, StopInstanceResult>(instanceGuid, new StopInstanceMessage(instanceGuid, stopStrategy));
if (result.Is(StopInstanceResult.StopInitiated)) {
await HandleInstanceManuallyLaunchedOrStopped(instanceGuid, false, auditLogUserGuid, auditLogRepository => auditLogRepository.AddInstanceLaunchedEvent(instanceGuid));
await SetInstanceShouldLaunchAutomatically(instanceGuid, false);
}
return result;
}
private async Task HandleInstanceManuallyLaunchedOrStopped(Guid instanceGuid, bool wasLaunched, Guid auditLogUserGuid, Action<AuditLogRepository> addAuditEvent) {
private async Task SetInstanceShouldLaunchAutomatically(Guid instanceGuid, bool shouldLaunchAutomatically) {
await modifyInstancesSemaphore.WaitAsync(cancellationToken);
try {
instances.ByGuid.TryReplace(instanceGuid, instance => instance with { LaunchAutomatically = wasLaunched });
instances.ByGuid.TryReplace(instanceGuid, instance => instance with { LaunchAutomatically = shouldLaunchAutomatically });
await using var db = dbProvider.Lazy();
var entity = await db.Ctx.Instances.FindAsync(new object[] { instanceGuid }, cancellationToken);
await using var ctx = dbProvider.Eager();
var entity = await ctx.Instances.FindAsync(new object[] { instanceGuid }, cancellationToken);
if (entity != null) {
entity.LaunchAutomatically = wasLaunched;
var auditLogRepository = new AuditLogRepository(db, auditLogUserGuid);
addAuditEvent(auditLogRepository);
await db.Ctx.SaveChangesAsync(cancellationToken);
entity.LaunchAutomatically = shouldLaunchAutomatically;
await ctx.SaveChangesAsync(cancellationToken);
}
} finally {
modifyInstancesSemaphore.Release();

View File

@ -1,7 +1,5 @@
using System.Collections.Immutable;
using Phantom.Common.Data;
using Phantom.Common.Data.Java;
using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Agent;
using Phantom.Common.Data.Web.Instance;
@ -11,7 +9,6 @@ using Phantom.Common.Messages.Web;
using Phantom.Common.Messages.Web.BiDirectional;
using Phantom.Common.Messages.Web.ToController;
using Phantom.Common.Messages.Web.ToWeb;
using Phantom.Controller.Minecraft;
using Phantom.Controller.Rpc;
using Phantom.Controller.Services.Agents;
using Phantom.Controller.Services.Instances;
@ -30,20 +27,16 @@ public sealed class WebMessageListener : IMessageToControllerListener {
private readonly UserManager userManager;
private readonly UserLoginManager userLoginManager;
private readonly AgentManager agentManager;
private readonly AgentJavaRuntimesManager agentJavaRuntimesManager;
private readonly InstanceManager instanceManager;
private readonly MinecraftVersions minecraftVersions;
private readonly TaskManager taskManager;
internal WebMessageListener(RpcConnectionToClient<IMessageToWebListener> connection, AuthToken authToken, UserManager userManager, UserLoginManager userLoginManager, AgentManager agentManager, AgentJavaRuntimesManager agentJavaRuntimesManager, InstanceManager instanceManager, MinecraftVersions minecraftVersions, TaskManager taskManager) {
internal WebMessageListener(RpcConnectionToClient<IMessageToWebListener> connection, AuthToken authToken, UserManager userManager, UserLoginManager userLoginManager, AgentManager agentManager, InstanceManager instanceManager, TaskManager taskManager) {
this.connection = connection;
this.authToken = authToken;
this.userManager = userManager;
this.userLoginManager = userLoginManager;
this.agentManager = agentManager;
this.agentJavaRuntimesManager = agentJavaRuntimesManager;
this.instanceManager = instanceManager;
this.minecraftVersions = minecraftVersions;
this.taskManager = taskManager;
}
@ -79,19 +72,7 @@ public sealed class WebMessageListener : IMessageToControllerListener {
}
public Task<InstanceActionResult<CreateOrUpdateInstanceResult>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message) {
return instanceManager.CreateOrUpdateInstance( message.LoggedInUserGuid, message.Configuration);
}
public Task<InstanceActionResult<LaunchInstanceResult>> HandleLaunchInstance(LaunchInstanceMessage message) {
return instanceManager.LaunchInstance(message.LoggedInUserGuid, message.InstanceGuid);
}
public Task<ImmutableArray<MinecraftVersion>> HandleGetMinecraftVersions(GetMinecraftVersionsMessage message) {
return minecraftVersions.GetVersions(CancellationToken.None);
}
public Task<ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>> HandleGetAgentJavaRuntimes(GetAgentJavaRuntimes message) {
return Task.FromResult(agentJavaRuntimesManager.All);
return instanceManager.CreateOrUpdateInstance(message.Configuration, message.LoggedInUserGuid);
}
public Task<LogInSuccess?> HandleLogIn(LogInMessage message) {

View File

@ -10,11 +10,11 @@ public sealed class AgentManager {
public EventSubscribers<ImmutableArray<AgentWithStats>> AgentsChanged => agents.Subs;
internal void RefreshAgents(ImmutableArray<AgentWithStats> newAgents) {
agents.SetTo(newAgents);
}
public ImmutableDictionary<Guid, AgentWithStats> ToDictionaryByGuid() {
return agents.Value.ToImmutableDictionary(static agent => agent.Guid);
}
internal void RefreshAgents(ImmutableArray<AgentWithStats> newAgents) {
agents.SetTo(newAgents);
}
}

View File

@ -3,10 +3,10 @@ using Phantom.Common.Data.Web.Users;
namespace Phantom.Web.Services.Authentication;
public sealed record UserInfo(Guid UserGuid, string Username, PermissionSet Permissions) {
sealed record UserInfo(Guid UserGuid, string Username, PermissionSet Permissions) {
private const string AuthenticationType = "Phantom";
internal ClaimsPrincipal AsClaimsPrincipal {
public ClaimsPrincipal AsClaimsPrincipal {
get {
var identity = new ClaimsIdentity(AuthenticationType);
@ -18,6 +18,6 @@ public sealed record UserInfo(Guid UserGuid, string Username, PermissionSet Perm
}
public static Guid? TryGetGuid(ClaimsPrincipal principal) {
return principal.Identity is { IsAuthenticated: true, AuthenticationType: AuthenticationType } && principal.FindFirstValue(ClaimTypes.NameIdentifier) is {} guidStr && Guid.TryParse(guidStr, out var guid) ? guid : null;
return principal.FindFirstValue(ClaimTypes.NameIdentifier) is {} guidStr && Guid.TryParse(guidStr, out var guid) ? guid : null;
}
}

View File

@ -9,33 +9,21 @@ using Phantom.Web.Services.Rpc;
namespace Phantom.Web.Services.Instances;
using InstanceDictionary = ImmutableDictionary<Guid, Instance>;
public sealed class InstanceManager {
private readonly ControllerConnection controllerConnection;
private readonly SimpleObservableState<InstanceDictionary> instances = new (PhantomLogger.Create<InstanceManager>("Instances"), InstanceDictionary.Empty);
private readonly SimpleObservableState<ImmutableArray<Instance>> instances = new (PhantomLogger.Create<InstanceManager>("Instances"), ImmutableArray<Instance>.Empty);
public InstanceManager(ControllerConnection controllerConnection) {
this.controllerConnection = controllerConnection;
}
public EventSubscribers<InstanceDictionary> InstancesChanged => instances.Subs;
public EventSubscribers<ImmutableArray<Instance>> InstancesChanged => instances.Subs;
internal void RefreshInstances(ImmutableArray<Instance> newInstances) {
instances.SetTo(newInstances.ToImmutableDictionary(static instance => instance.Configuration.InstanceGuid));
instances.SetTo(newInstances);
}
public Instance? GetByGuid(Guid instanceGuid) {
return instances.Value.GetValueOrDefault(instanceGuid);
}
public Task<InstanceActionResult<CreateOrUpdateInstanceResult>> CreateOrUpdateInstance(Guid loggedInUserGuid, InstanceConfiguration configuration) {
var message = new CreateOrUpdateInstanceMessage(loggedInUserGuid, configuration);
return controllerConnection.Send<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(message, TimeSpan.FromSeconds(30));
}
public Task<InstanceActionResult<LaunchInstanceResult>> LaunchInstance(Guid loggedInUserGuid, Guid instanceGuid) {
var message = new LaunchInstanceMessage(loggedInUserGuid, instanceGuid);
return controllerConnection.Send<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(message, TimeSpan.FromSeconds(30));
public Task<InstanceActionResult<CreateOrUpdateInstanceResult>> CreateOrUpdateInstance(InstanceConfiguration configuration) {
return controllerConnection.Send<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(new CreateOrUpdateInstanceMessage(configuration), TimeSpan.FromSeconds(30));
}
}

View File

@ -14,7 +14,7 @@ public sealed class ControllerConnection {
return connection.Send(message);
}
public Task<TReply> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken = default) where TMessage : IMessageToController<TReply> {
return connection.Send<TMessage, TReply>(message, waitForReplyTime, waitForReplyCancellationToken);
public Task<TReply> Send<TMessage, TReply>(TMessage message, TimeSpan timeout) where TMessage : IMessageToController<TReply> {
return connection.Send<TMessage, TReply>(message, timeout, CancellationToken.None);
}
}

View File

@ -4,7 +4,6 @@ using Phantom.Common.Data.Web.Users;
using Phantom.Common.Logging;
using Phantom.Web.Services.Authorization;
using ILogger = Serilog.ILogger;
using UserInfo = Phantom.Web.Services.Authentication.UserInfo;
namespace Phantom.Web.Base;
@ -17,11 +16,6 @@ public abstract class PhantomComponent : ComponentBase {
[Inject]
public PermissionManager PermissionManager { get; set; } = null!;
public async Task<Guid?> GetUserGuid() {
var authenticationState = await AuthenticationStateTask;
return UserInfo.TryGetGuid(authenticationState.User);
}
protected async Task<bool> CheckPermission(Permission permission) {
var authenticationState = await AuthenticationStateTask;
return PermissionManager.CheckPermission(authenticationState.User, permission);

View File

@ -1,13 +1,12 @@
@page "/instances/{InstanceGuid:guid}"
@attribute [Authorize(Permission.ViewInstancesPolicy)]
@inherits PhantomComponent
@using Phantom.Common.Data.Instance
@using Phantom.Common.Data.Replies
@using Phantom.Common.Data.Web.Instance
@using Phantom.Common.Data.Web.Users
@using Phantom.Web.Services.Instances
@using Phantom.Common.Data.Web.Users
@using Phantom.Common.Data.Replies
@implements IDisposable
@inject InstanceManager InstanceManager
@inject AuditLog AuditLog
@if (Instance == null) {
<h1>Instance Not Found</h1>
@ -46,7 +45,7 @@ else {
@code {
[Parameter]
public Guid InstanceGuid { get; init; }
public Guid InstanceGuid { get; set; }
private string? lastError = null;
private bool isLaunchingInstance = false;
@ -64,11 +63,6 @@ else {
}
private async Task LaunchInstance() {
var loggedInUserGuid = await GetUserGuid();
if (loggedInUserGuid == null) {
return;
}
isLaunchingInstance = true;
lastError = null;
@ -78,8 +72,11 @@ else {
return;
}
var result = await InstanceManager.LaunchInstance(loggedInUserGuid.Value, InstanceGuid);
if (!result.Is(LaunchInstanceResult.LaunchInitiated)) {
var result = await InstanceManager.LaunchInstance(InstanceGuid);
if (result.Is(LaunchInstanceResult.LaunchInitiated)) {
await AuditLog.AddInstanceLaunchedEvent(InstanceGuid);
}
else {
lastError = result.ToSentence(Messages.ToSentence);
}
} finally {

View File

@ -18,12 +18,12 @@ else {
@code {
[Parameter]
public Guid InstanceGuid { get; init; }
public Guid InstanceGuid { get; set; }
private InstanceConfiguration? InstanceConfiguration { get; set; }
protected override void OnInitialized() {
InstanceConfiguration = InstanceManager.GetByGuid(InstanceGuid)?.Configuration;
InstanceConfiguration = InstanceManager.GetInstanceConfiguration(InstanceGuid);
}
}

View File

@ -83,8 +83,7 @@
});
InstanceManager.InstancesChanged.Subscribe(this, instances => {
this.instances = instances.Values
.OrderBy(instance => agentNames.TryGetValue(instance.Configuration.AgentGuid, out var agentName) ? agentName : string.Empty)
this.instances = instances.OrderBy(instance => agentNames.TryGetValue(instance.Configuration.AgentGuid, out var agentName) ? agentName : string.Empty)
.ThenBy(static instance => instance.Configuration.InstanceName)
.ToImmutableArray();
InvokeAsync(StateHasChanged);

View File

@ -87,7 +87,7 @@
}
private async Task<Result<string>> CreateOrUpdateAdministrator() {
var reply = await ControllerConnection.Send<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(new CreateOrUpdateAdministratorUserMessage(form.Username, form.Password), Timeout.InfiniteTimeSpan);
var reply = await ControllerConnection.Send<CreateOrUpdateAdministratorUser, CreateOrUpdateAdministratorUserResult>(new CreateOrUpdateAdministratorUser(form.Username, form.Password), Timeout.InfiniteTimeSpan);
return reply switch {
Success => Result.Ok<string>(),
CreationFailed fail => fail.Error.ToSentences("\n"),

View File

@ -1,31 +1,27 @@
@using Phantom.Web.Components.Utils
@using Phantom.Common.Data.Minecraft
@using Phantom.Common.Data.Web.Minecraft
@using Phantom.Common.Data.Instance
@using Phantom.Common.Data.Java
@using System.Collections.Immutable
@using System.ComponentModel.DataAnnotations
@using System.Diagnostics.CodeAnalysis
@using Phantom.Common.Data.Minecraft
@using Phantom.Common.Data.Web.Agent
@using Phantom.Common.Data.Web.Instance
@using Phantom.Common.Data.Web.Minecraft
@using Phantom.Common.Messages.Web.ToController
@using Phantom.Common.Data.Instance
@using Phantom.Common.Data.Java
@using Phantom.Common.Data
@using Phantom.Web.Services
@using Phantom.Web.Services.Agents
@using Phantom.Web.Services.Instances
@using Phantom.Web.Services.Rpc
@inherits PhantomComponent
@inject INavigation Nav
@inject ControllerConnection ControllerConnection
@inject MinecraftVersions MinecraftVersions
@inject AgentManager AgentManager
@inject AgentJavaRuntimesManager AgentJavaRuntimesManager
@inject InstanceManager InstanceManager
@inject AuditLog AuditLog
<Form Model="form" OnSubmit="AddOrEditInstance">
@{ var selectedAgent = form.SelectedAgent; }
<div class="row">
<div class="col-xl-7 mb-3">
@{
static RenderFragment GetAgentOption(AgentWithStats agent) {
static RenderFragment GetAgentOption(Agent agent) {
return @<option value="@agent.Guid">
@agent.Name
&bullet;
@ -38,14 +34,14 @@
@if (EditedInstanceConfiguration == null) {
<FormSelectInput Id="instance-agent" Label="Agent" @bind-Value="form.SelectedAgentGuid">
<option value="" selected>Select which agent will run the instance...</option>
@foreach (var agent in allAgentsByGuid.Values.Where(static agent => agent.IsOnline).OrderBy(static agent => agent.Name)) {
@foreach (var agent in form.AgentsByGuid.Values.Where(static agent => agent.IsOnline).OrderBy(static agent => agent.Name)) {
@GetAgentOption(agent)
}
</FormSelectInput>
}
else {
<FormSelectInput Id="instance-agent" Label="Agent" @bind-Value="form.SelectedAgentGuid" disabled="true">
@if (form.SelectedAgentGuid is {} guid && allAgentsByGuid.TryGetValue(guid, out var agent)) {
@if (form.SelectedAgentGuid is {} guid && form.AgentsByGuid.TryGetValue(guid, out var agent)) {
@GetAgentOption(agent)
}
</FormSelectInput>
@ -164,25 +160,23 @@
@code {
[Parameter, EditorRequired]
public InstanceConfiguration? EditedInstanceConfiguration { get; init; }
public InstanceConfiguration? EditedInstanceConfiguration { get; set; }
private ConfigureInstanceFormModel form = null!;
private ImmutableDictionary<Guid, AgentWithStats> allAgentsByGuid = ImmutableDictionary<Guid, AgentWithStats>.Empty;
private ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>> allAgentJavaRuntimes = ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>.Empty;
private MinecraftVersionType minecraftVersionType = MinecraftVersionType.Release;
private ImmutableArray<MinecraftVersion> allMinecraftVersions = ImmutableArray<MinecraftVersion>.Empty;
private ImmutableArray<MinecraftVersion> availableMinecraftVersions = ImmutableArray<MinecraftVersion>.Empty;
private bool IsSubmittable => form.SelectedAgentGuid != null && !form.EditContext.GetValidationMessages(form.EditContext.Field(nameof(ConfigureInstanceFormModel.SelectedAgentGuid))).Any();
private sealed class ConfigureInstanceFormModel : FormModel {
private readonly InstanceAddOrEditForm page;
public ImmutableDictionary<Guid, Agent> AgentsByGuid { get; }
private readonly ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>> javaRuntimesByAgentGuid;
private readonly RamAllocationUnits? editedInstanceRamAllocation;
public ConfigureInstanceFormModel(InstanceAddOrEditForm page, RamAllocationUnits? editedInstanceRamAllocation) {
this.page = page;
public ConfigureInstanceFormModel(AgentManager agentManager, AgentJavaRuntimesManager agentJavaRuntimesManager, RamAllocationUnits? editedInstanceRamAllocation) {
this.AgentsByGuid = agentManager.GetAgents().ToImmutableDictionary();
this.javaRuntimesByAgentGuid = agentJavaRuntimesManager.All;
this.editedInstanceRamAllocation = editedInstanceRamAllocation;
}
@ -196,13 +190,13 @@
}
}
private bool TryGetAgent(Guid? agentGuid, [NotNullWhen(true)] out AgentWithStats? agent) {
return TryGet(page.allAgentsByGuid, agentGuid, out agent);
private bool TryGetAgent(Guid? agentGuid, [NotNullWhen(true)] out Agent? agent) {
return TryGet(AgentsByGuid, agentGuid, out agent);
}
public AgentWithStats? SelectedAgent => TryGetAgent(SelectedAgentGuid, out var agent) ? agent : null;
public Agent? SelectedAgent => TryGetAgent(SelectedAgentGuid, out var agent) ? agent : null;
public ImmutableArray<TaggedJavaRuntime> JavaRuntimesForSelectedAgent => TryGet(page.allAgentJavaRuntimes, SelectedAgentGuid, out var javaRuntimes) ? javaRuntimes : ImmutableArray<TaggedJavaRuntime>.Empty;
public ImmutableArray<TaggedJavaRuntime> JavaRuntimesForSelectedAgent => TryGet(javaRuntimesByAgentGuid, SelectedAgentGuid, out var javaRuntimes) ? javaRuntimes : ImmutableArray<TaggedJavaRuntime>.Empty;
public ushort MaximumMemoryUnits => SelectedAgent?.MaxMemory.RawValue ?? 0;
public ushort AvailableMemoryUnits => Math.Min((SelectedAgent?.AvailableMemory + editedInstanceRamAllocation)?.RawValue ?? MaximumMemoryUnits, MaximumMemoryUnits);
@ -269,16 +263,7 @@
}
protected override void OnInitialized() {
form = new ConfigureInstanceFormModel(this, EditedInstanceConfiguration?.MemoryAllocation);
}
protected override async Task OnInitializedAsync() {
var agentJavaRuntimesTask = ControllerConnection.Send<GetAgentJavaRuntimes, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(new GetAgentJavaRuntimes(), TimeSpan.FromSeconds(30));
var minecraftVersionsTask = ControllerConnection.Send<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(new GetMinecraftVersionsMessage(), TimeSpan.FromSeconds(30));
allAgentsByGuid = AgentManager.ToDictionaryByGuid();
allAgentJavaRuntimes = await agentJavaRuntimesTask;
allMinecraftVersions = await minecraftVersionsTask;
form = new ConfigureInstanceFormModel(AgentManager, AgentJavaRuntimesManager, EditedInstanceConfiguration?.MemoryAllocation);
if (EditedInstanceConfiguration != null) {
form.SelectedAgentGuid = EditedInstanceConfiguration.AgentGuid;
@ -290,8 +275,6 @@
form.MemoryUnits = EditedInstanceConfiguration.MemoryAllocation.RawValue;
form.JavaRuntimeGuid = EditedInstanceConfiguration.JavaRuntimeGuid;
form.JvmArguments = JvmArgumentsHelper.Join(EditedInstanceConfiguration.JvmArguments);
minecraftVersionType = allMinecraftVersions.FirstOrDefault(version => version.Id == EditedInstanceConfiguration.MinecraftVersion)?.Type ?? minecraftVersionType;
}
form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.MemoryUnits));
@ -299,12 +282,21 @@
form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.ServerPort));
form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.RconPort));
form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.ServerPort), revalidated: nameof(ConfigureInstanceFormModel.RconPort));
SetMinecraftVersionType(minecraftVersionType);
}
private void SetMinecraftVersionType(MinecraftVersionType type) {
protected override async Task OnInitializedAsync() {
if (EditedInstanceConfiguration != null) {
var allMinecraftVersions = await MinecraftVersions.GetVersions(CancellationToken.None);
minecraftVersionType = allMinecraftVersions.FirstOrDefault(version => version.Id == EditedInstanceConfiguration.MinecraftVersion)?.Type ?? minecraftVersionType;
}
await SetMinecraftVersionType(minecraftVersionType);
}
private async Task SetMinecraftVersionType(MinecraftVersionType type) {
minecraftVersionType = type;
var allMinecraftVersions = await MinecraftVersions.GetVersions(CancellationToken.None);
availableMinecraftVersions = allMinecraftVersions.Where(version => version.Type == type).ToImmutableArray();
if (!availableMinecraftVersions.IsEmpty && !availableMinecraftVersions.Any(version => version.Id == form.MinecraftVersion)) {
@ -313,11 +305,6 @@
}
private async Task AddOrEditInstance(EditContext context) {
var loggedInUserGuid = await GetUserGuid();
if (loggedInUserGuid == null) {
return;
}
var selectedAgent = form.SelectedAgent;
if (selectedAgent == null) {
return;
@ -338,12 +325,13 @@
JvmArgumentsHelper.Split(form.JvmArguments)
);
var result = await InstanceManager.CreateOrUpdateInstance(loggedInUserGuid.Value, instance);
if (result.Is(CreateOrUpdateInstanceResult.Success)) {
await Nav.NavigateTo("instances/" + instance.InstanceGuid);
var result = await InstanceManager.AddOrEditInstance(instance);
if (result.Is(AddOrEditInstanceResult.Success)) {
await (EditedInstanceConfiguration == null ? AuditLog.AddInstanceCreatedEvent(instance.InstanceGuid) : AuditLog.AddInstanceEditedEvent(instance.InstanceGuid));
Nav.NavigateTo("instances/" + instance.InstanceGuid);
}
else {
form.SubmitModel.StopSubmitting(result.ToSentence(CreateOrUpdateInstanceResultExtensions.ToSentence));
form.SubmitModel.StopSubmitting(result.ToSentence(AddOrEditInstanceResultExtensions.ToSentence));
}
}