mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2025-04-29 04:15:45 +02:00
242 lines
11 KiB
C#
242 lines
11 KiB
C#
using System.Collections.Immutable;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using Phantom.Common.Data;
|
|
using Phantom.Common.Data.Instance;
|
|
using Phantom.Common.Data.Minecraft;
|
|
using Phantom.Common.Data.Replies;
|
|
using Phantom.Common.Data.Web.Instance;
|
|
using Phantom.Common.Data.Web.Minecraft;
|
|
using Phantom.Common.Logging;
|
|
using Phantom.Common.Messages.Agent;
|
|
using Phantom.Common.Messages.Agent.ToAgent;
|
|
using Phantom.Controller.Database;
|
|
using Phantom.Controller.Database.Entities;
|
|
using Phantom.Controller.Database.Repositories;
|
|
using Phantom.Controller.Minecraft;
|
|
using Phantom.Controller.Services.Agents;
|
|
using Phantom.Utils.Collections;
|
|
using Phantom.Utils.Events;
|
|
using Serilog;
|
|
|
|
namespace Phantom.Controller.Services.Instances;
|
|
|
|
sealed class InstanceManager {
|
|
private static readonly ILogger Logger = PhantomLogger.Create<InstanceManager>();
|
|
|
|
private readonly ObservableInstances instances = new (PhantomLogger.Create<InstanceManager, ObservableInstances>());
|
|
|
|
public EventSubscribers<ImmutableDictionary<Guid, Instance>> InstancesChanged => instances.Subs;
|
|
|
|
private readonly AgentManager agentManager;
|
|
private readonly MinecraftVersions minecraftVersions;
|
|
private readonly IDbContextProvider dbProvider;
|
|
private readonly CancellationToken cancellationToken;
|
|
|
|
private readonly SemaphoreSlim modifyInstancesSemaphore = new (1, 1);
|
|
|
|
public InstanceManager(AgentManager agentManager, MinecraftVersions minecraftVersions, IDbContextProvider dbProvider, CancellationToken cancellationToken) {
|
|
this.agentManager = agentManager;
|
|
this.minecraftVersions = minecraftVersions;
|
|
this.dbProvider = dbProvider;
|
|
this.cancellationToken = cancellationToken;
|
|
}
|
|
|
|
public async Task Initialize() {
|
|
await using var ctx = dbProvider.Eager();
|
|
await foreach (var entity in ctx.Instances.AsAsyncEnumerable().WithCancellation(cancellationToken)) {
|
|
var configuration = new InstanceConfiguration(
|
|
entity.AgentGuid,
|
|
entity.InstanceGuid,
|
|
entity.InstanceName,
|
|
entity.ServerPort,
|
|
entity.RconPort,
|
|
entity.MinecraftVersion,
|
|
entity.MinecraftServerKind,
|
|
entity.MemoryAllocation,
|
|
entity.JavaRuntimeGuid,
|
|
JvmArgumentsHelper.Split(entity.JvmArguments)
|
|
);
|
|
|
|
var instance = Instance.Offline(configuration, entity.LaunchAutomatically);
|
|
instances.ByGuid[instance.Configuration.InstanceGuid] = instance;
|
|
}
|
|
}
|
|
|
|
[SuppressMessage("ReSharper", "ConvertIfStatementToConditionalTernaryExpression")]
|
|
public async Task<InstanceActionResult<CreateOrUpdateInstanceResult>> CreateOrUpdateInstance(Guid auditLogUserGuid, InstanceConfiguration configuration) {
|
|
var agent = agentManager.GetAgent(configuration.AgentGuid);
|
|
if (agent == null) {
|
|
return InstanceActionResult.Concrete(CreateOrUpdateInstanceResult.AgentNotFound);
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(configuration.InstanceName)) {
|
|
return InstanceActionResult.Concrete(CreateOrUpdateInstanceResult.InstanceNameMustNotBeEmpty);
|
|
}
|
|
|
|
if (configuration.MemoryAllocation <= RamAllocationUnits.Zero) {
|
|
return InstanceActionResult.Concrete(CreateOrUpdateInstanceResult.InstanceMemoryMustNotBeZero);
|
|
}
|
|
|
|
var serverExecutableInfo = await minecraftVersions.GetServerExecutableInfo(configuration.MinecraftVersion, cancellationToken);
|
|
if (serverExecutableInfo == null) {
|
|
return InstanceActionResult.Concrete(CreateOrUpdateInstanceResult.MinecraftVersionDownloadInfoNotFound);
|
|
}
|
|
|
|
InstanceActionResult<CreateOrUpdateInstanceResult> result;
|
|
bool isNewInstance;
|
|
|
|
await modifyInstancesSemaphore.WaitAsync(cancellationToken);
|
|
try {
|
|
isNewInstance = !instances.ByGuid.TryReplace(configuration.InstanceGuid, instance => instance with { Configuration = configuration });
|
|
if (isNewInstance) {
|
|
instances.ByGuid.TryAdd(configuration.InstanceGuid, Instance.Offline(configuration));
|
|
}
|
|
|
|
var message = new ConfigureInstanceMessage(configuration, new InstanceLaunchProperties(serverExecutableInfo));
|
|
var reply = await agentManager.SendMessage<ConfigureInstanceMessage, InstanceActionResult<ConfigureInstanceResult>>(configuration.AgentGuid, message, TimeSpan.FromSeconds(10));
|
|
|
|
result = reply.DidNotReplyIfNull().Map(static result => result switch {
|
|
ConfigureInstanceResult.Success => CreateOrUpdateInstanceResult.Success,
|
|
_ => CreateOrUpdateInstanceResult.UnknownError
|
|
});
|
|
|
|
if (result.Is(CreateOrUpdateInstanceResult.Success)) {
|
|
await using var db = dbProvider.Lazy();
|
|
|
|
InstanceEntity entity = db.Ctx.InstanceUpsert.Fetch(configuration.InstanceGuid);
|
|
entity.AgentGuid = configuration.AgentGuid;
|
|
entity.InstanceName = configuration.InstanceName;
|
|
entity.ServerPort = configuration.ServerPort;
|
|
entity.RconPort = configuration.RconPort;
|
|
entity.MinecraftVersion = configuration.MinecraftVersion;
|
|
entity.MinecraftServerKind = configuration.MinecraftServerKind;
|
|
entity.MemoryAllocation = configuration.MemoryAllocation;
|
|
entity.JavaRuntimeGuid = configuration.JavaRuntimeGuid;
|
|
entity.JvmArguments = JvmArgumentsHelper.Join(configuration.JvmArguments);
|
|
|
|
var auditLogWriter = new AuditLogRepository(db).Writer(auditLogUserGuid);
|
|
if (isNewInstance) {
|
|
auditLogWriter.InstanceCreated(configuration.InstanceGuid);
|
|
}
|
|
else {
|
|
auditLogWriter.InstanceEdited(configuration.InstanceGuid);
|
|
}
|
|
|
|
await db.Ctx.SaveChangesAsync(cancellationToken);
|
|
}
|
|
else if (isNewInstance) {
|
|
instances.ByGuid.Remove(configuration.InstanceGuid);
|
|
}
|
|
} finally {
|
|
modifyInstancesSemaphore.Release();
|
|
}
|
|
|
|
if (result.Is(CreateOrUpdateInstanceResult.Success)) {
|
|
if (isNewInstance) {
|
|
Logger.Information("Added instance \"{InstanceName}\" (GUID {InstanceGuid}) to agent \"{AgentName}\".", configuration.InstanceName, configuration.InstanceGuid, agent.Name);
|
|
}
|
|
else {
|
|
Logger.Information("Edited instance \"{InstanceName}\" (GUID {InstanceGuid}) in agent \"{AgentName}\".", configuration.InstanceName, configuration.InstanceGuid, agent.Name);
|
|
}
|
|
}
|
|
else {
|
|
if (isNewInstance) {
|
|
Logger.Information("Failed adding instance \"{InstanceName}\" (GUID {InstanceGuid}) to agent \"{AgentName}\". {ErrorMessage}", configuration.InstanceName, configuration.InstanceGuid, agent.Name, result.ToSentence(CreateOrUpdateInstanceResultExtensions.ToSentence));
|
|
}
|
|
else {
|
|
Logger.Information("Failed editing instance \"{InstanceName}\" (GUID {InstanceGuid}) in agent \"{AgentName}\". {ErrorMessage}", configuration.InstanceName, configuration.InstanceGuid, agent.Name, result.ToSentence(CreateOrUpdateInstanceResultExtensions.ToSentence));
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
internal void SetInstanceState(Guid instanceGuid, IInstanceStatus instanceStatus) {
|
|
instances.ByGuid.TryReplace(instanceGuid, instance => instance with { Status = instanceStatus });
|
|
}
|
|
|
|
internal void SetInstanceStatesForAgent(Guid agentGuid, IInstanceStatus instanceStatus) {
|
|
instances.ByGuid.ReplaceAllIf(instance => instance with { Status = instanceStatus }, instance => instance.Configuration.AgentGuid == agentGuid);
|
|
}
|
|
|
|
private async Task<InstanceActionResult<TReply>> SendInstanceActionMessage<TMessage, TReply>(Instance instance, TMessage message) where TMessage : IMessageToAgent<InstanceActionResult<TReply>> {
|
|
var reply = await agentManager.SendMessage<TMessage, InstanceActionResult<TReply>>(instance.Configuration.AgentGuid, message, TimeSpan.FromSeconds(10));
|
|
return reply.DidNotReplyIfNull();
|
|
}
|
|
|
|
private async Task<InstanceActionResult<TReply>> SendInstanceActionMessage<TMessage, TReply>(Guid instanceGuid, TMessage message) where TMessage : IMessageToAgent<InstanceActionResult<TReply>> {
|
|
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) {
|
|
var result = await SendInstanceActionMessage<LaunchInstanceMessage, LaunchInstanceResult>(instanceGuid, new LaunchInstanceMessage(instanceGuid));
|
|
if (result.Is(LaunchInstanceResult.LaunchInitiated)) {
|
|
await HandleInstanceManuallyLaunchedOrStopped(instanceGuid, true, auditLogUserGuid, auditLogWriter => auditLogWriter.InstanceLaunched(instanceGuid));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public async Task<InstanceActionResult<StopInstanceResult>> StopInstance(Guid auditLogUserGuid, 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, auditLogWriter => auditLogWriter.InstanceStopped(instanceGuid, stopStrategy.Seconds));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private async Task HandleInstanceManuallyLaunchedOrStopped(Guid instanceGuid, bool wasLaunched, Guid auditLogUserGuid, Action<AuditLogRepository.ItemWriter> addAuditEvent) {
|
|
await modifyInstancesSemaphore.WaitAsync(cancellationToken);
|
|
try {
|
|
instances.ByGuid.TryReplace(instanceGuid, instance => instance with { LaunchAutomatically = wasLaunched });
|
|
|
|
await using var db = dbProvider.Lazy();
|
|
var entity = await db.Ctx.Instances.FindAsync(new object[] { instanceGuid }, cancellationToken);
|
|
if (entity != null) {
|
|
entity.LaunchAutomatically = wasLaunched;
|
|
addAuditEvent(new AuditLogRepository(db).Writer(auditLogUserGuid));
|
|
await db.Ctx.SaveChangesAsync(cancellationToken);
|
|
}
|
|
} finally {
|
|
modifyInstancesSemaphore.Release();
|
|
}
|
|
}
|
|
|
|
public async Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommand(Guid auditLogUserId, Guid instanceGuid, string command) {
|
|
var result = await SendInstanceActionMessage<SendCommandToInstanceMessage, SendCommandToInstanceResult>(instanceGuid, new SendCommandToInstanceMessage(instanceGuid, command));
|
|
if (result.Is(SendCommandToInstanceResult.Success)) {
|
|
await using var db = dbProvider.Lazy();
|
|
var auditLogWriter = new AuditLogRepository(db).Writer(auditLogUserId);
|
|
|
|
auditLogWriter.InstanceCommandExecuted(instanceGuid, command);
|
|
await db.Ctx.SaveChangesAsync(cancellationToken);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
internal async Task<ImmutableArray<ConfigureInstanceMessage>> GetInstanceConfigurationsForAgent(Guid agentGuid) {
|
|
var configurationMessages = ImmutableArray.CreateBuilder<ConfigureInstanceMessage>();
|
|
|
|
foreach (var (configuration, _, launchAutomatically) in instances.ByGuid.ValuesCopy.Where(instance => instance.Configuration.AgentGuid == agentGuid)) {
|
|
var serverExecutableInfo = await minecraftVersions.GetServerExecutableInfo(configuration.MinecraftVersion, cancellationToken);
|
|
configurationMessages.Add(new ConfigureInstanceMessage(configuration, new InstanceLaunchProperties(serverExecutableInfo), launchAutomatically));
|
|
}
|
|
|
|
return configurationMessages.ToImmutable();
|
|
}
|
|
|
|
private sealed class ObservableInstances : ObservableState<ImmutableDictionary<Guid, Instance>> {
|
|
public RwLockedObservableDictionary<Guid, Instance> ByGuid { get; } = new (LockRecursionPolicy.NoRecursion);
|
|
|
|
public ObservableInstances(ILogger logger) : base(logger) {
|
|
ByGuid.CollectionChanged += Update;
|
|
}
|
|
|
|
protected override ImmutableDictionary<Guid, Instance> GetData() {
|
|
return ByGuid.ToImmutable();
|
|
}
|
|
}
|
|
}
|