mirror of
				https://github.com/chylex/Minecraft-Phantom-Panel.git
				synced 2025-10-31 11:17:15 +01:00 
			
		
		
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			02828a91c6
			...
			94148add2d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 94148add2d | |||
| c1cbb848a9 | |||
| 9fcc3c53fc | |||
| 72ddaf91ad | 
| @@ -1,5 +1,6 @@ | ||||
| using Phantom.Utils.Collections; | ||||
| using Phantom.Utils.Processes; | ||||
| using Phantom.Utils.Tasks; | ||||
|  | ||||
| namespace Phantom.Agent.Minecraft.Instance; | ||||
|  | ||||
| @@ -13,6 +14,7 @@ public sealed class InstanceProcess : IDisposable { | ||||
| 	public bool HasEnded { get; private set; } | ||||
|  | ||||
| 	private readonly Process process; | ||||
| 	private readonly TaskCompletionSource processExited = AsyncTasks.CreateCompletionSource(); | ||||
|  | ||||
| 	internal InstanceProcess(InstanceProperties instanceProperties, Process process) { | ||||
| 		this.InstanceProperties = instanceProperties; | ||||
| @@ -46,16 +48,15 @@ public sealed class InstanceProcess : IDisposable { | ||||
| 		OutputEvent = null; | ||||
| 		HasEnded = true; | ||||
| 		Ended?.Invoke(this, EventArgs.Empty); | ||||
| 		processExited.SetResult(); | ||||
| 	} | ||||
|  | ||||
| 	public void Kill() { | ||||
| 		process.Kill(true); | ||||
| 	} | ||||
|  | ||||
| 	public async Task WaitForExit(CancellationToken cancellationToken) { | ||||
| 		if (!HasEnded) { | ||||
| 			await process.WaitForExitAsync(cancellationToken); | ||||
| 		} | ||||
| 	public async Task WaitForExit(TimeSpan timeout) { | ||||
| 		await processExited.Task.WaitAsync(timeout); | ||||
| 	} | ||||
|  | ||||
| 	public void Dispose() { | ||||
|   | ||||
| @@ -1,8 +1,10 @@ | ||||
| using Phantom.Agent.Minecraft.Java; | ||||
| using Akka.Actor; | ||||
| using Phantom.Agent.Minecraft.Java; | ||||
| using Phantom.Agent.Rpc; | ||||
| using Phantom.Agent.Services.Backups; | ||||
| using Phantom.Agent.Services.Instances; | ||||
| using Phantom.Common.Data.Agent; | ||||
| using Phantom.Utils.Actor; | ||||
| using Phantom.Utils.Logging; | ||||
| using Phantom.Utils.Tasks; | ||||
| using Serilog; | ||||
| @@ -12,19 +14,30 @@ namespace Phantom.Agent.Services; | ||||
| public sealed class AgentServices { | ||||
| 	private static readonly ILogger Logger = PhantomLogger.Create<AgentServices>(); | ||||
| 	 | ||||
| 	public ActorSystem ActorSystem { get; } | ||||
|  | ||||
| 	private AgentFolders AgentFolders { get; } | ||||
| 	private AgentState AgentState { get; } | ||||
| 	private TaskManager TaskManager { get; } | ||||
| 	private BackupManager BackupManager { get; } | ||||
|  | ||||
| 	internal JavaRuntimeRepository JavaRuntimeRepository { get; } | ||||
| 	internal InstanceSessionManager InstanceSessionManager { get; } | ||||
| 	internal InstanceTicketManager InstanceTicketManager { get; } | ||||
| 	internal ActorRef<InstanceManagerActor.ICommand> InstanceManager { get; } | ||||
|  | ||||
| 	public AgentServices(AgentInfo agentInfo, AgentFolders agentFolders, AgentServiceConfiguration serviceConfiguration, ControllerConnection controllerConnection) { | ||||
| 		this.ActorSystem = ActorSystemFactory.Create("Agent"); | ||||
| 		 | ||||
| 		this.AgentFolders = agentFolders; | ||||
| 		this.AgentState = new AgentState(); | ||||
| 		this.TaskManager = new TaskManager(PhantomLogger.Create<TaskManager, AgentServices>()); | ||||
| 		this.BackupManager = new BackupManager(agentFolders, serviceConfiguration.MaxConcurrentCompressionTasks); | ||||
| 		 | ||||
| 		this.JavaRuntimeRepository = new JavaRuntimeRepository(); | ||||
| 		this.InstanceSessionManager = new InstanceSessionManager(controllerConnection, agentInfo, agentFolders, JavaRuntimeRepository, TaskManager, BackupManager); | ||||
| 		this.InstanceTicketManager = new InstanceTicketManager(agentInfo, controllerConnection); | ||||
| 		 | ||||
| 		var instanceManagerInit = new InstanceManagerActor.Init(controllerConnection, agentFolders, AgentState, JavaRuntimeRepository, InstanceTicketManager, TaskManager, BackupManager); | ||||
| 		this.InstanceManager = ActorSystem.ActorOf(InstanceManagerActor.Factory(instanceManagerInit), "InstanceManager"); | ||||
| 	} | ||||
|  | ||||
| 	public async Task Initialize() { | ||||
| @@ -36,11 +49,14 @@ public sealed class AgentServices { | ||||
| 	public async Task Shutdown() { | ||||
| 		Logger.Information("Stopping services..."); | ||||
| 		 | ||||
| 		await InstanceSessionManager.DisposeAsync(); | ||||
| 		await InstanceManager.Stop(new InstanceManagerActor.ShutdownCommand()); | ||||
| 		await TaskManager.Stop(); | ||||
| 		 | ||||
| 		BackupManager.Dispose(); | ||||
| 		 | ||||
| 		await ActorSystem.Terminate(); | ||||
| 		ActorSystem.Dispose(); | ||||
| 		 | ||||
| 		Logger.Information("Services stopped."); | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										15
									
								
								Agent/Phantom.Agent.Services/AgentState.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								Agent/Phantom.Agent.Services/AgentState.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Phantom.Agent.Services.Instances; | ||||
| using Phantom.Utils.Actor.Event; | ||||
|  | ||||
| namespace Phantom.Agent.Services; | ||||
|  | ||||
| sealed class AgentState { | ||||
| 	private readonly ObservableState<ImmutableDictionary<Guid, Instance>> instancesByGuid = new (ImmutableDictionary<Guid, Instance>.Empty); | ||||
| 	 | ||||
| 	public ImmutableDictionary<Guid, Instance> InstancesByGuid => instancesByGuid.State; | ||||
| 	 | ||||
| 	public void UpdateInstance(Instance instance) { | ||||
| 		instancesByGuid.PublisherSide.Publish(static (instancesByGuid, instance) => instancesByGuid.SetItem(instance.InstanceGuid, instance), instance); | ||||
| 	} | ||||
| } | ||||
| @@ -1,7 +1,6 @@ | ||||
| using Phantom.Agent.Minecraft.Instance; | ||||
| using Phantom.Agent.Minecraft.Server; | ||||
| using Phantom.Agent.Services.Instances; | ||||
| using Phantom.Agent.Services.Instances.Procedures; | ||||
| using Phantom.Common.Data.Backups; | ||||
| using Phantom.Utils.Logging; | ||||
| using Phantom.Utils.Tasks; | ||||
| @@ -16,8 +15,8 @@ sealed class BackupScheduler : CancellableBackgroundTask { | ||||
| 	private static readonly TimeSpan BackupFailureRetryDelay = TimeSpan.FromMinutes(5); | ||||
|  | ||||
| 	private readonly BackupManager backupManager; | ||||
| 	private readonly InstanceContext context; | ||||
| 	private readonly InstanceProcess process; | ||||
| 	private readonly IInstanceContext context; | ||||
| 	private readonly SemaphoreSlim backupSemaphore = new (1, 1); | ||||
| 	private readonly int serverPort; | ||||
| 	private readonly ServerStatusProtocol serverStatusProtocol; | ||||
| @@ -25,10 +24,10 @@ sealed class BackupScheduler : CancellableBackgroundTask { | ||||
| 	 | ||||
| 	public event EventHandler<BackupCreationResult>? BackupCompleted; | ||||
|  | ||||
| 	public BackupScheduler(TaskManager taskManager, BackupManager backupManager, InstanceProcess process, IInstanceContext context, int serverPort) : base(PhantomLogger.Create<BackupScheduler>(context.ShortName), taskManager, "Backup scheduler for " + context.ShortName) { | ||||
| 		this.backupManager = backupManager; | ||||
| 		this.process = process; | ||||
| 	public BackupScheduler(InstanceContext context, InstanceProcess process, int serverPort) : base(PhantomLogger.Create<BackupScheduler>(context.ShortName), context.Services.TaskManager, "Backup scheduler for " + context.ShortName) { | ||||
| 		this.backupManager = context.Services.BackupManager; | ||||
| 		this.context = context; | ||||
| 		this.process = process; | ||||
| 		this.serverPort = serverPort; | ||||
| 		this.serverStatusProtocol = new ServerStatusProtocol(context.ShortName); | ||||
| 		Start(); | ||||
| @@ -60,9 +59,10 @@ sealed class BackupScheduler : CancellableBackgroundTask { | ||||
| 		} | ||||
| 		 | ||||
| 		try { | ||||
| 			var procedure = new BackupInstanceProcedure(backupManager); | ||||
| 			context.EnqueueProcedure(procedure); | ||||
| 			return await procedure.Result; | ||||
| 			context.ActorCancellationToken.ThrowIfCancellationRequested(); | ||||
| 			return await context.Actor.Request(new InstanceActor.BackupInstanceCommand(backupManager), context.ActorCancellationToken); | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			return new BackupCreationResult(BackupCreationResultKind.InstanceNotRunning); | ||||
| 		} finally { | ||||
| 			backupSemaphore.Release(); | ||||
| 		} | ||||
|   | ||||
| @@ -1,25 +0,0 @@ | ||||
| using Phantom.Agent.Services.Instances.Procedures; | ||||
| using Phantom.Agent.Services.Instances.States; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Serilog; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances; | ||||
|  | ||||
| interface IInstanceContext { | ||||
| 	string ShortName { get; } | ||||
| 	ILogger Logger { get; } | ||||
| 	 | ||||
| 	InstanceServices Services { get; } | ||||
| 	IInstanceState CurrentState { get; } | ||||
|  | ||||
| 	void SetStatus(IInstanceStatus newStatus); | ||||
| 	void ReportEvent(IInstanceEvent instanceEvent); | ||||
| 	void EnqueueProcedure(IInstanceProcedure procedure, bool immediate = false); | ||||
| } | ||||
|  | ||||
| static class InstanceContextExtensions { | ||||
| 	public static void SetLaunchFailedStatusAndReportEvent(this IInstanceContext context, InstanceLaunchFailReason reason) { | ||||
| 		context.SetStatus(InstanceStatus.Failed(reason)); | ||||
| 		context.ReportEvent(new InstanceLaunchFailedEvent(reason)); | ||||
| 	} | ||||
| } | ||||
| @@ -1,171 +1,5 @@ | ||||
| using Phantom.Agent.Minecraft.Launcher; | ||||
| using Phantom.Agent.Services.Instances.Procedures; | ||||
| using Phantom.Agent.Services.Instances.States; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Common.Data.Minecraft; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Messages.Agent.ToController; | ||||
| using Phantom.Utils.Logging; | ||||
| using Serilog; | ||||
| using Phantom.Common.Data.Instance; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances; | ||||
|  | ||||
| sealed class Instance : IAsyncDisposable { | ||||
| 	private InstanceServices Services { get; } | ||||
| 	 | ||||
| 	public InstanceConfiguration Configuration { get; private set; } | ||||
| 	private IServerLauncher Launcher { get; set; } | ||||
| 	private readonly SemaphoreSlim configurationSemaphore = new (1, 1); | ||||
|  | ||||
| 	private readonly Guid instanceGuid; | ||||
| 	private readonly string shortName; | ||||
| 	private readonly ILogger logger; | ||||
|  | ||||
| 	private IInstanceStatus currentStatus; | ||||
|  | ||||
| 	private IInstanceState currentState; | ||||
| 	public bool IsRunning => currentState is not InstanceNotRunningState; | ||||
| 	 | ||||
| 	public event EventHandler? IsRunningChanged; | ||||
| 	 | ||||
| 	private readonly InstanceProcedureManager procedureManager; | ||||
|  | ||||
| 	public Instance(Guid instanceGuid, string shortName, InstanceServices services, InstanceConfiguration configuration, IServerLauncher launcher) { | ||||
| 		this.instanceGuid = instanceGuid; | ||||
| 		this.shortName = shortName; | ||||
| 		this.logger = PhantomLogger.Create<Instance>(shortName); | ||||
|  | ||||
| 		this.Services = services; | ||||
| 		this.Configuration = configuration; | ||||
| 		this.Launcher = launcher; | ||||
| 		 | ||||
| 		this.currentStatus = InstanceStatus.NotRunning; | ||||
| 		this.currentState = new InstanceNotRunningState(); | ||||
| 		 | ||||
| 		this.procedureManager = new InstanceProcedureManager(this, new Context(this), services.TaskManager); | ||||
| 	} | ||||
|  | ||||
| 	public void ReportLastStatus() { | ||||
| 		Services.ControllerConnection.Send(new ReportInstanceStatusMessage(instanceGuid, currentStatus)); | ||||
| 	} | ||||
|  | ||||
| 	private void ReportAndSetStatus(IInstanceStatus status) { | ||||
| 		currentStatus = status; | ||||
| 		Services.ControllerConnection.Send(new ReportInstanceStatusMessage(instanceGuid, status)); | ||||
| 	} | ||||
|  | ||||
| 	private void ReportEvent(IInstanceEvent instanceEvent) { | ||||
| 		Services.ControllerConnection.Send(new ReportInstanceEventMessage(Guid.NewGuid(), DateTime.UtcNow, instanceGuid, instanceEvent)); | ||||
| 	} | ||||
| 	 | ||||
| 	internal void TransitionState(IInstanceState newState) { | ||||
| 		if (currentState == newState) { | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		if (currentState is IDisposable disposable) { | ||||
| 			disposable.Dispose(); | ||||
| 		} | ||||
|  | ||||
| 		logger.Debug("Transitioning instance state to: {NewState}", newState.GetType().Name); | ||||
| 		 | ||||
| 		var wasRunning = IsRunning; | ||||
| 		currentState = newState; | ||||
| 		currentState.Initialize(); | ||||
|  | ||||
| 		if (IsRunning != wasRunning) { | ||||
| 			IsRunningChanged?.Invoke(this, EventArgs.Empty); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public async Task Reconfigure(InstanceConfiguration configuration, IServerLauncher launcher, CancellationToken cancellationToken) { | ||||
| 		await configurationSemaphore.WaitAsync(cancellationToken); | ||||
| 		try { | ||||
| 			Configuration = configuration; | ||||
| 			Launcher = launcher; | ||||
| 		} finally { | ||||
| 			configurationSemaphore.Release(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public async Task<LaunchInstanceResult> Launch(CancellationToken cancellationToken) { | ||||
| 		if (IsRunning) { | ||||
| 			return LaunchInstanceResult.InstanceAlreadyRunning; | ||||
| 		} | ||||
|  | ||||
| 		if (await procedureManager.GetCurrentProcedure(cancellationToken) is LaunchInstanceProcedure) { | ||||
| 			return LaunchInstanceResult.InstanceAlreadyLaunching; | ||||
| 		} | ||||
| 		 | ||||
| 		LaunchInstanceProcedure procedure; | ||||
| 		 | ||||
| 		await configurationSemaphore.WaitAsync(cancellationToken); | ||||
| 		try { | ||||
| 			procedure = new LaunchInstanceProcedure(instanceGuid, Configuration, Launcher); | ||||
| 		} finally { | ||||
| 			configurationSemaphore.Release(); | ||||
| 		} | ||||
| 		 | ||||
| 		ReportAndSetStatus(InstanceStatus.Launching); | ||||
| 		await procedureManager.Enqueue(procedure); | ||||
| 		return LaunchInstanceResult.LaunchInitiated; | ||||
| 	} | ||||
|  | ||||
| 	public async Task<StopInstanceResult> Stop(MinecraftStopStrategy stopStrategy, CancellationToken cancellationToken) { | ||||
| 		if (!IsRunning) { | ||||
| 			return StopInstanceResult.InstanceAlreadyStopped; | ||||
| 		} | ||||
| 		 | ||||
| 		if (await procedureManager.GetCurrentProcedure(cancellationToken) is StopInstanceProcedure) { | ||||
| 			return StopInstanceResult.InstanceAlreadyStopping; | ||||
| 		} | ||||
| 		 | ||||
| 		ReportAndSetStatus(InstanceStatus.Stopping); | ||||
| 		await procedureManager.Enqueue(new StopInstanceProcedure(stopStrategy)); | ||||
| 		return StopInstanceResult.StopInitiated; | ||||
| 	} | ||||
|  | ||||
| 	public async Task<bool> SendCommand(string command, CancellationToken cancellationToken) { | ||||
| 		return await currentState.SendCommand(command, cancellationToken); | ||||
| 	} | ||||
|  | ||||
| 	public async ValueTask DisposeAsync() { | ||||
| 		await procedureManager.DisposeAsync(); | ||||
|  | ||||
| 		while (currentState is not InstanceNotRunningState) { | ||||
| 			await Task.Delay(TimeSpan.FromMilliseconds(250), CancellationToken.None); | ||||
| 		} | ||||
|  | ||||
| 		if (currentState is IDisposable disposable) { | ||||
| 			disposable.Dispose(); | ||||
| 		} | ||||
| 		 | ||||
| 		configurationSemaphore.Dispose(); | ||||
| 	} | ||||
|  | ||||
| 	private sealed class Context : IInstanceContext { | ||||
| 		public string ShortName => instance.shortName; | ||||
| 		public ILogger Logger => instance.logger; | ||||
| 		 | ||||
| 		public InstanceServices Services => instance.Services; | ||||
| 		public IInstanceState CurrentState => instance.currentState; | ||||
|  | ||||
| 		private readonly Instance instance; | ||||
| 		 | ||||
| 		public Context(Instance instance) { | ||||
| 			this.instance = instance; | ||||
| 		} | ||||
|  | ||||
| 		public void SetStatus(IInstanceStatus newStatus) { | ||||
| 			instance.ReportAndSetStatus(newStatus); | ||||
| 		} | ||||
|  | ||||
| 		public void ReportEvent(IInstanceEvent instanceEvent) { | ||||
| 			instance.ReportEvent(instanceEvent); | ||||
| 		} | ||||
|  | ||||
| 		public void EnqueueProcedure(IInstanceProcedure procedure, bool immediate) { | ||||
| 			Services.TaskManager.Run("Enqueue procedure for instance " + instance.shortName, () => instance.procedureManager.Enqueue(procedure, immediate)); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| sealed record Instance(Guid InstanceGuid, IInstanceStatus Status); | ||||
|   | ||||
							
								
								
									
										156
									
								
								Agent/Phantom.Agent.Services/Instances/InstanceActor.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								Agent/Phantom.Agent.Services/Instances/InstanceActor.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,156 @@ | ||||
| using Phantom.Agent.Minecraft.Launcher; | ||||
| using Phantom.Agent.Services.Backups; | ||||
| using Phantom.Agent.Services.Instances.State; | ||||
| using Phantom.Common.Data.Backups; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Common.Data.Minecraft; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Messages.Agent.ToController; | ||||
| using Phantom.Utils.Actor; | ||||
| using Phantom.Utils.Actor.Mailbox; | ||||
| using Phantom.Utils.Logging; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances; | ||||
|  | ||||
| sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> { | ||||
| 	public readonly record struct Init(AgentState AgentState, Guid InstanceGuid, string ShortName, InstanceServices InstanceServices, InstanceTicketManager InstanceTicketManager, CancellationToken ShutdownCancellationToken); | ||||
|  | ||||
| 	public static Props<ICommand> Factory(Init init) { | ||||
| 		return Props<ICommand>.Create(() => new InstanceActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume, MailboxType = UnboundedJumpAheadMailbox.Name }); | ||||
| 	} | ||||
|  | ||||
| 	private readonly AgentState agentState; | ||||
| 	private readonly CancellationToken shutdownCancellationToken; | ||||
| 	 | ||||
| 	private readonly Guid instanceGuid; | ||||
| 	private readonly InstanceServices instanceServices; | ||||
| 	private readonly InstanceTicketManager instanceTicketManager; | ||||
| 	private readonly InstanceContext context; | ||||
| 	 | ||||
| 	private readonly CancellationTokenSource actorCancellationTokenSource = new (); | ||||
|  | ||||
| 	private IInstanceStatus currentStatus = InstanceStatus.NotRunning; | ||||
| 	private InstanceRunningState? runningState = null; | ||||
|  | ||||
| 	private InstanceActor(Init init) { | ||||
| 		this.agentState = init.AgentState; | ||||
| 		this.instanceGuid = init.InstanceGuid; | ||||
| 		this.instanceServices = init.InstanceServices; | ||||
| 		this.instanceTicketManager = init.InstanceTicketManager; | ||||
| 		this.shutdownCancellationToken = init.ShutdownCancellationToken; | ||||
| 		 | ||||
| 		var logger = PhantomLogger.Create<InstanceActor>(init.ShortName); | ||||
| 		this.context = new InstanceContext(instanceGuid, init.ShortName, logger, instanceServices, SelfTyped, actorCancellationTokenSource.Token); | ||||
|  | ||||
| 		Receive<ReportInstanceStatusCommand>(ReportInstanceStatus); | ||||
| 		ReceiveAsync<LaunchInstanceCommand>(LaunchInstance); | ||||
| 		ReceiveAsync<StopInstanceCommand>(StopInstance); | ||||
| 		ReceiveAsyncAndReply<SendCommandToInstanceCommand, SendCommandToInstanceResult>(SendCommandToInstance); | ||||
| 		ReceiveAsyncAndReply<BackupInstanceCommand, BackupCreationResult>(BackupInstance); | ||||
| 		Receive<HandleProcessEndedCommand>(HandleProcessEnded); | ||||
| 		ReceiveAsync<ShutdownCommand>(Shutdown); | ||||
| 	} | ||||
|  | ||||
| 	private void SetAndReportStatus(IInstanceStatus status) { | ||||
| 		currentStatus = status; | ||||
| 		ReportCurrentStatus(); | ||||
| 	} | ||||
|  | ||||
| 	private void ReportCurrentStatus() { | ||||
| 		agentState.UpdateInstance(new Instance(instanceGuid, currentStatus)); | ||||
| 		instanceServices.ControllerConnection.Send(new ReportInstanceStatusMessage(instanceGuid, currentStatus)); | ||||
| 	} | ||||
|  | ||||
| 	private void TransitionState(InstanceRunningState? newState) { | ||||
| 		if (runningState == newState) { | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		runningState?.Dispose(); | ||||
| 		runningState = newState; | ||||
| 		runningState?.Initialize(); | ||||
| 	} | ||||
| 	 | ||||
| 	public interface ICommand {} | ||||
|  | ||||
| 	public sealed record ReportInstanceStatusCommand : ICommand; | ||||
| 	 | ||||
| 	public sealed record LaunchInstanceCommand(InstanceConfiguration Configuration, IServerLauncher Launcher, InstanceTicketManager.Ticket Ticket, bool IsRestarting) : ICommand; | ||||
| 	 | ||||
| 	public sealed record StopInstanceCommand(MinecraftStopStrategy StopStrategy) : ICommand; | ||||
| 	 | ||||
| 	public sealed record SendCommandToInstanceCommand(string Command) : ICommand, ICanReply<SendCommandToInstanceResult>; | ||||
| 	 | ||||
| 	public sealed record BackupInstanceCommand(BackupManager BackupManager) : ICommand, ICanReply<BackupCreationResult>; | ||||
| 	 | ||||
| 	public sealed record HandleProcessEndedCommand(IInstanceStatus Status) : ICommand, IJumpAhead; | ||||
| 	 | ||||
| 	public sealed record ShutdownCommand : ICommand; | ||||
|  | ||||
| 	private void ReportInstanceStatus(ReportInstanceStatusCommand command) { | ||||
| 		ReportCurrentStatus(); | ||||
| 	} | ||||
| 	 | ||||
| 	private async Task LaunchInstance(LaunchInstanceCommand command) { | ||||
| 		if (command.IsRestarting || runningState is null) { | ||||
| 			SetAndReportStatus(command.IsRestarting ? InstanceStatus.Restarting : InstanceStatus.Launching); | ||||
| 			 | ||||
| 			var newState = await InstanceLaunchProcedure.Run(context, command.Configuration, command.Launcher, instanceTicketManager, command.Ticket, SetAndReportStatus, shutdownCancellationToken); | ||||
| 			if (newState is null) { | ||||
| 				instanceTicketManager.Release(command.Ticket); | ||||
| 			} | ||||
| 			 | ||||
| 			TransitionState(newState); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private async Task StopInstance(StopInstanceCommand command) { | ||||
| 		if (runningState is null) { | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		IInstanceStatus oldStatus = currentStatus; | ||||
| 		SetAndReportStatus(InstanceStatus.Stopping); | ||||
| 		 | ||||
| 		if (await InstanceStopProcedure.Run(context, command.StopStrategy, runningState, SetAndReportStatus, shutdownCancellationToken)) { | ||||
| 			instanceTicketManager.Release(runningState.Ticket); | ||||
| 			TransitionState(null); | ||||
| 		} | ||||
| 		else { | ||||
| 			SetAndReportStatus(oldStatus); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private async Task<SendCommandToInstanceResult> SendCommandToInstance(SendCommandToInstanceCommand command) { | ||||
| 		if (runningState is null) { | ||||
| 			return SendCommandToInstanceResult.InstanceNotRunning; | ||||
| 		} | ||||
| 		else { | ||||
| 			return await runningState.SendCommand(command.Command, shutdownCancellationToken); | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	private async Task<BackupCreationResult> BackupInstance(BackupInstanceCommand command) { | ||||
| 		if (runningState is null || runningState.Process.HasEnded) { | ||||
| 			return new BackupCreationResult(BackupCreationResultKind.InstanceNotRunning); | ||||
| 		} | ||||
| 		else { | ||||
| 			return await command.BackupManager.CreateBackup(context.ShortName, runningState.Process, shutdownCancellationToken); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private void HandleProcessEnded(HandleProcessEndedCommand command) { | ||||
| 		if (runningState is { Process.HasEnded: true }) { | ||||
| 			SetAndReportStatus(command.Status); | ||||
| 			context.ReportEvent(InstanceEvent.Stopped); | ||||
| 			instanceTicketManager.Release(runningState.Ticket); | ||||
| 			TransitionState(null); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private async Task Shutdown(ShutdownCommand command) { | ||||
| 		await StopInstance(new StopInstanceCommand(MinecraftStopStrategy.Instant)); | ||||
| 		await actorCancellationTokenSource.CancelAsync(); | ||||
| 		Context.Stop(Self); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										12
									
								
								Agent/Phantom.Agent.Services/Instances/InstanceContext.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								Agent/Phantom.Agent.Services/Instances/InstanceContext.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Common.Messages.Agent.ToController; | ||||
| using Phantom.Utils.Actor; | ||||
| using Serilog; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances; | ||||
|  | ||||
| sealed record InstanceContext(Guid InstanceGuid, string ShortName, ILogger Logger, InstanceServices Services, ActorRef<InstanceActor.ICommand> Actor, CancellationToken ActorCancellationToken) { | ||||
| 	public void ReportEvent(IInstanceEvent instanceEvent) { | ||||
| 		Services.ControllerConnection.Send(new ReportInstanceEventMessage(Guid.NewGuid(), DateTime.UtcNow, InstanceGuid, instanceEvent)); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										208
									
								
								Agent/Phantom.Agent.Services/Instances/InstanceManagerActor.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										208
									
								
								Agent/Phantom.Agent.Services/Instances/InstanceManagerActor.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,208 @@ | ||||
| using Phantom.Agent.Minecraft.Instance; | ||||
| using Phantom.Agent.Minecraft.Java; | ||||
| using Phantom.Agent.Minecraft.Launcher; | ||||
| using Phantom.Agent.Minecraft.Launcher.Types; | ||||
| using Phantom.Agent.Minecraft.Properties; | ||||
| using Phantom.Agent.Minecraft.Server; | ||||
| using Phantom.Agent.Rpc; | ||||
| using Phantom.Agent.Services.Backups; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Common.Data.Minecraft; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Utils.Actor; | ||||
| using Phantom.Utils.IO; | ||||
| using Phantom.Utils.Logging; | ||||
| using Phantom.Utils.Tasks; | ||||
| 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, AgentFolders AgentFolders, AgentState AgentState, JavaRuntimeRepository JavaRuntimeRepository, InstanceTicketManager InstanceTicketManager, TaskManager TaskManager, 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 string basePath; | ||||
|  | ||||
| 	private readonly InstanceServices instanceServices; | ||||
| 	private readonly InstanceTicketManager instanceTicketManager; | ||||
| 	private readonly Dictionary<Guid, InstanceInfo> instances = new (); | ||||
|  | ||||
| 	private readonly CancellationTokenSource shutdownCancellationTokenSource = new (); | ||||
| 	private readonly CancellationToken shutdownCancellationToken; | ||||
|  | ||||
| 	private uint instanceLoggerSequenceId = 0; | ||||
|  | ||||
| 	private InstanceManagerActor(Init init) { | ||||
| 		this.agentState = init.AgentState; | ||||
| 		this.basePath = init.AgentFolders.InstancesFolderPath; | ||||
| 		this.instanceTicketManager = init.InstanceTicketManager; | ||||
| 		this.shutdownCancellationToken = shutdownCancellationTokenSource.Token; | ||||
|  | ||||
| 		var minecraftServerExecutables = new MinecraftServerExecutables(init.AgentFolders.ServerExecutableFolderPath); | ||||
| 		var launchServices = new LaunchServices(minecraftServerExecutables, init.JavaRuntimeRepository); | ||||
|  | ||||
| 		this.instanceServices = new InstanceServices(init.ControllerConnection, init.TaskManager, init.BackupManager, launchServices); | ||||
| 		 | ||||
| 		ReceiveAndReply<ConfigureInstanceCommand, InstanceActionResult<ConfigureInstanceResult>>(ConfigureInstance); | ||||
| 		ReceiveAndReply<LaunchInstanceCommand, InstanceActionResult<LaunchInstanceResult>>(LaunchInstance); | ||||
| 		ReceiveAndReply<StopInstanceCommand, InstanceActionResult<StopInstanceResult>>(StopInstance); | ||||
| 		ReceiveAsyncAndReply<SendCommandToInstanceCommand, InstanceActionResult<SendCommandToInstanceResult>>(SendCommandToInstance); | ||||
| 		ReceiveAsync<ShutdownCommand>(Shutdown); | ||||
| 	} | ||||
|  | ||||
| 	private string GetInstanceLoggerName(Guid guid) { | ||||
| 		var prefix = guid.ToString(); | ||||
| 		return prefix[..prefix.IndexOf('-')] + "/" + Interlocked.Increment(ref instanceLoggerSequenceId); | ||||
| 	} | ||||
|  | ||||
| 	private sealed record InstanceInfo(ActorRef<InstanceActor.ICommand> Actor, InstanceConfiguration Configuration, IServerLauncher Launcher); | ||||
| 	 | ||||
| 	public interface ICommand {} | ||||
| 	 | ||||
| 	public sealed record ConfigureInstanceCommand(Guid InstanceGuid, InstanceConfiguration Configuration, InstanceLaunchProperties LaunchProperties, bool LaunchNow, bool AlwaysReportStatus) : ICommand, ICanReply<InstanceActionResult<ConfigureInstanceResult>>; | ||||
| 	 | ||||
| 	public sealed record LaunchInstanceCommand(Guid InstanceGuid) : ICommand, ICanReply<InstanceActionResult<LaunchInstanceResult>>; | ||||
| 	 | ||||
| 	public sealed record StopInstanceCommand(Guid InstanceGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<InstanceActionResult<StopInstanceResult>>; | ||||
| 	 | ||||
| 	public sealed record SendCommandToInstanceCommand(Guid InstanceGuid, string Command) : ICommand, ICanReply<InstanceActionResult<SendCommandToInstanceResult>>; | ||||
| 	 | ||||
| 	public sealed record ShutdownCommand : ICommand; | ||||
|  | ||||
| 	private InstanceActionResult<ConfigureInstanceResult> ConfigureInstance(ConfigureInstanceCommand command) { | ||||
| 		var instanceGuid = command.InstanceGuid; | ||||
| 		var configuration = command.Configuration; | ||||
|  | ||||
| 		var instanceFolder = Path.Combine(basePath, instanceGuid.ToString()); | ||||
| 		Directories.Create(instanceFolder, Chmod.URWX_GRX); | ||||
|  | ||||
| 		var heapMegabytes = configuration.MemoryAllocation.InMegabytes; | ||||
| 		var jvmProperties = new JvmProperties( | ||||
| 			InitialHeapMegabytes: heapMegabytes / 2, | ||||
| 			MaximumHeapMegabytes: heapMegabytes | ||||
| 		); | ||||
|  | ||||
| 		var properties = new InstanceProperties( | ||||
| 			instanceGuid, | ||||
| 			configuration.JavaRuntimeGuid, | ||||
| 			jvmProperties, | ||||
| 			configuration.JvmArguments, | ||||
| 			instanceFolder, | ||||
| 			configuration.MinecraftVersion, | ||||
| 			new ServerProperties(configuration.ServerPort, configuration.RconPort), | ||||
| 			command.LaunchProperties | ||||
| 		); | ||||
|  | ||||
| 		IServerLauncher launcher = configuration.MinecraftServerKind switch { | ||||
| 			MinecraftServerKind.Vanilla => new VanillaLauncher(properties), | ||||
| 			MinecraftServerKind.Fabric  => new FabricLauncher(properties), | ||||
| 			_                           => InvalidLauncher.Instance | ||||
| 		}; | ||||
|  | ||||
| 		if (instances.TryGetValue(instanceGuid, out var instance)) { | ||||
| 			instances[instanceGuid] = instance with { | ||||
| 				Configuration = configuration, | ||||
| 				Launcher = launcher | ||||
| 			}; | ||||
| 				 | ||||
| 			Logger.Information("Reconfigured instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, instanceGuid); | ||||
|  | ||||
| 			if (command.AlwaysReportStatus) { | ||||
| 				instance.Actor.Tell(new InstanceActor.ReportInstanceStatusCommand()); | ||||
| 			} | ||||
| 		} | ||||
| 		else { | ||||
| 			var instanceInit = new InstanceActor.Init(agentState, instanceGuid, GetInstanceLoggerName(instanceGuid), instanceServices, instanceTicketManager, shutdownCancellationToken); | ||||
| 			instances[instanceGuid] = instance = new InstanceInfo(Context.ActorOf(InstanceActor.Factory(instanceInit), "Instance-" + instanceGuid), configuration, launcher); | ||||
| 				 | ||||
| 			Logger.Information("Created instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, instanceGuid); | ||||
|  | ||||
| 			instance.Actor.Tell(new InstanceActor.ReportInstanceStatusCommand()); | ||||
| 		} | ||||
|  | ||||
| 		if (command.LaunchNow) { | ||||
| 			LaunchInstance(new LaunchInstanceCommand(instanceGuid)); | ||||
| 		} | ||||
|  | ||||
| 		return InstanceActionResult.Concrete(ConfigureInstanceResult.Success); | ||||
| 	} | ||||
|  | ||||
| 	private InstanceActionResult<LaunchInstanceResult> LaunchInstance(LaunchInstanceCommand command) { | ||||
| 		var instanceGuid = command.InstanceGuid; | ||||
| 		if (!instances.TryGetValue(instanceGuid, out var instanceInfo)) { | ||||
| 			return InstanceActionResult.General<LaunchInstanceResult>(InstanceActionGeneralResult.InstanceDoesNotExist); | ||||
| 		} | ||||
| 		 | ||||
| 		var ticket = instanceTicketManager.Reserve(instanceInfo.Configuration); | ||||
| 		if (ticket is Result<InstanceTicketManager.Ticket, LaunchInstanceResult>.Fail fail) { | ||||
| 			return InstanceActionResult.Concrete(fail.Error); | ||||
| 		} | ||||
|  | ||||
| 		if (agentState.InstancesByGuid.TryGetValue(instanceGuid, out var instance)) { | ||||
| 			var status = instance.Status; | ||||
| 			if (status.IsRunning()) { | ||||
| 				return InstanceActionResult.Concrete(LaunchInstanceResult.InstanceAlreadyRunning); | ||||
| 			} | ||||
| 			else if (status.IsLaunching()) { | ||||
| 				return InstanceActionResult.Concrete(LaunchInstanceResult.InstanceAlreadyLaunching); | ||||
| 			} | ||||
| 		} | ||||
| 		 | ||||
| 		instanceInfo.Actor.Tell(new InstanceActor.LaunchInstanceCommand(instanceInfo.Configuration, instanceInfo.Launcher, ticket.Value, IsRestarting: false)); | ||||
| 		return InstanceActionResult.Concrete(LaunchInstanceResult.LaunchInitiated); | ||||
| 	} | ||||
|  | ||||
| 	private InstanceActionResult<StopInstanceResult> StopInstance(StopInstanceCommand command) { | ||||
| 		var instanceGuid = command.InstanceGuid; | ||||
| 		if (!instances.TryGetValue(instanceGuid, out var instanceInfo)) { | ||||
| 			return InstanceActionResult.General<StopInstanceResult>(InstanceActionGeneralResult.InstanceDoesNotExist); | ||||
| 		} | ||||
|          | ||||
| 		if (agentState.InstancesByGuid.TryGetValue(instanceGuid, out var instance)) { | ||||
| 			var status = instance.Status; | ||||
| 			if (status.IsStopping()) { | ||||
| 				return InstanceActionResult.Concrete(StopInstanceResult.InstanceAlreadyStopping); | ||||
| 			} | ||||
| 			else if (!status.CanStop()) { | ||||
| 				return InstanceActionResult.Concrete(StopInstanceResult.InstanceAlreadyStopped); | ||||
| 			} | ||||
| 		} | ||||
| 			 | ||||
| 		instanceInfo.Actor.Tell(new InstanceActor.StopInstanceCommand(command.StopStrategy)); | ||||
| 		return InstanceActionResult.Concrete(StopInstanceResult.StopInitiated); | ||||
| 	} | ||||
|  | ||||
| 	private async Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommandToInstance(SendCommandToInstanceCommand command) { | ||||
| 		var instanceGuid = command.InstanceGuid; | ||||
| 		if (!instances.TryGetValue(instanceGuid, out var instanceInfo)) { | ||||
| 			return InstanceActionResult.General<SendCommandToInstanceResult>(InstanceActionGeneralResult.InstanceDoesNotExist); | ||||
| 		} | ||||
|  | ||||
| 		try { | ||||
| 			return InstanceActionResult.Concrete(await instanceInfo.Actor.Request(new InstanceActor.SendCommandToInstanceCommand(command.Command), shutdownCancellationToken)); | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			return InstanceActionResult.General<SendCommandToInstanceResult>(InstanceActionGeneralResult.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()))); | ||||
| 		instances.Clear(); | ||||
| 		 | ||||
| 		shutdownCancellationTokenSource.Dispose(); | ||||
| 		 | ||||
| 		Logger.Information("All instances stopped."); | ||||
| 		 | ||||
| 		Context.Stop(Self); | ||||
| 	} | ||||
| } | ||||
| @@ -1,85 +0,0 @@ | ||||
| using Phantom.Agent.Services.Instances.Procedures; | ||||
| using Phantom.Common.Data.Minecraft; | ||||
| using Phantom.Utils.Tasks; | ||||
| using Phantom.Utils.Threading; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances; | ||||
|  | ||||
| sealed class InstanceProcedureManager : IAsyncDisposable { | ||||
| 	private readonly record struct CurrentProcedure(IInstanceProcedure Procedure, CancellationTokenSource CancellationTokenSource); | ||||
|  | ||||
| 	private readonly ThreadSafeStructRef<CurrentProcedure> currentProcedure = new (); | ||||
| 	private readonly ThreadSafeLinkedList<IInstanceProcedure> procedureQueue = new (); | ||||
| 	private readonly AutoResetEvent procedureQueueReady = new (false); | ||||
| 	private readonly ManualResetEventSlim procedureQueueFinished = new (false); | ||||
|  | ||||
| 	private readonly Instance instance; | ||||
| 	private readonly IInstanceContext context; | ||||
| 	private readonly CancellationTokenSource shutdownCancellationTokenSource = new (); | ||||
|  | ||||
| 	public InstanceProcedureManager(Instance instance, IInstanceContext context, TaskManager taskManager) { | ||||
| 		this.instance = instance; | ||||
| 		this.context = context; | ||||
| 		taskManager.Run("Procedure manager for instance " + context.ShortName, Run); | ||||
| 	} | ||||
|  | ||||
| 	public async Task Enqueue(IInstanceProcedure procedure, bool immediate = false) { | ||||
| 		await procedureQueue.Add(procedure, toFront: immediate, shutdownCancellationTokenSource.Token); | ||||
| 		procedureQueueReady.Set(); | ||||
| 	} | ||||
|  | ||||
| 	public async Task<IInstanceProcedure?> GetCurrentProcedure(CancellationToken cancellationToken) { | ||||
| 		return (await currentProcedure.Get(cancellationToken))?.Procedure; | ||||
| 	} | ||||
|  | ||||
| 	private async Task Run() { | ||||
| 		try { | ||||
| 			var shutdownCancellationToken = shutdownCancellationTokenSource.Token; | ||||
| 			while (true) { | ||||
| 				await procedureQueueReady.WaitOneAsync(shutdownCancellationToken); | ||||
| 				while (await procedureQueue.TryTakeFromFront(shutdownCancellationToken) is {} nextProcedure) { | ||||
| 					using var procedureCancellationTokenSource = new CancellationTokenSource(); | ||||
| 					await currentProcedure.Set(new CurrentProcedure(nextProcedure, procedureCancellationTokenSource), shutdownCancellationToken); | ||||
| 					await RunProcedure(nextProcedure, procedureCancellationTokenSource.Token); | ||||
| 					await currentProcedure.Set(null, shutdownCancellationToken); | ||||
| 				} | ||||
| 			} | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			// Ignore. | ||||
| 		} | ||||
|  | ||||
| 		await RunProcedure(new StopInstanceProcedure(MinecraftStopStrategy.Instant), CancellationToken.None); | ||||
| 		procedureQueueFinished.Set(); | ||||
| 	} | ||||
|  | ||||
| 	private async Task RunProcedure(IInstanceProcedure procedure, CancellationToken cancellationToken) { | ||||
| 		var procedureName = procedure.GetType().Name; | ||||
|  | ||||
| 		context.Logger.Debug("Started procedure: {Procedure}", procedureName); | ||||
| 		try { | ||||
| 			var newState = await procedure.Run(context, cancellationToken); | ||||
| 			context.Logger.Debug("Finished procedure: {Procedure}", procedureName); | ||||
|  | ||||
| 			if (newState != null) { | ||||
| 				instance.TransitionState(newState); | ||||
| 			} | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			context.Logger.Debug("Cancelled procedure: {Procedure}", procedureName); | ||||
| 		} catch (Exception e) { | ||||
| 			context.Logger.Error(e, "Caught exception while running procedure: {Procedure}", procedureName); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public async ValueTask DisposeAsync() { | ||||
| 		shutdownCancellationTokenSource.Cancel(); | ||||
|  | ||||
| 		(await currentProcedure.Get(CancellationToken.None))?.CancellationTokenSource.Cancel(); | ||||
| 		await procedureQueueFinished.WaitHandle.WaitOneAsync(); | ||||
|  | ||||
| 		currentProcedure.Dispose(); | ||||
| 		procedureQueue.Dispose(); | ||||
| 		procedureQueueReady.Dispose(); | ||||
| 		procedureQueueFinished.Dispose(); | ||||
| 		shutdownCancellationTokenSource.Dispose(); | ||||
| 	} | ||||
| } | ||||
| @@ -5,4 +5,4 @@ using Phantom.Utils.Tasks; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances; | ||||
|  | ||||
| sealed record InstanceServices(ControllerConnection ControllerConnection, TaskManager TaskManager, PortManager PortManager, BackupManager BackupManager, LaunchServices LaunchServices); | ||||
| sealed record InstanceServices(ControllerConnection ControllerConnection, TaskManager TaskManager, BackupManager BackupManager, LaunchServices LaunchServices); | ||||
|   | ||||
| @@ -1,197 +0,0 @@ | ||||
| using System.Collections.Immutable; | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using Phantom.Agent.Minecraft.Instance; | ||||
| using Phantom.Agent.Minecraft.Java; | ||||
| using Phantom.Agent.Minecraft.Launcher; | ||||
| using Phantom.Agent.Minecraft.Launcher.Types; | ||||
| using Phantom.Agent.Minecraft.Properties; | ||||
| using Phantom.Agent.Minecraft.Server; | ||||
| using Phantom.Agent.Rpc; | ||||
| using Phantom.Agent.Services.Backups; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Agent; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Common.Data.Minecraft; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Messages.Agent.ToController; | ||||
| using Phantom.Utils.IO; | ||||
| using Phantom.Utils.Logging; | ||||
| using Phantom.Utils.Tasks; | ||||
| using Serilog; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances; | ||||
|  | ||||
| sealed class InstanceSessionManager : IAsyncDisposable { | ||||
| 	private static readonly ILogger Logger = PhantomLogger.Create<InstanceSessionManager>(); | ||||
|  | ||||
| 	private readonly ControllerConnection controllerConnection; | ||||
| 	private readonly AgentInfo agentInfo; | ||||
| 	private readonly string basePath; | ||||
|  | ||||
| 	private readonly InstanceServices instanceServices; | ||||
| 	private readonly Dictionary<Guid, Instance> instances = new (); | ||||
|  | ||||
| 	private readonly CancellationTokenSource shutdownCancellationTokenSource = new (); | ||||
| 	private readonly CancellationToken shutdownCancellationToken; | ||||
| 	private readonly SemaphoreSlim semaphore = new (1, 1); | ||||
|  | ||||
| 	private uint instanceLoggerSequenceId = 0; | ||||
|  | ||||
| 	public InstanceSessionManager(ControllerConnection controllerConnection, AgentInfo agentInfo, AgentFolders agentFolders, JavaRuntimeRepository javaRuntimeRepository, TaskManager taskManager, BackupManager backupManager) { | ||||
| 		this.controllerConnection = controllerConnection; | ||||
| 		this.agentInfo = agentInfo; | ||||
| 		this.basePath = agentFolders.InstancesFolderPath; | ||||
| 		this.shutdownCancellationToken = shutdownCancellationTokenSource.Token; | ||||
|  | ||||
| 		var minecraftServerExecutables = new MinecraftServerExecutables(agentFolders.ServerExecutableFolderPath); | ||||
| 		var launchServices = new LaunchServices(minecraftServerExecutables, javaRuntimeRepository); | ||||
| 		var portManager = new PortManager(agentInfo.AllowedServerPorts, agentInfo.AllowedRconPorts); | ||||
|  | ||||
| 		this.instanceServices = new InstanceServices(controllerConnection, taskManager, portManager, backupManager, launchServices); | ||||
| 	} | ||||
|  | ||||
| 	private async Task<InstanceActionResult<T>> AcquireSemaphoreAndRun<T>(Func<Task<InstanceActionResult<T>>> func) { | ||||
| 		try { | ||||
| 			await semaphore.WaitAsync(shutdownCancellationToken); | ||||
| 			try { | ||||
| 				return await func(); | ||||
| 			} finally { | ||||
| 				semaphore.Release(); | ||||
| 			} | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			return InstanceActionResult.General<T>(InstanceActionGeneralResult.AgentShuttingDown); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	[SuppressMessage("ReSharper", "ConvertIfStatementToReturnStatement")] | ||||
| 	private Task<InstanceActionResult<T>> AcquireSemaphoreAndRunWithInstance<T>(Guid instanceGuid, Func<Instance, Task<T>> func) { | ||||
| 		return AcquireSemaphoreAndRun(async () => { | ||||
| 			if (instances.TryGetValue(instanceGuid, out var instance)) { | ||||
| 				return InstanceActionResult.Concrete(await func(instance)); | ||||
| 			} | ||||
| 			else { | ||||
| 				return InstanceActionResult.General<T>(InstanceActionGeneralResult.InstanceDoesNotExist); | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	public async Task<InstanceActionResult<ConfigureInstanceResult>> Configure(Guid instanceGuid, InstanceConfiguration configuration, InstanceLaunchProperties launchProperties, bool launchNow, bool alwaysReportStatus) { | ||||
| 		return await AcquireSemaphoreAndRun(async () => { | ||||
| 			var instanceFolder = Path.Combine(basePath, instanceGuid.ToString()); | ||||
| 			Directories.Create(instanceFolder, Chmod.URWX_GRX); | ||||
|  | ||||
| 			var heapMegabytes = configuration.MemoryAllocation.InMegabytes; | ||||
| 			var jvmProperties = new JvmProperties( | ||||
| 				InitialHeapMegabytes: heapMegabytes / 2, | ||||
| 				MaximumHeapMegabytes: heapMegabytes | ||||
| 			); | ||||
|  | ||||
| 			var properties = new InstanceProperties( | ||||
| 				instanceGuid, | ||||
| 				configuration.JavaRuntimeGuid, | ||||
| 				jvmProperties, | ||||
| 				configuration.JvmArguments, | ||||
| 				instanceFolder, | ||||
| 				configuration.MinecraftVersion, | ||||
| 				new ServerProperties(configuration.ServerPort, configuration.RconPort), | ||||
| 				launchProperties | ||||
| 			); | ||||
|  | ||||
| 			IServerLauncher launcher = configuration.MinecraftServerKind switch { | ||||
| 				MinecraftServerKind.Vanilla => new VanillaLauncher(properties), | ||||
| 				MinecraftServerKind.Fabric  => new FabricLauncher(properties), | ||||
| 				_                           => InvalidLauncher.Instance | ||||
| 			}; | ||||
|  | ||||
| 			if (instances.TryGetValue(instanceGuid, out var instance)) { | ||||
| 				await instance.Reconfigure(configuration, launcher, shutdownCancellationToken); | ||||
| 				Logger.Information("Reconfigured instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, instanceGuid); | ||||
|  | ||||
| 				if (alwaysReportStatus) { | ||||
| 					instance.ReportLastStatus(); | ||||
| 				} | ||||
| 			} | ||||
| 			else { | ||||
| 				instances[instanceGuid] = instance = new Instance(instanceGuid, GetInstanceLoggerName(instanceGuid), instanceServices, configuration, launcher); | ||||
| 				Logger.Information("Created instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, instanceGuid); | ||||
|  | ||||
| 				instance.ReportLastStatus(); | ||||
| 				instance.IsRunningChanged += OnInstanceIsRunningChanged; | ||||
| 			} | ||||
|  | ||||
| 			if (launchNow) { | ||||
| 				await LaunchInternal(instance); | ||||
| 			} | ||||
|  | ||||
| 			return InstanceActionResult.Concrete(ConfigureInstanceResult.Success); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	private string GetInstanceLoggerName(Guid guid) { | ||||
| 		var prefix = guid.ToString(); | ||||
| 		return prefix[..prefix.IndexOf('-')] + "/" + Interlocked.Increment(ref instanceLoggerSequenceId); | ||||
| 	} | ||||
|  | ||||
| 	private ImmutableArray<Instance> GetRunningInstancesInternal() { | ||||
| 		return instances.Values.Where(static instance => instance.IsRunning).ToImmutableArray(); | ||||
| 	} | ||||
|  | ||||
| 	private void OnInstanceIsRunningChanged(object? sender, EventArgs e) { | ||||
| 		instanceServices.TaskManager.Run("Handle instance running state changed event", RefreshAgentStatus); | ||||
| 	} | ||||
|  | ||||
| 	public async Task RefreshAgentStatus() { | ||||
| 		try { | ||||
| 			await semaphore.WaitAsync(shutdownCancellationToken); | ||||
| 			try { | ||||
| 				var runningInstances = GetRunningInstancesInternal(); | ||||
| 				var runningInstanceCount = runningInstances.Length; | ||||
| 				var runningInstanceMemory = runningInstances.Aggregate(RamAllocationUnits.Zero, static (total, instance) => total + instance.Configuration.MemoryAllocation); | ||||
| 				await controllerConnection.Send(new ReportAgentStatusMessage(runningInstanceCount, runningInstanceMemory)); | ||||
| 			} finally { | ||||
| 				semaphore.Release(); | ||||
| 			} | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			// ignore | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public Task<InstanceActionResult<LaunchInstanceResult>> Launch(Guid instanceGuid) { | ||||
| 		return AcquireSemaphoreAndRunWithInstance(instanceGuid, LaunchInternal); | ||||
| 	} | ||||
|  | ||||
| 	private async Task<LaunchInstanceResult> LaunchInternal(Instance instance) { | ||||
| 		var runningInstances = GetRunningInstancesInternal(); | ||||
| 		if (runningInstances.Length + 1 > agentInfo.MaxInstances) { | ||||
| 			return LaunchInstanceResult.InstanceLimitExceeded; | ||||
| 		} | ||||
|  | ||||
| 		var availableMemory = agentInfo.MaxMemory - runningInstances.Aggregate(RamAllocationUnits.Zero, static (total, instance) => total + instance.Configuration.MemoryAllocation); | ||||
| 		if (availableMemory < instance.Configuration.MemoryAllocation) { | ||||
| 			return LaunchInstanceResult.MemoryLimitExceeded; | ||||
| 		} | ||||
|  | ||||
| 		return await instance.Launch(shutdownCancellationToken); | ||||
| 	} | ||||
|  | ||||
| 	public Task<InstanceActionResult<StopInstanceResult>> Stop(Guid instanceGuid, MinecraftStopStrategy stopStrategy) { | ||||
| 		return AcquireSemaphoreAndRunWithInstance(instanceGuid, instance => instance.Stop(stopStrategy, shutdownCancellationToken)); | ||||
| 	} | ||||
|  | ||||
| 	public Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommand(Guid instanceGuid, string command) { | ||||
| 		return AcquireSemaphoreAndRunWithInstance(instanceGuid, async instance => await instance.SendCommand(command, shutdownCancellationToken) ? SendCommandToInstanceResult.Success : SendCommandToInstanceResult.UnknownError); | ||||
| 	} | ||||
|  | ||||
| 	public async ValueTask DisposeAsync() { | ||||
| 		Logger.Information("Stopping all instances..."); | ||||
| 		 | ||||
| 		shutdownCancellationTokenSource.Cancel(); | ||||
|  | ||||
| 		await semaphore.WaitAsync(CancellationToken.None); | ||||
| 		await Task.WhenAll(instances.Values.Select(static instance => instance.DisposeAsync().AsTask())); | ||||
| 		instances.Clear(); | ||||
| 		 | ||||
| 		shutdownCancellationTokenSource.Dispose(); | ||||
| 		semaphore.Dispose(); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										100
									
								
								Agent/Phantom.Agent.Services/Instances/InstanceTicketManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								Agent/Phantom.Agent.Services/Instances/InstanceTicketManager.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | ||||
| using Phantom.Agent.Rpc; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Agent; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Messages.Agent.ToController; | ||||
| using Phantom.Utils.Logging; | ||||
| using Phantom.Utils.Tasks; | ||||
| using Serilog; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances; | ||||
|  | ||||
| sealed class InstanceTicketManager { | ||||
| 	private static readonly ILogger Logger = PhantomLogger.Create<InstanceTicketManager>(); | ||||
| 	 | ||||
| 	private readonly AgentInfo agentInfo; | ||||
| 	private readonly ControllerConnection controllerConnection; | ||||
| 	 | ||||
| 	private readonly HashSet<Guid> activeTicketGuids = new (); | ||||
| 	private readonly HashSet<ushort> usedPorts = new (); | ||||
| 	private RamAllocationUnits usedMemory = new (); | ||||
|  | ||||
| 	public InstanceTicketManager(AgentInfo agentInfo, ControllerConnection controllerConnection) { | ||||
| 		this.agentInfo = agentInfo; | ||||
| 		this.controllerConnection = controllerConnection; | ||||
| 	} | ||||
|  | ||||
| 	public Result<Ticket, LaunchInstanceResult> Reserve(InstanceConfiguration configuration) { | ||||
| 		var memoryAllocation = configuration.MemoryAllocation; | ||||
| 		var serverPort = configuration.ServerPort; | ||||
| 		var rconPort = configuration.RconPort; | ||||
| 		 | ||||
| 		if (!agentInfo.AllowedServerPorts.Contains(serverPort)) { | ||||
| 			return LaunchInstanceResult.ServerPortNotAllowed; | ||||
| 		} | ||||
|  | ||||
| 		if (!agentInfo.AllowedRconPorts.Contains(rconPort)) { | ||||
| 			return LaunchInstanceResult.RconPortNotAllowed; | ||||
| 		} | ||||
| 		 | ||||
| 		lock (this) { | ||||
| 			if (activeTicketGuids.Count + 1 > agentInfo.MaxInstances) { | ||||
| 				return LaunchInstanceResult.InstanceLimitExceeded; | ||||
| 			} | ||||
|  | ||||
| 			if (usedMemory + memoryAllocation > agentInfo.MaxMemory) { | ||||
| 				return LaunchInstanceResult.MemoryLimitExceeded; | ||||
| 			} | ||||
| 			 | ||||
| 			if (usedPorts.Contains(serverPort)) { | ||||
| 				return LaunchInstanceResult.ServerPortAlreadyInUse; | ||||
| 			} | ||||
|  | ||||
| 			if (usedPorts.Contains(rconPort)) { | ||||
| 				return LaunchInstanceResult.RconPortAlreadyInUse; | ||||
| 			} | ||||
|  | ||||
| 			var ticket = new Ticket(Guid.NewGuid(), memoryAllocation, serverPort, rconPort); | ||||
| 			 | ||||
| 			activeTicketGuids.Add(ticket.TicketGuid); | ||||
| 			usedMemory += memoryAllocation; | ||||
| 			usedPorts.Add(serverPort); | ||||
| 			usedPorts.Add(rconPort); | ||||
| 			 | ||||
| 			RefreshAgentStatus(); | ||||
| 			Logger.Debug("Reserved ticket {TicketGuid} (server port {ServerPort}, rcon port {RconPort}, memory allocation {MemoryAllocation} MB).", ticket.TicketGuid, ticket.ServerPort, ticket.RconPort, ticket.MemoryAllocation.InMegabytes); | ||||
|  | ||||
| 			return ticket; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public bool IsValid(Ticket ticket) { | ||||
| 		lock (this) { | ||||
| 			return activeTicketGuids.Contains(ticket.TicketGuid); | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	public void Release(Ticket ticket) { | ||||
| 		lock (this) { | ||||
| 			if (!activeTicketGuids.Remove(ticket.TicketGuid)) { | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			usedMemory -= ticket.MemoryAllocation; | ||||
| 			usedPorts.Remove(ticket.ServerPort); | ||||
| 			usedPorts.Remove(ticket.RconPort); | ||||
| 			 | ||||
| 			RefreshAgentStatus(); | ||||
| 			Logger.Debug("Released ticket {TicketGuid} (server port {ServerPort}, rcon port {RconPort}, memory allocation {MemoryAllocation} MB).", ticket.TicketGuid, ticket.ServerPort, ticket.RconPort, ticket.MemoryAllocation.InMegabytes); | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	public void RefreshAgentStatus() { | ||||
| 		lock (this) { | ||||
| 			controllerConnection.Send(new ReportAgentStatusMessage(activeTicketGuids.Count, usedMemory)); | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	public sealed record Ticket(Guid TicketGuid, RamAllocationUnits MemoryAllocation, ushort ServerPort, ushort RconPort); | ||||
| } | ||||
| @@ -1,58 +0,0 @@ | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Instance; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances; | ||||
|  | ||||
| sealed class PortManager { | ||||
| 	private readonly AllowedPorts allowedServerPorts; | ||||
| 	private readonly AllowedPorts allowedRconPorts; | ||||
| 	private readonly HashSet<ushort> usedPorts = new (); | ||||
|  | ||||
| 	public PortManager(AllowedPorts allowedServerPorts, AllowedPorts allowedRconPorts) { | ||||
| 		this.allowedServerPorts = allowedServerPorts; | ||||
| 		this.allowedRconPorts = allowedRconPorts; | ||||
| 	} | ||||
|  | ||||
| 	public Result Reserve(InstanceConfiguration configuration) { | ||||
| 		var serverPort = configuration.ServerPort; | ||||
| 		var rconPort = configuration.RconPort; | ||||
| 		 | ||||
| 		if (!allowedServerPorts.Contains(serverPort)) { | ||||
| 			return Result.ServerPortNotAllowed; | ||||
| 		} | ||||
|  | ||||
| 		if (!allowedRconPorts.Contains(rconPort)) { | ||||
| 			return Result.RconPortNotAllowed; | ||||
| 		} | ||||
| 		 | ||||
| 		lock (usedPorts) { | ||||
| 			if (usedPorts.Contains(serverPort)) { | ||||
| 				return Result.ServerPortAlreadyInUse; | ||||
| 			} | ||||
|  | ||||
| 			if (usedPorts.Contains(rconPort)) { | ||||
| 				return Result.RconPortAlreadyInUse; | ||||
| 			} | ||||
|  | ||||
| 			usedPorts.Add(serverPort); | ||||
| 			usedPorts.Add(rconPort); | ||||
| 		} | ||||
|  | ||||
| 		return Result.Success; | ||||
| 	} | ||||
| 	 | ||||
| 	public void Release(InstanceConfiguration configuration) { | ||||
| 		lock (usedPorts) { | ||||
| 			usedPorts.Remove(configuration.ServerPort); | ||||
| 			usedPorts.Remove(configuration.RconPort); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public enum Result { | ||||
| 		Success, | ||||
| 		ServerPortNotAllowed, | ||||
| 		ServerPortAlreadyInUse, | ||||
| 		RconPortNotAllowed, | ||||
| 		RconPortAlreadyInUse | ||||
| 	} | ||||
| } | ||||
| @@ -1,29 +0,0 @@ | ||||
| using Phantom.Agent.Services.Backups; | ||||
| using Phantom.Agent.Services.Instances.States; | ||||
| using Phantom.Common.Data.Backups; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances.Procedures; | ||||
|  | ||||
| sealed record BackupInstanceProcedure(BackupManager BackupManager) : IInstanceProcedure { | ||||
| 	private readonly TaskCompletionSource<BackupCreationResult> resultCompletionSource = new (); | ||||
|  | ||||
| 	public Task<BackupCreationResult> Result => resultCompletionSource.Task; | ||||
|  | ||||
| 	public async Task<IInstanceState?> Run(IInstanceContext context, CancellationToken cancellationToken) { | ||||
| 		if (context.CurrentState is not InstanceRunningState runningState || runningState.Process.HasEnded) { | ||||
| 			resultCompletionSource.SetResult(new BackupCreationResult(BackupCreationResultKind.InstanceNotRunning)); | ||||
| 			return null; | ||||
| 		} | ||||
|  | ||||
| 		try { | ||||
| 			var result = await BackupManager.CreateBackup(context.ShortName, runningState.Process, cancellationToken); | ||||
| 			resultCompletionSource.SetResult(result); | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			resultCompletionSource.SetCanceled(cancellationToken); | ||||
| 		} catch (Exception e) { | ||||
| 			resultCompletionSource.SetException(e); | ||||
| 		} | ||||
|  | ||||
| 		return null; | ||||
| 	} | ||||
| } | ||||
| @@ -1,7 +0,0 @@ | ||||
| using Phantom.Agent.Services.Instances.States; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances.Procedures; | ||||
|  | ||||
| interface IInstanceProcedure { | ||||
| 	Task<IInstanceState?> Run(IInstanceContext context, CancellationToken cancellationToken); | ||||
| } | ||||
| @@ -1,97 +0,0 @@ | ||||
| using Phantom.Agent.Minecraft.Instance; | ||||
| using Phantom.Agent.Minecraft.Launcher; | ||||
| using Phantom.Agent.Minecraft.Server; | ||||
| using Phantom.Agent.Services.Instances.States; | ||||
| using Phantom.Common.Data.Instance; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances.Procedures; | ||||
|  | ||||
| sealed record LaunchInstanceProcedure(Guid InstanceGuid, InstanceConfiguration Configuration, IServerLauncher Launcher, bool IsRestarting = false) : IInstanceProcedure { | ||||
| 	public async Task<IInstanceState?> Run(IInstanceContext context, CancellationToken cancellationToken) { | ||||
| 		if (!IsRestarting && context.CurrentState is InstanceRunningState) { | ||||
| 			return null; | ||||
| 		} | ||||
| 		 | ||||
| 		context.SetStatus(IsRestarting ? InstanceStatus.Restarting : InstanceStatus.Launching); | ||||
|  | ||||
| 		InstanceLaunchFailReason? failReason = context.Services.PortManager.Reserve(Configuration) switch { | ||||
| 			PortManager.Result.ServerPortNotAllowed   => InstanceLaunchFailReason.ServerPortNotAllowed, | ||||
| 			PortManager.Result.ServerPortAlreadyInUse => InstanceLaunchFailReason.ServerPortAlreadyInUse, | ||||
| 			PortManager.Result.RconPortNotAllowed     => InstanceLaunchFailReason.RconPortNotAllowed, | ||||
| 			PortManager.Result.RconPortAlreadyInUse   => InstanceLaunchFailReason.RconPortAlreadyInUse, | ||||
| 			_                                         => null | ||||
| 		}; | ||||
|  | ||||
| 		if (failReason is {} reason) { | ||||
| 			context.SetLaunchFailedStatusAndReportEvent(reason); | ||||
| 			return new InstanceNotRunningState(); | ||||
| 		} | ||||
| 		 | ||||
| 		context.Logger.Information("Session starting..."); | ||||
| 		try { | ||||
| 			InstanceProcess process = await DoLaunch(context, cancellationToken); | ||||
| 			return new InstanceRunningState(InstanceGuid, Configuration, Launcher, process, context); | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			context.SetStatus(InstanceStatus.NotRunning); | ||||
| 		} catch (LaunchFailureException e) { | ||||
| 			context.Logger.Error(e.LogMessage); | ||||
| 			context.SetLaunchFailedStatusAndReportEvent(e.Reason); | ||||
| 		} catch (Exception e) { | ||||
| 			context.Logger.Error(e, "Caught exception while launching instance."); | ||||
| 			context.SetLaunchFailedStatusAndReportEvent(InstanceLaunchFailReason.UnknownError); | ||||
| 		} | ||||
| 		 | ||||
| 		context.Services.PortManager.Release(Configuration); | ||||
| 		return new InstanceNotRunningState(); | ||||
| 	} | ||||
|  | ||||
| 	private async Task<InstanceProcess> DoLaunch(IInstanceContext context, CancellationToken cancellationToken) { | ||||
| 		cancellationToken.ThrowIfCancellationRequested(); | ||||
|  | ||||
| 		byte lastDownloadProgress = byte.MaxValue; | ||||
|  | ||||
| 		void OnDownloadProgress(object? sender, DownloadProgressEventArgs args) { | ||||
| 			byte progress = (byte) Math.Min(args.DownloadedBytes * 100 / args.TotalBytes, 100); | ||||
|  | ||||
| 			if (lastDownloadProgress != progress) { | ||||
| 				lastDownloadProgress = progress; | ||||
| 				context.SetStatus(InstanceStatus.Downloading(progress)); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		var launchResult = await Launcher.Launch(context.Logger, context.Services.LaunchServices, OnDownloadProgress, cancellationToken); | ||||
| 		if (launchResult is LaunchResult.InvalidJavaRuntime) { | ||||
| 			throw new LaunchFailureException(InstanceLaunchFailReason.JavaRuntimeNotFound, "Session failed to launch, invalid Java runtime."); | ||||
| 		} | ||||
| 		else if (launchResult is LaunchResult.CouldNotDownloadMinecraftServer) { | ||||
| 			throw new LaunchFailureException(InstanceLaunchFailReason.CouldNotDownloadMinecraftServer, "Session failed to launch, could not download Minecraft server."); | ||||
| 		} | ||||
| 		else if (launchResult is LaunchResult.CouldNotPrepareMinecraftServerLauncher) { | ||||
| 			throw new LaunchFailureException(InstanceLaunchFailReason.CouldNotPrepareMinecraftServerLauncher, "Session failed to launch, could not prepare Minecraft server launcher."); | ||||
| 		} | ||||
| 		else if (launchResult is LaunchResult.CouldNotConfigureMinecraftServer) { | ||||
| 			throw new LaunchFailureException(InstanceLaunchFailReason.CouldNotConfigureMinecraftServer, "Session failed to launch, could not configure Minecraft server."); | ||||
| 		} | ||||
| 		else if (launchResult is LaunchResult.CouldNotStartMinecraftServer) { | ||||
| 			throw new LaunchFailureException(InstanceLaunchFailReason.CouldNotStartMinecraftServer, "Session failed to launch, could not start Minecraft server."); | ||||
| 		} | ||||
|  | ||||
| 		if (launchResult is not LaunchResult.Success launchSuccess) { | ||||
| 			throw new LaunchFailureException(InstanceLaunchFailReason.UnknownError, "Session failed to launch."); | ||||
| 		} | ||||
|  | ||||
| 		context.SetStatus(InstanceStatus.Running); | ||||
| 		context.ReportEvent(InstanceEvent.LaunchSucceeded); | ||||
| 		return launchSuccess.Process; | ||||
| 	} | ||||
|  | ||||
| 	private sealed class LaunchFailureException : Exception { | ||||
| 		public InstanceLaunchFailReason Reason { get; } | ||||
| 		public string LogMessage { get; } | ||||
|  | ||||
| 		public LaunchFailureException(InstanceLaunchFailReason reason, string logMessage) { | ||||
| 			this.Reason = reason; | ||||
| 			this.LogMessage = logMessage; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,17 +0,0 @@ | ||||
| using Phantom.Agent.Services.Instances.States; | ||||
| using Phantom.Common.Data.Instance; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances.Procedures; | ||||
|  | ||||
| sealed record SetInstanceToNotRunningStateProcedure(IInstanceStatus Status) : IInstanceProcedure { | ||||
| 	public Task<IInstanceState?> Run(IInstanceContext context, CancellationToken cancellationToken) { | ||||
| 		if (context.CurrentState is InstanceRunningState { Process.HasEnded: true }) { | ||||
| 			context.SetStatus(Status); | ||||
| 			context.ReportEvent(InstanceEvent.Stopped); | ||||
| 			return Task.FromResult<IInstanceState?>(new InstanceNotRunningState()); | ||||
| 		} | ||||
| 		else { | ||||
| 			return Task.FromResult<IInstanceState?>(null); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,86 @@ | ||||
| using Phantom.Agent.Minecraft.Instance; | ||||
| using Phantom.Agent.Minecraft.Launcher; | ||||
| using Phantom.Agent.Minecraft.Server; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Utils.Tasks; | ||||
|  | ||||
| namespace Phantom.Agent.Services.Instances.State; | ||||
|  | ||||
| static class InstanceLaunchProcedure { | ||||
| 	public static async Task<InstanceRunningState?> Run(InstanceContext context, InstanceConfiguration configuration, IServerLauncher launcher, InstanceTicketManager ticketManager, InstanceTicketManager.Ticket ticket, Action<IInstanceStatus> reportStatus, CancellationToken cancellationToken) { | ||||
| 		context.Logger.Information("Session starting..."); | ||||
|  | ||||
| 		Result<InstanceProcess, InstanceLaunchFailReason> result; | ||||
|  | ||||
| 		if (ticketManager.IsValid(ticket)) { | ||||
| 			try { | ||||
| 				result = await LaunchInstance(context, launcher, reportStatus, cancellationToken); | ||||
| 			} catch (OperationCanceledException) { | ||||
| 				reportStatus(InstanceStatus.NotRunning); | ||||
| 				return null; | ||||
| 			} catch (Exception e) { | ||||
| 				context.Logger.Error(e, "Caught exception while launching instance."); | ||||
| 				result = InstanceLaunchFailReason.UnknownError; | ||||
| 			} | ||||
| 		} | ||||
| 		else { | ||||
| 			context.Logger.Error("Attempted to launch instance with an invalid ticket!"); | ||||
| 			result = InstanceLaunchFailReason.UnknownError; | ||||
| 		} | ||||
|  | ||||
| 		if (result) { | ||||
| 			reportStatus(InstanceStatus.Running); | ||||
| 			context.ReportEvent(InstanceEvent.LaunchSucceeded); | ||||
| 			return new InstanceRunningState(context, configuration, launcher, ticket, result.Value, cancellationToken); | ||||
| 		} | ||||
| 		else { | ||||
| 			reportStatus(InstanceStatus.Failed(result.Error)); | ||||
| 			context.ReportEvent(new InstanceLaunchFailedEvent(result.Error)); | ||||
| 			return null; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private static async Task<Result<InstanceProcess, InstanceLaunchFailReason>> LaunchInstance(InstanceContext context, IServerLauncher launcher, Action<IInstanceStatus> reportStatus, CancellationToken cancellationToken) { | ||||
| 		cancellationToken.ThrowIfCancellationRequested(); | ||||
|  | ||||
| 		byte lastDownloadProgress = byte.MaxValue; | ||||
|  | ||||
| 		void OnDownloadProgress(object? sender, DownloadProgressEventArgs args) { | ||||
| 			byte progress = (byte) Math.Min(args.DownloadedBytes * 100 / args.TotalBytes, 100); | ||||
|  | ||||
| 			if (lastDownloadProgress != progress) { | ||||
| 				lastDownloadProgress = progress; | ||||
| 				reportStatus(InstanceStatus.Downloading(progress)); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		switch (await launcher.Launch(context.Logger, context.Services.LaunchServices, OnDownloadProgress, cancellationToken)) { | ||||
| 			case LaunchResult.Success launchSuccess: | ||||
| 				return launchSuccess.Process; | ||||
|  | ||||
| 			case LaunchResult.InvalidJavaRuntime: | ||||
| 				context.Logger.Error("Session failed to launch, invalid Java runtime."); | ||||
| 				return InstanceLaunchFailReason.JavaRuntimeNotFound; | ||||
|  | ||||
| 			case LaunchResult.CouldNotDownloadMinecraftServer: | ||||
| 				context.Logger.Error("Session failed to launch, could not download Minecraft server."); | ||||
| 				return InstanceLaunchFailReason.CouldNotDownloadMinecraftServer; | ||||
|  | ||||
| 			case LaunchResult.CouldNotPrepareMinecraftServerLauncher: | ||||
| 				context.Logger.Error("Session failed to launch, could not prepare Minecraft server launcher."); | ||||
| 				return InstanceLaunchFailReason.CouldNotPrepareMinecraftServerLauncher; | ||||
|  | ||||
| 			case LaunchResult.CouldNotConfigureMinecraftServer: | ||||
| 				context.Logger.Error("Session failed to launch, could not configure Minecraft server."); | ||||
| 				return InstanceLaunchFailReason.CouldNotConfigureMinecraftServer; | ||||
|  | ||||
| 			case LaunchResult.CouldNotStartMinecraftServer: | ||||
| 				context.Logger.Error("Session failed to launch, could not start Minecraft server."); | ||||
| 				return InstanceLaunchFailReason.CouldNotStartMinecraftServer; | ||||
|  | ||||
| 			default: | ||||
| 				context.Logger.Error("Session failed to launch."); | ||||
| 				return InstanceLaunchFailReason.UnknownError; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -5,10 +5,10 @@ using Phantom.Common.Messages.Agent.ToController; | ||||
| using Phantom.Utils.Logging; | ||||
| using Phantom.Utils.Tasks; | ||||
| 
 | ||||
| namespace Phantom.Agent.Services.Instances; | ||||
| namespace Phantom.Agent.Services.Instances.State; | ||||
| 
 | ||||
| sealed class InstanceLogSender : CancellableBackgroundTask { | ||||
| 	private static readonly BoundedChannelOptions BufferOptions = new (capacity: 64) { | ||||
| 	private static readonly BoundedChannelOptions BufferOptions = new (capacity: 100) { | ||||
| 		SingleReader = true, | ||||
| 		SingleWriter = true, | ||||
| 		FullMode = BoundedChannelFullMode.DropNewest | ||||
| @@ -1,37 +1,39 @@ | ||||
| using Phantom.Agent.Minecraft.Instance; | ||||
| using Phantom.Agent.Minecraft.Launcher; | ||||
| using Phantom.Agent.Services.Backups; | ||||
| using Phantom.Agent.Services.Instances.Procedures; | ||||
| using Phantom.Common.Data.Backups; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Common.Data.Replies; | ||||
| 
 | ||||
| namespace Phantom.Agent.Services.Instances.States; | ||||
| namespace Phantom.Agent.Services.Instances.State; | ||||
| 
 | ||||
| sealed class InstanceRunningState : IInstanceState, IDisposable { | ||||
| sealed class InstanceRunningState : IDisposable { | ||||
| 	public InstanceTicketManager.Ticket Ticket { get; } | ||||
| 	public InstanceProcess Process { get; } | ||||
| 
 | ||||
| 	internal bool IsStopping { get; set; } | ||||
| 
 | ||||
| 	private readonly Guid instanceGuid; | ||||
| 	private readonly InstanceContext context; | ||||
| 	private readonly InstanceConfiguration configuration; | ||||
| 	private readonly IServerLauncher launcher; | ||||
| 	private readonly IInstanceContext context; | ||||
| 	private readonly CancellationToken cancellationToken; | ||||
| 
 | ||||
| 	private readonly InstanceLogSender logSender; | ||||
| 	private readonly BackupScheduler backupScheduler; | ||||
| 
 | ||||
| 	private bool isDisposed; | ||||
| 
 | ||||
| 	public InstanceRunningState(Guid instanceGuid, InstanceConfiguration configuration, IServerLauncher launcher, InstanceProcess process, IInstanceContext context) { | ||||
| 		this.instanceGuid = instanceGuid; | ||||
| 	public InstanceRunningState(InstanceContext context, InstanceConfiguration configuration, IServerLauncher launcher, InstanceTicketManager.Ticket ticket, InstanceProcess process, CancellationToken cancellationToken) { | ||||
| 		this.context = context; | ||||
| 		this.configuration = configuration; | ||||
| 		this.launcher = launcher; | ||||
| 		this.context = context; | ||||
| 		this.Ticket = ticket; | ||||
| 		this.Process = process; | ||||
| 		this.cancellationToken = cancellationToken; | ||||
| 
 | ||||
| 		this.logSender = new InstanceLogSender(context.Services.ControllerConnection, context.Services.TaskManager, instanceGuid, context.ShortName); | ||||
| 		this.logSender = new InstanceLogSender(context.Services.ControllerConnection, context.Services.TaskManager, context.InstanceGuid, context.ShortName); | ||||
| 
 | ||||
| 		this.backupScheduler = new BackupScheduler(context.Services.TaskManager, context.Services.BackupManager, process, context, configuration.ServerPort); | ||||
| 		this.backupScheduler = new BackupScheduler(context, process, configuration.ServerPort); | ||||
| 		this.backupScheduler.BackupCompleted += OnScheduledBackupCompleted; | ||||
| 	} | ||||
| 
 | ||||
| @@ -41,7 +43,7 @@ sealed class InstanceRunningState : IInstanceState, IDisposable { | ||||
| 		if (Process.HasEnded) { | ||||
| 			if (TryDispose()) { | ||||
| 				context.Logger.Warning("Session ended immediately after it was started."); | ||||
| 				context.EnqueueProcedure(new SetInstanceToNotRunningStateProcedure(InstanceStatus.Failed(InstanceLaunchFailReason.UnknownError)), immediate: true); | ||||
| 				context.Actor.Tell(new InstanceActor.HandleProcessEndedCommand(InstanceStatus.Failed(InstanceLaunchFailReason.UnknownError))); | ||||
| 			} | ||||
| 		} | ||||
| 		else { | ||||
| @@ -60,13 +62,17 @@ sealed class InstanceRunningState : IInstanceState, IDisposable { | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		if (cancellationToken.IsCancellationRequested) { | ||||
| 			return; | ||||
| 		} | ||||
| 		 | ||||
| 		if (IsStopping) { | ||||
| 			context.EnqueueProcedure(new SetInstanceToNotRunningStateProcedure(InstanceStatus.NotRunning), immediate: true); | ||||
| 			context.Actor.Tell(new InstanceActor.HandleProcessEndedCommand(InstanceStatus.NotRunning)); | ||||
| 		} | ||||
| 		else { | ||||
| 			context.Logger.Information("Session ended unexpectedly, restarting..."); | ||||
| 			context.ReportEvent(InstanceEvent.Crashed); | ||||
| 			context.EnqueueProcedure(new LaunchInstanceProcedure(instanceGuid, configuration, launcher, IsRestarting: true)); | ||||
| 			context.Actor.Tell(new InstanceActor.LaunchInstanceCommand(configuration, launcher, Ticket, IsRestarting: true)); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| @@ -74,16 +80,16 @@ sealed class InstanceRunningState : IInstanceState, IDisposable { | ||||
| 		context.ReportEvent(new InstanceBackupCompletedEvent(e.Kind, e.Warnings)); | ||||
| 	} | ||||
| 
 | ||||
| 	public async Task<bool> SendCommand(string command, CancellationToken cancellationToken) { | ||||
| 	public async Task<SendCommandToInstanceResult> SendCommand(string command, CancellationToken cancellationToken) { | ||||
| 		try { | ||||
| 			context.Logger.Information("Sending command: {Command}", command); | ||||
| 			await Process.SendCommand(command, cancellationToken); | ||||
| 			return true; | ||||
| 			return SendCommandToInstanceResult.Success; | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			return false; | ||||
| 			return SendCommandToInstanceResult.UnknownError; | ||||
| 		} catch (Exception e) { | ||||
| 			context.Logger.Warning(e, "Caught exception while sending command."); | ||||
| 			return false; | ||||
| 			return SendCommandToInstanceResult.UnknownError; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| @@ -100,7 +106,6 @@ sealed class InstanceRunningState : IInstanceState, IDisposable { | ||||
| 		backupScheduler.Stop(); | ||||
| 		 | ||||
| 		Process.Dispose(); | ||||
| 		context.Services.PortManager.Release(configuration); | ||||
| 		 | ||||
| 		return true; | ||||
| 	} | ||||
| @@ -1,32 +1,25 @@ | ||||
| using System.Diagnostics; | ||||
| using Phantom.Agent.Minecraft.Command; | ||||
| using Phantom.Agent.Minecraft.Instance; | ||||
| using Phantom.Agent.Services.Instances.States; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Common.Data.Minecraft; | ||||
| 
 | ||||
| namespace Phantom.Agent.Services.Instances.Procedures; | ||||
| namespace Phantom.Agent.Services.Instances.State; | ||||
| 
 | ||||
| sealed record StopInstanceProcedure(MinecraftStopStrategy StopStrategy) : IInstanceProcedure { | ||||
| static class InstanceStopProcedure { | ||||
| 	private static readonly ushort[] Stops = { 60, 30, 10, 5, 4, 3, 2, 1, 0 }; | ||||
| 
 | ||||
| 	public async Task<IInstanceState?> Run(IInstanceContext context, CancellationToken cancellationToken) { | ||||
| 		if (context.CurrentState is not InstanceRunningState runningState) { | ||||
| 			return null; | ||||
| 		} | ||||
| 
 | ||||
| 	public static async Task<bool> Run(InstanceContext context, MinecraftStopStrategy stopStrategy, InstanceRunningState runningState, Action<IInstanceStatus> reportStatus, CancellationToken cancellationToken) { | ||||
| 		var process = runningState.Process; | ||||
| 
 | ||||
| 		runningState.IsStopping = true; | ||||
| 		context.SetStatus(InstanceStatus.Stopping); | ||||
| 
 | ||||
| 		var seconds = StopStrategy.Seconds; | ||||
| 		var seconds = stopStrategy.Seconds; | ||||
| 		if (seconds > 0) { | ||||
| 			try { | ||||
| 				await CountDownWithAnnouncements(context, process, seconds, cancellationToken); | ||||
| 			} catch (OperationCanceledException) { | ||||
| 				runningState.IsStopping = false; | ||||
| 				return null; | ||||
| 				return false; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| @@ -38,14 +31,14 @@ sealed record StopInstanceProcedure(MinecraftStopStrategy StopStrategy) : IInsta | ||||
| 			} | ||||
| 		} finally { | ||||
| 			context.Logger.Information("Session stopped."); | ||||
| 			context.SetStatus(InstanceStatus.NotRunning); | ||||
| 			reportStatus(InstanceStatus.NotRunning); | ||||
| 			context.ReportEvent(InstanceEvent.Stopped); | ||||
| 		} | ||||
| 
 | ||||
| 		return new InstanceNotRunningState(); | ||||
| 		return true; | ||||
| 	} | ||||
| 
 | ||||
| 	private async Task CountDownWithAnnouncements(IInstanceContext context, InstanceProcess process, ushort seconds, CancellationToken cancellationToken) { | ||||
| 	private static async Task CountDownWithAnnouncements(InstanceContext context, InstanceProcess process, ushort seconds, CancellationToken cancellationToken) { | ||||
| 		context.Logger.Information("Session stopping in {Seconds} seconds.", seconds); | ||||
| 
 | ||||
| 		foreach (var stop in Stops) { | ||||
| @@ -66,7 +59,7 @@ sealed record StopInstanceProcedure(MinecraftStopStrategy StopStrategy) : IInsta | ||||
| 		return MinecraftCommand.Say("Server shutting down in " + seconds + (seconds == 1 ? " second." : " seconds.")); | ||||
| 	} | ||||
| 
 | ||||
| 	private async Task DoStop(IInstanceContext context, InstanceProcess process) { | ||||
| 	private static async Task DoStop(InstanceContext context, InstanceProcess process) { | ||||
| 		context.Logger.Information("Sending stop command..."); | ||||
| 		await TrySendStopCommand(context, process); | ||||
| 
 | ||||
| @@ -74,7 +67,7 @@ sealed record StopInstanceProcedure(MinecraftStopStrategy StopStrategy) : IInsta | ||||
| 		await WaitForSessionToEnd(context, process); | ||||
| 	} | ||||
| 
 | ||||
| 	private async Task TrySendStopCommand(IInstanceContext context, InstanceProcess process) { | ||||
| 	private static async Task TrySendStopCommand(InstanceContext context, InstanceProcess process) { | ||||
| 		using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); | ||||
| 		try { | ||||
| 			await process.SendCommand(MinecraftCommand.Stop, timeout.Token); | ||||
| @@ -82,15 +75,16 @@ sealed record StopInstanceProcedure(MinecraftStopStrategy StopStrategy) : IInsta | ||||
| 			// Ignore. | ||||
| 		} catch (ObjectDisposedException e) when (e.ObjectName == typeof(Process).FullName && process.HasEnded) { | ||||
| 			// Ignore. | ||||
| 		} catch (IOException e) when (e.HResult == -2147024664 /* The pipe is being closed */) { | ||||
| 			// Ignore. | ||||
| 		} catch (Exception e) { | ||||
| 			context.Logger.Warning(e, "Caught exception while sending stop command."); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	private async Task WaitForSessionToEnd(IInstanceContext context, InstanceProcess process) { | ||||
| 		using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(55)); | ||||
| 	private static async Task WaitForSessionToEnd(InstanceContext context, InstanceProcess process) { | ||||
| 		try { | ||||
| 			await process.WaitForExit(timeout.Token); | ||||
| 			await process.WaitForExit(TimeSpan.FromSeconds(55)); | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			try { | ||||
| 				context.Logger.Warning("Waiting timed out, killing session..."); | ||||
| @@ -1,6 +0,0 @@ | ||||
| namespace Phantom.Agent.Services.Instances.States; | ||||
|  | ||||
| interface IInstanceState { | ||||
| 	void Initialize(); | ||||
| 	Task<bool> SendCommand(string command, CancellationToken cancellationToken); | ||||
| } | ||||
| @@ -1,9 +0,0 @@ | ||||
| namespace Phantom.Agent.Services.Instances.States; | ||||
|  | ||||
| sealed class InstanceNotRunningState : IInstanceState { | ||||
| 	public void Initialize() {} | ||||
|  | ||||
| 	public Task<bool> SendCommand(string command, CancellationToken cancellationToken) { | ||||
| 		return Task.FromResult(false); | ||||
| 	} | ||||
| } | ||||
| @@ -1,4 +1,5 @@ | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Agent.Services.Instances; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Messages.Agent; | ||||
| using Phantom.Common.Messages.Agent.BiDirectional; | ||||
| @@ -57,7 +58,7 @@ public sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToAgent | ||||
| 		connection.SetIsReady(); | ||||
| 		 | ||||
| 		await connection.Send(new AdvertiseJavaRuntimesMessage(agent.JavaRuntimeRepository.All)); | ||||
| 		await agent.InstanceSessionManager.RefreshAgentStatus(); | ||||
| 		agent.InstanceTicketManager.RefreshAgentStatus(); | ||||
| 	} | ||||
|  | ||||
| 	private void HandleRegisterAgentFailure(RegisterAgentFailureMessage message) { | ||||
| @@ -74,7 +75,7 @@ public sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToAgent | ||||
| 	} | ||||
| 	 | ||||
| 	private Task<InstanceActionResult<ConfigureInstanceResult>> HandleConfigureInstance(ConfigureInstanceMessage message, bool alwaysReportStatus) { | ||||
| 		return agent.InstanceSessionManager.Configure(message.InstanceGuid, message.Configuration, message.LaunchProperties, message.LaunchNow, alwaysReportStatus); | ||||
| 		return agent.InstanceManager.Request(new InstanceManagerActor.ConfigureInstanceCommand(message.InstanceGuid, message.Configuration, message.LaunchProperties, message.LaunchNow, alwaysReportStatus)); | ||||
| 	} | ||||
| 	 | ||||
| 	private async Task<InstanceActionResult<ConfigureInstanceResult>> HandleConfigureInstance(ConfigureInstanceMessage message) { | ||||
| @@ -82,15 +83,15 @@ public sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToAgent | ||||
| 	} | ||||
|  | ||||
| 	private async Task<InstanceActionResult<LaunchInstanceResult>> HandleLaunchInstance(LaunchInstanceMessage message) { | ||||
| 		return await agent.InstanceSessionManager.Launch(message.InstanceGuid); | ||||
| 		return await agent.InstanceManager.Request(new InstanceManagerActor.LaunchInstanceCommand(message.InstanceGuid)); | ||||
| 	} | ||||
|  | ||||
| 	private async Task<InstanceActionResult<StopInstanceResult>> HandleStopInstance(StopInstanceMessage message) { | ||||
| 		return await agent.InstanceSessionManager.Stop(message.InstanceGuid, message.StopStrategy); | ||||
| 		return await agent.InstanceManager.Request(new InstanceManagerActor.StopInstanceCommand(message.InstanceGuid, message.StopStrategy)); | ||||
| 	} | ||||
|  | ||||
| 	private async Task<InstanceActionResult<SendCommandToInstanceResult>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) { | ||||
| 		return await agent.InstanceSessionManager.SendCommand(message.InstanceGuid, message.Command); | ||||
| 		return await agent.InstanceManager.Request(new InstanceManagerActor.SendCommandToInstanceCommand(message.InstanceGuid, message.Command)); | ||||
| 	} | ||||
|  | ||||
| 	private void HandleReply(ReplyMessage message) { | ||||
|   | ||||
| @@ -18,12 +18,13 @@ const int ProtocolVersion = 1; | ||||
| var shutdownCancellationTokenSource = new CancellationTokenSource(); | ||||
| var shutdownCancellationToken = shutdownCancellationTokenSource.Token; | ||||
|  | ||||
| ProgramCulture.UseInvariantCulture(); | ||||
| ThreadPool.SetMinThreads(workerThreads: 2, completionPortThreads: 1); | ||||
|  | ||||
| PosixSignals.RegisterCancellation(shutdownCancellationTokenSource, static () => { | ||||
| 	PhantomLogger.Root.InformationHeading("Stopping Phantom Panel agent..."); | ||||
| }); | ||||
|  | ||||
| ThreadPool.SetMinThreads(workerThreads: 2, completionPortThreads: 1); | ||||
|  | ||||
| try { | ||||
| 	var fullVersion = AssemblyAttributes.GetFullVersion(Assembly.GetExecutingAssembly()); | ||||
|  | ||||
| @@ -58,10 +59,8 @@ try { | ||||
| 	var agentServices = new AgentServices(agentInfo, folders, new AgentServiceConfiguration(maxConcurrentBackupCompressionTasks), new ControllerConnection(rpcSocket.Connection)); | ||||
| 	await agentServices.Initialize(); | ||||
|  | ||||
| 	using var actorSystem = ActorSystemFactory.Create("Agent"); | ||||
| 	 | ||||
| 	var rpcMessageHandlerInit = new ControllerMessageHandlerActor.Init(rpcSocket.Connection, agentServices, shutdownCancellationTokenSource); | ||||
| 	var rpcMessageHandlerActor = actorSystem.ActorOf(ControllerMessageHandlerActor.Factory(rpcMessageHandlerInit), "ControllerMessageHandler"); | ||||
| 	var rpcMessageHandlerActor = agentServices.ActorSystem.ActorOf(ControllerMessageHandlerActor.Factory(rpcMessageHandlerInit), "ControllerMessageHandler"); | ||||
| 	 | ||||
| 	var rpcDisconnectSemaphore = new SemaphoreSlim(0, 1); | ||||
| 	var rpcTask = RpcClientRuntime.Launch(rpcSocket, rpcMessageHandlerActor, rpcDisconnectSemaphore, shutdownCancellationToken); | ||||
|   | ||||
| @@ -52,6 +52,18 @@ public static class InstanceStatus { | ||||
| 	public static IInstanceStatus Invalid(string reason) => new InstanceIsInvalid(reason); | ||||
| 	public static IInstanceStatus Downloading(byte progress) => new InstanceIsDownloading(progress); | ||||
| 	public static IInstanceStatus Failed(InstanceLaunchFailReason reason) => new InstanceIsFailed(reason); | ||||
|  | ||||
| 	public static bool IsLaunching(this IInstanceStatus status) { | ||||
| 		return status is InstanceIsDownloading or InstanceIsLaunching or InstanceIsRestarting; | ||||
| 	} | ||||
|  | ||||
| 	public static bool IsRunning(this IInstanceStatus status) { | ||||
| 		return status is InstanceIsRunning; | ||||
| 	} | ||||
| 	 | ||||
| 	public static bool IsStopping(this IInstanceStatus status) { | ||||
| 		return status is InstanceIsStopping; | ||||
| 	} | ||||
| 	 | ||||
| 	public static bool CanLaunch(this IInstanceStatus status) { | ||||
| 		return status is InstanceIsNotRunning or InstanceIsFailed; | ||||
|   | ||||
| @@ -2,10 +2,6 @@ | ||||
|  | ||||
| public enum InstanceLaunchFailReason : byte { | ||||
| 	UnknownError                           = 0, | ||||
| 	ServerPortNotAllowed                   = 1, | ||||
| 	ServerPortAlreadyInUse                 = 2, | ||||
| 	RconPortNotAllowed                     = 3, | ||||
| 	RconPortAlreadyInUse                   = 4, | ||||
| 	JavaRuntimeNotFound                    = 5, | ||||
| 	CouldNotDownloadMinecraftServer        = 6, | ||||
| 	CouldNotConfigureMinecraftServer       = 7, | ||||
|   | ||||
| @@ -5,5 +5,9 @@ public enum LaunchInstanceResult : byte { | ||||
| 	InstanceAlreadyLaunching = 2, | ||||
| 	InstanceAlreadyRunning   = 3, | ||||
| 	InstanceLimitExceeded    = 4, | ||||
| 	MemoryLimitExceeded      = 5 | ||||
| 	MemoryLimitExceeded      = 5, | ||||
| 	ServerPortNotAllowed     = 6, | ||||
| 	ServerPortAlreadyInUse   = 7, | ||||
| 	RconPortNotAllowed       = 8, | ||||
| 	RconPortAlreadyInUse     = 9 | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| namespace Phantom.Common.Data.Replies; | ||||
|  | ||||
| public enum SendCommandToInstanceResult : byte { | ||||
| 	UnknownError, | ||||
| 	Success | ||||
| 	UnknownError = 0, | ||||
| 	Success = 1, | ||||
| 	InstanceNotRunning = 2 | ||||
| } | ||||
|   | ||||
| @@ -100,7 +100,7 @@ public sealed class UserRepository { | ||||
|  | ||||
| 		user.PasswordHash = UserPasswords.Hash(password); | ||||
|  | ||||
| 		return Result.Ok<SetUserPasswordError>(); | ||||
| 		return Result.Ok; | ||||
| 	} | ||||
|  | ||||
| 	public void DeleteUser(UserEntity user) { | ||||
|   | ||||
| @@ -15,7 +15,6 @@ | ||||
|     <ProjectReference Include="..\..\Common\Phantom.Common.Messages.Agent\Phantom.Common.Messages.Agent.csproj" /> | ||||
|     <ProjectReference Include="..\..\Common\Phantom.Common.Messages.Web\Phantom.Common.Messages.Web.csproj" /> | ||||
|     <ProjectReference Include="..\..\Utils\Phantom.Utils.Actor\Phantom.Utils.Actor.csproj" /> | ||||
|     <ProjectReference Include="..\..\Utils\Phantom.Utils.Events\Phantom.Utils.Events.csproj" /> | ||||
|     <ProjectReference Include="..\Phantom.Controller.Database\Phantom.Controller.Database.csproj" /> | ||||
|     <ProjectReference Include="..\Phantom.Controller.Minecraft\Phantom.Controller.Minecraft.csproj" /> | ||||
|   </ItemGroup> | ||||
|   | ||||
| @@ -15,6 +15,8 @@ using Phantom.Utils.Tasks; | ||||
| var shutdownCancellationTokenSource = new CancellationTokenSource(); | ||||
| var shutdownCancellationToken = shutdownCancellationTokenSource.Token; | ||||
|  | ||||
| ProgramCulture.UseInvariantCulture(); | ||||
|  | ||||
| PosixSignals.RegisterCancellation(shutdownCancellationTokenSource, static () => { | ||||
| 	PhantomLogger.Root.InformationHeading("Stopping Phantom Panel controller..."); | ||||
| }); | ||||
|   | ||||
| @@ -29,6 +29,10 @@ public readonly struct ActorRef<TMessage> { | ||||
| 		return Request(message, timeout: null, cancellationToken); | ||||
| 	} | ||||
|  | ||||
| 	public Task<bool> Stop(TMessage message, TimeSpan? timeout = null) { | ||||
| 		return actorRef.GracefulStop(timeout ?? Timeout.InfiniteTimeSpan, message); | ||||
| 	} | ||||
| 	 | ||||
| 	public Task<bool> Stop(TimeSpan? timeout = null) { | ||||
| 		return actorRef.GracefulStop(timeout ?? Timeout.InfiniteTimeSpan); | ||||
| 	} | ||||
|   | ||||
| @@ -37,8 +37,8 @@ public sealed class Process : IDisposable { | ||||
|  | ||||
| 		// https://github.com/dotnet/runtime/issues/81896 | ||||
| 		if (OperatingSystem.IsWindows()) { | ||||
| 			Task.Factory.StartNew(ReadStandardOutputSynchronously, TaskCreationOptions.LongRunning); | ||||
| 			Task.Factory.StartNew(ReadStandardErrorSynchronously, TaskCreationOptions.LongRunning); | ||||
| 			Task.Factory.StartNew(ReadStandardOutputSynchronously, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default); | ||||
| 			Task.Factory.StartNew(ReadStandardErrorSynchronously, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default); | ||||
| 		} | ||||
| 		else { | ||||
| 			this.wrapped.BeginOutputReadLine(); | ||||
| @@ -79,7 +79,11 @@ public sealed class Process : IDisposable { | ||||
| 	} | ||||
|  | ||||
| 	public Task WaitForExitAsync(CancellationToken cancellationToken) { | ||||
| 		return wrapped.WaitForExitAsync(cancellationToken); | ||||
| 		try { | ||||
| 			return wrapped.WaitForExitAsync(cancellationToken); | ||||
| 		} catch (InvalidOperationException) { | ||||
| 			return Task.CompletedTask; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public void Kill(bool entireProcessTree = false) { | ||||
|   | ||||
							
								
								
									
										13
									
								
								Utils/Phantom.Utils/Runtime/ProgramCulture.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								Utils/Phantom.Utils/Runtime/ProgramCulture.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| using System.Globalization; | ||||
|  | ||||
| namespace Phantom.Utils.Runtime; | ||||
|  | ||||
| public static class ProgramCulture { | ||||
| 	public static void UseInvariantCulture() { | ||||
| 		CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; | ||||
| 		CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture; | ||||
| 		 | ||||
| 		CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; | ||||
| 		CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; | ||||
| 	} | ||||
| } | ||||
| @@ -41,14 +41,16 @@ public abstract record Result<TError> { | ||||
| 	public static implicit operator Result<TError>(TError error) { | ||||
| 		return new Fail(error); | ||||
| 	} | ||||
|  | ||||
| 	public static implicit operator Result<TError>(Result.OkType _) { | ||||
| 		return new Ok(); | ||||
| 	} | ||||
| 	 | ||||
| 	public static implicit operator bool(Result<TError> result) { | ||||
| 		return result is Ok; | ||||
| 	} | ||||
| 	 | ||||
| 	public sealed record Ok : Result<TError> { | ||||
| 		internal static Ok Instance { get; } = new (); | ||||
| 		 | ||||
| 		public override TError Error { | ||||
| 			get => throw new InvalidOperationException("Attempted to get error from Ok result."); | ||||
| 			init {} | ||||
| @@ -59,19 +61,7 @@ public abstract record Result<TError> { | ||||
| } | ||||
|  | ||||
| public static class Result { | ||||
| 	public static Result<TError> Ok<TError>() { | ||||
| 		return Result<TError>.Ok.Instance; | ||||
| 	} | ||||
| 	 | ||||
| 	public static Result<TError> Fail<TError>(TError error) { | ||||
| 		return new Result<TError>.Fail(error); | ||||
| 	} | ||||
| 	 | ||||
| 	public static Result<TValue, TError> Ok<TValue, TError>(TValue value) { | ||||
| 		return new Result<TValue, TError>.Ok(value); | ||||
| 	} | ||||
| 	 | ||||
| 	public static Result<TValue, TError> Fail<TValue, TError>(TError error) { | ||||
| 		return new Result<TValue, TError>.Fail(error); | ||||
| 	} | ||||
| 	public static OkType Ok { get; }  = new (); | ||||
|  | ||||
| 	public readonly record struct OkType; | ||||
| } | ||||
|   | ||||
| @@ -90,7 +90,7 @@ | ||||
|   private async Task<Result<string>> CreateOrUpdateAdministrator() { | ||||
|     var reply = await ControllerConnection.Send<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(new CreateOrUpdateAdministratorUserMessage(form.Username, form.Password), Timeout.InfiniteTimeSpan); | ||||
|     return reply switch { | ||||
|            Success             => Result.Ok<string>(), | ||||
|            Success             => Result.Ok, | ||||
|            CreationFailed fail => fail.Error.ToSentences("\n"), | ||||
|            UpdatingFailed fail => fail.Error.ToSentences("\n"), | ||||
|            AddingToRoleFailed  => "Could not assign administrator role to user.", | ||||
|   | ||||
| @@ -17,6 +17,8 @@ using Phantom.Web.Services.Rpc; | ||||
| var shutdownCancellationTokenSource = new CancellationTokenSource(); | ||||
| var shutdownCancellationToken = shutdownCancellationTokenSource.Token; | ||||
|  | ||||
| ProgramCulture.UseInvariantCulture(); | ||||
|  | ||||
| PosixSignals.RegisterCancellation(shutdownCancellationTokenSource, static () => { | ||||
| 	PhantomLogger.Root.InformationHeading("Stopping Phantom Panel web..."); | ||||
| }); | ||||
|   | ||||
| @@ -57,16 +57,16 @@ static class Messages { | ||||
| 			LaunchInstanceResult.InstanceAlreadyRunning   => "Instance is already running.", | ||||
| 			LaunchInstanceResult.InstanceLimitExceeded    => "Agent does not have any more available instances.", | ||||
| 			LaunchInstanceResult.MemoryLimitExceeded      => "Agent does not have enough available memory.", | ||||
| 			LaunchInstanceResult.ServerPortNotAllowed     => "Server port not allowed.", | ||||
| 			LaunchInstanceResult.ServerPortAlreadyInUse   => "Server port already in use.", | ||||
| 			LaunchInstanceResult.RconPortNotAllowed       => "Rcon port not allowed.", | ||||
| 			LaunchInstanceResult.RconPortAlreadyInUse     => "Rcon port already in use.", | ||||
| 			_                                             => "Unknown error." | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	public static string ToSentence(this InstanceLaunchFailReason reason) { | ||||
| 		return reason switch { | ||||
| 			InstanceLaunchFailReason.ServerPortNotAllowed                   => "Server port not allowed.", | ||||
| 			InstanceLaunchFailReason.ServerPortAlreadyInUse                 => "Server port already in use.", | ||||
| 			InstanceLaunchFailReason.RconPortNotAllowed                     => "Rcon port not allowed.", | ||||
| 			InstanceLaunchFailReason.RconPortAlreadyInUse                   => "Rcon port already in use.", | ||||
| 			InstanceLaunchFailReason.JavaRuntimeNotFound                    => "Java runtime not found.", | ||||
| 			InstanceLaunchFailReason.CouldNotDownloadMinecraftServer        => "Could not download Minecraft server.", | ||||
| 			InstanceLaunchFailReason.CouldNotConfigureMinecraftServer       => "Could not configure Minecraft server.", | ||||
| @@ -78,8 +78,9 @@ static class Messages { | ||||
|  | ||||
| 	public static string ToSentence(this SendCommandToInstanceResult reason) { | ||||
| 		return reason switch { | ||||
| 			SendCommandToInstanceResult.Success => "Command sent.", | ||||
| 			_                                   => "Unknown error." | ||||
| 			SendCommandToInstanceResult.Success            => "Command sent.", | ||||
| 			SendCommandToInstanceResult.InstanceNotRunning => "Instance is not running.", | ||||
| 			_                                              => "Unknown error." | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user