1
0
mirror of https://github.com/chylex/Minecraft-Phantom-Panel.git synced 2026-05-04 00:03:08 +02:00
Files
Minecraft-Phantom-Panel/Agent/Phantom.Agent.Services/Instances/InstanceManagerActor.cs
2026-04-06 09:33:01 +02:00

208 lines
8.9 KiB
C#

using Phantom.Agent.Services.Backups;
using Phantom.Agent.Services.Downloads;
using Phantom.Agent.Services.Instances.Launch;
using Phantom.Agent.Services.Java;
using Phantom.Agent.Services.Rpc;
using Phantom.Common.Data;
using Phantom.Common.Data.Agent.Instance;
using Phantom.Common.Data.Agent.Instance.Backups;
using Phantom.Common.Data.Agent.Instance.Launch;
using Phantom.Common.Data.Agent.Instance.Stop;
using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Replies;
using Phantom.Utils.Actor;
using Phantom.Utils.IO;
using Phantom.Utils.Logging;
using Serilog;
namespace Phantom.Agent.Services.Instances;
sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand> {
private static readonly ILogger Logger = PhantomLogger.Create<InstanceManagerActor>();
public readonly record struct Init(ControllerConnection ControllerConnection, AgentDirectories AgentDirectories, AgentState AgentState, JavaRuntimeRepository JavaRuntimeRepository, InstanceTicketManager InstanceTicketManager, BackupManager BackupManager);
public static Props<ICommand> Factory(Init init) {
return Props<ICommand>.Create(() => new InstanceManagerActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume });
}
private readonly AgentState agentState;
private readonly AgentDirectories agentDirectories;
private readonly InstanceServices instanceServices;
private readonly InstanceTicketManager instanceTicketManager;
private readonly Dictionary<Guid, Instance> instances = new ();
private readonly CancellationTokenSource shutdownCancellationTokenSource = new ();
private readonly CancellationToken shutdownCancellationToken;
private uint instanceLoggerSequenceId = 0;
private InstanceManagerActor(Init init) {
this.agentState = init.AgentState;
this.agentDirectories = init.AgentDirectories;
this.instanceServices = new InstanceServices(init.ControllerConnection, init.BackupManager, new FileDownloadManager(), init.JavaRuntimeRepository);
this.instanceTicketManager = init.InstanceTicketManager;
this.shutdownCancellationToken = shutdownCancellationTokenSource.Token;
ReceiveAndReply<ConfigureInstanceCommand, Result<ConfigureInstanceResult, InstanceActionFailure>>(ConfigureInstance);
ReceiveAndReply<LaunchInstanceCommand, Result<LaunchInstanceResult, InstanceActionFailure>>(LaunchInstance);
ReceiveAndReply<StopInstanceCommand, Result<StopInstanceResult, InstanceActionFailure>>(StopInstance);
ReceiveAsyncAndReply<SendCommandToInstanceCommand, Result<SendCommandToInstanceResult, InstanceActionFailure>>(SendCommandToInstance);
ReceiveAsync<ShutdownCommand>(Shutdown);
}
private sealed record Instance(
ActorRef<InstanceActor.ICommand> Actor,
InstanceInfo Info,
InstanceProperties Properties,
InstanceLaunchRecipe? LaunchRecipe,
InstanceStopRecipe StopRecipe,
InstanceBackupConfiguration? BackupConfiguration
);
public interface ICommand;
public sealed record ConfigureInstanceCommand(Guid InstanceGuid, InstanceInfo InstanceInfo, InstanceLaunchRecipe? LaunchRecipe, bool LaunchNow, InstanceStopRecipe StopRecipe, InstanceBackupConfiguration? BackupConfiguration, bool AlwaysReportStatus) : ICommand, ICanReply<Result<ConfigureInstanceResult, InstanceActionFailure>>;
public sealed record LaunchInstanceCommand(Guid InstanceGuid) : ICommand, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>;
public sealed record StopInstanceCommand(Guid InstanceGuid, InstanceStopRecipe StopRecipe) : ICommand, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>;
public sealed record SendCommandToInstanceCommand(Guid InstanceGuid, string Command) : ICommand, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>;
public sealed record ShutdownCommand : ICommand;
private Result<ConfigureInstanceResult, InstanceActionFailure> ConfigureInstance(ConfigureInstanceCommand command) {
var instanceGuid = command.InstanceGuid;
var instanceInfo = command.InstanceInfo;
var launchRecipe = command.LaunchRecipe;
var stopRecipe = command.StopRecipe;
var backupConfiguration = command.BackupConfiguration;
if (instances.TryGetValue(instanceGuid, out var instance)) {
instances[instanceGuid] = instance with {
Info = instanceInfo,
LaunchRecipe = launchRecipe,
StopRecipe = stopRecipe,
BackupConfiguration = backupConfiguration,
};
Logger.Information("Reconfigured instance \"{Name}\" (GUID {Guid}).", instanceInfo.InstanceName, instanceGuid);
if (command.AlwaysReportStatus) {
instance.Actor.Tell(new InstanceActor.ReportInstanceStatusCommand());
}
}
else {
var instanceLoggerName = PhantomLogger.ShortenGuid(instanceGuid) + "/" + Interlocked.Increment(ref instanceLoggerSequenceId);
var instanceDirectoryPath = Path.Combine(agentDirectories.InstancesDirectoryPath, instanceGuid.ToString());
var instanceProperties = new InstanceProperties(instanceGuid, instanceDirectoryPath);
var instanceInit = new InstanceActor.Init(agentState, instanceGuid, instanceLoggerName, instanceServices, instanceTicketManager, shutdownCancellationToken);
var instanceActor = Context.ActorOf(InstanceActor.Factory(instanceInit), "Instance-" + instanceGuid);
instances[instanceGuid] = instance = new Instance(instanceActor, instanceInfo, instanceProperties, launchRecipe, stopRecipe, backupConfiguration);
Logger.Information("Created instance \"{Name}\" (GUID {Guid}).", instanceInfo.InstanceName, instanceGuid);
instance.Actor.Tell(new InstanceActor.ReportInstanceStatusCommand());
}
try {
Directories.Create(instance.Properties.InstanceDirectoryPath, Chmod.URWX_GRX);
} catch (Exception e) {
Logger.Error(e, "Could not create instance directory: {Path}", instance.Properties.InstanceDirectoryPath);
return ConfigureInstanceResult.CouldNotCreateInstanceDirectory;
}
if (command.LaunchNow) {
LaunchInstance(new LaunchInstanceCommand(instanceGuid));
}
return ConfigureInstanceResult.Success;
}
private Result<LaunchInstanceResult, InstanceActionFailure> LaunchInstance(LaunchInstanceCommand command) {
var instanceGuid = command.InstanceGuid;
if (!instances.TryGetValue(instanceGuid, out var instance)) {
return InstanceActionFailure.InstanceDoesNotExist;
}
if (instance.LaunchRecipe is not {} launchRecipe) {
return LaunchInstanceResult.InvalidConfiguration;
}
var ticket = instanceTicketManager.Reserve(instance.Info);
if (!ticket) {
return ticket.Error;
}
if (agentState.InstancesByGuid.TryGetValue(instanceGuid, out var agentInstance)) {
var status = agentInstance.Status;
if (status.IsRunning()) {
return LaunchInstanceResult.InstanceAlreadyRunning;
}
else if (status.IsLaunching()) {
return LaunchInstanceResult.InstanceAlreadyLaunching;
}
}
var pathResolver = new InstancePathResolver(agentDirectories, instanceServices.JavaRuntimeRepository, instance.Properties);
var valueResolver = new InstanceValueResolver(pathResolver);
var launcher = new InstanceLauncher(instanceServices.DownloadManager, pathResolver, valueResolver, instance.Properties, launchRecipe);
instance.Actor.Tell(new InstanceActor.LaunchInstanceCommand(instance.Info, launcher, instance.BackupConfiguration, ticket.Value, IsRestarting: false));
return LaunchInstanceResult.LaunchInitiated;
}
private Result<StopInstanceResult, InstanceActionFailure> StopInstance(StopInstanceCommand command) {
var instanceGuid = command.InstanceGuid;
if (!instances.TryGetValue(instanceGuid, out var instanceInfo)) {
return InstanceActionFailure.InstanceDoesNotExist;
}
if (agentState.InstancesByGuid.TryGetValue(instanceGuid, out var instance)) {
var status = instance.Status;
if (status.IsStopping()) {
return StopInstanceResult.InstanceAlreadyStopping;
}
else if (!status.CanStop()) {
return StopInstanceResult.InstanceAlreadyStopped;
}
}
instanceInfo.Actor.Tell(new InstanceActor.StopInstanceCommand(command.StopRecipe));
return StopInstanceResult.StopInitiated;
}
private async Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> SendCommandToInstance(SendCommandToInstanceCommand command) {
var instanceGuid = command.InstanceGuid;
if (!instances.TryGetValue(instanceGuid, out var instanceInfo)) {
return InstanceActionFailure.InstanceDoesNotExist;
}
try {
return await instanceInfo.Actor.Request(new InstanceActor.SendCommandToInstanceCommand(command.Command), shutdownCancellationToken);
} catch (OperationCanceledException) {
return InstanceActionFailure.AgentShuttingDown;
}
}
private async Task Shutdown(ShutdownCommand command) {
Logger.Information("Stopping all instances...");
await shutdownCancellationTokenSource.CancelAsync();
await Task.WhenAll(instances.Values.Select(static instance => instance.Actor.Stop(new InstanceActor.ShutdownCommand(instance.StopRecipe))));
instances.Clear();
shutdownCancellationTokenSource.Dispose();
Logger.Information("All instances stopped.");
Context.Stop(Self);
}
}