mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2025-05-21 04:34:05 +02:00
Rework Agent instance state management into a procedure-based system
This commit is contained in:
parent
def6c41a77
commit
01d6648b1f
Agent/Phantom.Agent.Services
AgentServices.cs
Instances
Common/Phantom.Common.Data/Replies
Utils
@ -35,9 +35,7 @@ public sealed class AgentServices {
|
|||||||
public async Task Shutdown() {
|
public async Task Shutdown() {
|
||||||
Logger.Information("Stopping services...");
|
Logger.Information("Stopping services...");
|
||||||
|
|
||||||
await InstanceSessionManager.StopAll();
|
await InstanceSessionManager.DisposeAsync();
|
||||||
InstanceSessionManager.Dispose();
|
|
||||||
|
|
||||||
await TaskManager.Stop();
|
await TaskManager.Stop();
|
||||||
|
|
||||||
BackupManager.Dispose();
|
BackupManager.Dispose();
|
||||||
|
25
Agent/Phantom.Agent.Services/Instances/IInstanceContext.cs
Normal file
25
Agent/Phantom.Agent.Services/Instances/IInstanceContext.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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,5 +1,6 @@
|
|||||||
using Phantom.Agent.Minecraft.Launcher;
|
using Phantom.Agent.Minecraft.Launcher;
|
||||||
using Phantom.Agent.Rpc;
|
using Phantom.Agent.Rpc;
|
||||||
|
using Phantom.Agent.Services.Instances.Procedures;
|
||||||
using Phantom.Agent.Services.Instances.States;
|
using Phantom.Agent.Services.Instances.States;
|
||||||
using Phantom.Common.Data.Instance;
|
using Phantom.Common.Data.Instance;
|
||||||
using Phantom.Common.Data.Minecraft;
|
using Phantom.Common.Data.Minecraft;
|
||||||
@ -10,18 +11,12 @@ using Serilog;
|
|||||||
|
|
||||||
namespace Phantom.Agent.Services.Instances;
|
namespace Phantom.Agent.Services.Instances;
|
||||||
|
|
||||||
sealed class Instance : IDisposable {
|
sealed class Instance : IAsyncDisposable {
|
||||||
private static uint loggerSequenceId = 0;
|
|
||||||
|
|
||||||
private static string GetLoggerName(Guid guid) {
|
|
||||||
var prefix = guid.ToString();
|
|
||||||
return prefix[..prefix.IndexOf('-')] + "/" + Interlocked.Increment(ref loggerSequenceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private InstanceServices Services { get; }
|
private InstanceServices Services { get; }
|
||||||
|
|
||||||
public InstanceConfiguration Configuration { get; private set; }
|
public InstanceConfiguration Configuration { get; private set; }
|
||||||
private IServerLauncher Launcher { get; set; }
|
private IServerLauncher Launcher { get; set; }
|
||||||
|
private readonly SemaphoreSlim configurationSemaphore = new (1, 1);
|
||||||
|
|
||||||
private readonly string shortName;
|
private readonly string shortName;
|
||||||
private readonly ILogger logger;
|
private readonly ILogger logger;
|
||||||
@ -30,14 +25,14 @@ sealed class Instance : IDisposable {
|
|||||||
private int statusUpdateCounter;
|
private int statusUpdateCounter;
|
||||||
|
|
||||||
private IInstanceState currentState;
|
private IInstanceState currentState;
|
||||||
private readonly SemaphoreSlim stateTransitioningActionSemaphore = new (1, 1);
|
|
||||||
|
|
||||||
public bool IsRunning => currentState is not InstanceNotRunningState;
|
public bool IsRunning => currentState is not InstanceNotRunningState;
|
||||||
|
|
||||||
public event EventHandler? IsRunningChanged;
|
public event EventHandler? IsRunningChanged;
|
||||||
|
|
||||||
|
private readonly InstanceProcedureManager procedureManager;
|
||||||
|
|
||||||
public Instance(InstanceServices services, InstanceConfiguration configuration, IServerLauncher launcher) {
|
public Instance(string shortName, InstanceServices services, InstanceConfiguration configuration, IServerLauncher launcher) {
|
||||||
this.shortName = GetLoggerName(configuration.InstanceGuid);
|
this.shortName = shortName;
|
||||||
this.logger = PhantomLogger.Create<Instance>(shortName);
|
this.logger = PhantomLogger.Create<Instance>(shortName);
|
||||||
|
|
||||||
this.Services = services;
|
this.Services = services;
|
||||||
@ -46,6 +41,8 @@ sealed class Instance : IDisposable {
|
|||||||
|
|
||||||
this.currentState = new InstanceNotRunningState();
|
this.currentState = new InstanceNotRunningState();
|
||||||
this.currentStatus = InstanceStatus.NotRunning;
|
this.currentStatus = InstanceStatus.NotRunning;
|
||||||
|
|
||||||
|
this.procedureManager = new InstanceProcedureManager(this, new Context(this), services.TaskManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TryUpdateStatus(string taskName, Func<Task> getUpdateTask) {
|
private void TryUpdateStatus(string taskName, Func<Task> getUpdateTask) {
|
||||||
@ -76,7 +73,7 @@ sealed class Instance : IDisposable {
|
|||||||
Services.TaskManager.Run("Report event for instance " + shortName, async () => await ServerMessaging.Send(message));
|
Services.TaskManager.Run("Report event for instance " + shortName, async () => await ServerMessaging.Send(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TransitionState(IInstanceState newState) {
|
internal void TransitionState(IInstanceState newState) {
|
||||||
if (currentState == newState) {
|
if (currentState == newState) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -96,114 +93,92 @@ sealed class Instance : IDisposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private T TransitionStateAndReturn<T>((IInstanceState State, T Result) newStateAndResult) {
|
|
||||||
TransitionState(newStateAndResult.State);
|
|
||||||
return newStateAndResult.Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Reconfigure(InstanceConfiguration configuration, IServerLauncher launcher, CancellationToken cancellationToken) {
|
public async Task Reconfigure(InstanceConfiguration configuration, IServerLauncher launcher, CancellationToken cancellationToken) {
|
||||||
await stateTransitioningActionSemaphore.WaitAsync(cancellationToken);
|
await configurationSemaphore.WaitAsync(cancellationToken);
|
||||||
try {
|
try {
|
||||||
Configuration = configuration;
|
Configuration = configuration;
|
||||||
Launcher = launcher;
|
Launcher = launcher;
|
||||||
} finally {
|
} finally {
|
||||||
stateTransitioningActionSemaphore.Release();
|
configurationSemaphore.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<LaunchInstanceResult> Launch(CancellationToken shutdownCancellationToken) {
|
public async Task<LaunchInstanceResult> Launch(CancellationToken cancellationToken) {
|
||||||
await stateTransitioningActionSemaphore.WaitAsync(shutdownCancellationToken);
|
if (IsRunning) {
|
||||||
|
return LaunchInstanceResult.InstanceAlreadyRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await procedureManager.GetCurrentProcedure(cancellationToken) is LaunchInstanceProcedure) {
|
||||||
|
return LaunchInstanceResult.InstanceAlreadyLaunching;
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchInstanceProcedure procedure;
|
||||||
|
|
||||||
|
await configurationSemaphore.WaitAsync(cancellationToken);
|
||||||
try {
|
try {
|
||||||
return TransitionStateAndReturn(currentState.Launch(new InstanceContextImpl(this, shutdownCancellationToken)));
|
procedure = new LaunchInstanceProcedure(Configuration, Launcher);
|
||||||
} catch (Exception e) {
|
|
||||||
logger.Error(e, "Caught exception while launching instance.");
|
|
||||||
return LaunchInstanceResult.UnknownError;
|
|
||||||
} finally {
|
} finally {
|
||||||
stateTransitioningActionSemaphore.Release();
|
configurationSemaphore.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await procedureManager.Enqueue(procedure);
|
||||||
|
return LaunchInstanceResult.LaunchInitiated;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<StopInstanceResult> Stop(MinecraftStopStrategy stopStrategy) {
|
public async Task<StopInstanceResult> Stop(MinecraftStopStrategy stopStrategy, CancellationToken cancellationToken) {
|
||||||
await stateTransitioningActionSemaphore.WaitAsync();
|
if (!IsRunning) {
|
||||||
try {
|
return StopInstanceResult.InstanceAlreadyStopped;
|
||||||
return TransitionStateAndReturn(currentState.Stop(stopStrategy));
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.Error(e, "Caught exception while stopping instance.");
|
|
||||||
return StopInstanceResult.UnknownError;
|
|
||||||
} finally {
|
|
||||||
stateTransitioningActionSemaphore.Release();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if (await procedureManager.GetCurrentProcedure(cancellationToken) is StopInstanceProcedure) {
|
||||||
public async Task StopAndWait(TimeSpan waitTime) {
|
return StopInstanceResult.InstanceAlreadyStopping;
|
||||||
await Stop(MinecraftStopStrategy.Instant);
|
|
||||||
|
|
||||||
using var waitTokenSource = new CancellationTokenSource(waitTime);
|
|
||||||
var waitToken = waitTokenSource.Token;
|
|
||||||
|
|
||||||
while (currentState is not InstanceNotRunningState) {
|
|
||||||
await Task.Delay(TimeSpan.FromMilliseconds(250), waitToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await procedureManager.Enqueue(new StopInstanceProcedure(stopStrategy));
|
||||||
|
return StopInstanceResult.StopInitiated;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> SendCommand(string command, CancellationToken cancellationToken) {
|
public async Task<bool> SendCommand(string command, CancellationToken cancellationToken) {
|
||||||
return await currentState.SendCommand(command, cancellationToken);
|
return await currentState.SendCommand(command, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class InstanceContextImpl : InstanceContext {
|
public async ValueTask DisposeAsync() {
|
||||||
private readonly Instance instance;
|
await procedureManager.DisposeAsync();
|
||||||
private readonly CancellationToken shutdownCancellationToken;
|
|
||||||
|
while (currentState is not InstanceNotRunningState) {
|
||||||
public InstanceContextImpl(Instance instance, CancellationToken shutdownCancellationToken) : base(instance.Services, instance.Configuration, instance.Launcher) {
|
await Task.Delay(TimeSpan.FromMilliseconds(250), CancellationToken.None);
|
||||||
this.instance = instance;
|
|
||||||
this.shutdownCancellationToken = shutdownCancellationToken;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override ILogger Logger => instance.logger;
|
|
||||||
public override string ShortName => instance.shortName;
|
|
||||||
|
|
||||||
public override void SetStatus(IInstanceStatus newStatus) {
|
|
||||||
instance.ReportAndSetStatus(newStatus);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void ReportEvent(IInstanceEvent instanceEvent) {
|
|
||||||
instance.ReportEvent(instanceEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void TransitionState(Func<(IInstanceState, IInstanceStatus?)> newStateAndStatus) {
|
|
||||||
instance.stateTransitioningActionSemaphore.Wait(CancellationToken.None);
|
|
||||||
try {
|
|
||||||
var (state, status) = newStateAndStatus();
|
|
||||||
|
|
||||||
if (!instance.IsRunning) {
|
|
||||||
// Only InstanceSessionManager is allowed to transition an instance out of a non-running state.
|
|
||||||
instance.logger.Debug("Cancelled state transition to {State} because instance is not running.", state.GetType().Name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state is not InstanceNotRunningState && shutdownCancellationToken.IsCancellationRequested) {
|
|
||||||
instance.logger.Debug("Cancelled state transition to {State} due to Agent shutdown.", state.GetType().Name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status != null) {
|
|
||||||
SetStatus(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
instance.TransitionState(state);
|
|
||||||
} catch (Exception e) {
|
|
||||||
instance.logger.Error(e, "Caught exception during state transition.");
|
|
||||||
} finally {
|
|
||||||
instance.stateTransitioningActionSemaphore.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose() {
|
|
||||||
stateTransitioningActionSemaphore.Dispose();
|
|
||||||
|
|
||||||
if (currentState is IDisposable disposable) {
|
if (currentState is IDisposable disposable) {
|
||||||
disposable.Dispose();
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
using Phantom.Agent.Minecraft.Launcher;
|
|
||||||
using Phantom.Agent.Services.Instances.States;
|
|
||||||
using Phantom.Common.Data.Instance;
|
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
namespace Phantom.Agent.Services.Instances;
|
|
||||||
|
|
||||||
abstract class InstanceContext {
|
|
||||||
public InstanceServices Services { get; }
|
|
||||||
public InstanceConfiguration Configuration { get; }
|
|
||||||
public IServerLauncher Launcher { get; }
|
|
||||||
|
|
||||||
public abstract ILogger Logger { get; }
|
|
||||||
public abstract string ShortName { get; }
|
|
||||||
|
|
||||||
protected InstanceContext(InstanceServices services, InstanceConfiguration configuration, IServerLauncher launcher) {
|
|
||||||
Services = services;
|
|
||||||
Configuration = configuration;
|
|
||||||
Launcher = launcher;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract void SetStatus(IInstanceStatus newStatus);
|
|
||||||
|
|
||||||
public void SetLaunchFailedStatusAndReportEvent(InstanceLaunchFailReason reason) {
|
|
||||||
SetStatus(InstanceStatus.Failed(reason));
|
|
||||||
ReportEvent(new InstanceLaunchFailedEvent(reason));
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract void ReportEvent(IInstanceEvent instanceEvent);
|
|
||||||
public abstract void TransitionState(Func<(IInstanceState, IInstanceStatus?)> newStateAndStatus);
|
|
||||||
|
|
||||||
public void TransitionState(IInstanceState newState, IInstanceStatus? newStatus = null) {
|
|
||||||
TransitionState(() => (newState, newStatus));
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,89 @@
|
|||||||
|
using Phantom.Agent.Services.Instances.Procedures;
|
||||||
|
using Phantom.Common.Data.Minecraft;
|
||||||
|
using Phantom.Utils.Collections;
|
||||||
|
using Phantom.Utils.Runtime;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CancelCurrentProcedure() {
|
||||||
|
(await currentProcedure.Get(shutdownCancellationTokenSource.Token))?.CancellationTokenSource.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 CancelCurrentProcedure();
|
||||||
|
await procedureQueueFinished.WaitHandle.WaitOneAsync();
|
||||||
|
|
||||||
|
currentProcedure.Dispose();
|
||||||
|
procedureQueue.Dispose();
|
||||||
|
procedureQueueReady.Dispose();
|
||||||
|
procedureQueueFinished.Dispose();
|
||||||
|
shutdownCancellationTokenSource.Dispose();
|
||||||
|
}
|
||||||
|
}
|
@ -21,9 +21,9 @@ using Serilog;
|
|||||||
|
|
||||||
namespace Phantom.Agent.Services.Instances;
|
namespace Phantom.Agent.Services.Instances;
|
||||||
|
|
||||||
sealed class InstanceSessionManager : IDisposable {
|
sealed class InstanceSessionManager : IAsyncDisposable {
|
||||||
private static readonly ILogger Logger = PhantomLogger.Create<InstanceSessionManager>();
|
private static readonly ILogger Logger = PhantomLogger.Create<InstanceSessionManager>();
|
||||||
|
|
||||||
private readonly AgentInfo agentInfo;
|
private readonly AgentInfo agentInfo;
|
||||||
private readonly string basePath;
|
private readonly string basePath;
|
||||||
|
|
||||||
@ -34,15 +34,17 @@ sealed class InstanceSessionManager : IDisposable {
|
|||||||
private readonly CancellationToken shutdownCancellationToken;
|
private readonly CancellationToken shutdownCancellationToken;
|
||||||
private readonly SemaphoreSlim semaphore = new (1, 1);
|
private readonly SemaphoreSlim semaphore = new (1, 1);
|
||||||
|
|
||||||
|
private uint instanceLoggerSequenceId = 0;
|
||||||
|
|
||||||
public InstanceSessionManager(AgentInfo agentInfo, AgentFolders agentFolders, JavaRuntimeRepository javaRuntimeRepository, TaskManager taskManager, BackupManager backupManager) {
|
public InstanceSessionManager(AgentInfo agentInfo, AgentFolders agentFolders, JavaRuntimeRepository javaRuntimeRepository, TaskManager taskManager, BackupManager backupManager) {
|
||||||
this.agentInfo = agentInfo;
|
this.agentInfo = agentInfo;
|
||||||
this.basePath = agentFolders.InstancesFolderPath;
|
this.basePath = agentFolders.InstancesFolderPath;
|
||||||
this.shutdownCancellationToken = shutdownCancellationTokenSource.Token;
|
this.shutdownCancellationToken = shutdownCancellationTokenSource.Token;
|
||||||
|
|
||||||
var minecraftServerExecutables = new MinecraftServerExecutables(agentFolders.ServerExecutableFolderPath);
|
var minecraftServerExecutables = new MinecraftServerExecutables(agentFolders.ServerExecutableFolderPath);
|
||||||
var launchServices = new LaunchServices(minecraftServerExecutables, javaRuntimeRepository);
|
var launchServices = new LaunchServices(minecraftServerExecutables, javaRuntimeRepository);
|
||||||
var portManager = new PortManager(agentInfo.AllowedServerPorts, agentInfo.AllowedRconPorts);
|
var portManager = new PortManager(agentInfo.AllowedServerPorts, agentInfo.AllowedRconPorts);
|
||||||
|
|
||||||
this.instanceServices = new InstanceServices(taskManager, portManager, backupManager, launchServices);
|
this.instanceServices = new InstanceServices(taskManager, portManager, backupManager, launchServices);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +78,7 @@ sealed class InstanceSessionManager : IDisposable {
|
|||||||
var instanceGuid = configuration.InstanceGuid;
|
var instanceGuid = configuration.InstanceGuid;
|
||||||
var instanceFolder = Path.Combine(basePath, instanceGuid.ToString());
|
var instanceFolder = Path.Combine(basePath, instanceGuid.ToString());
|
||||||
Directories.Create(instanceFolder, Chmod.URWX_GRX);
|
Directories.Create(instanceFolder, Chmod.URWX_GRX);
|
||||||
|
|
||||||
var heapMegabytes = configuration.MemoryAllocation.InMegabytes;
|
var heapMegabytes = configuration.MemoryAllocation.InMegabytes;
|
||||||
var jvmProperties = new JvmProperties(
|
var jvmProperties = new JvmProperties(
|
||||||
InitialHeapMegabytes: heapMegabytes / 2,
|
InitialHeapMegabytes: heapMegabytes / 2,
|
||||||
@ -103,15 +105,15 @@ sealed class InstanceSessionManager : IDisposable {
|
|||||||
if (instances.TryGetValue(instanceGuid, out var instance)) {
|
if (instances.TryGetValue(instanceGuid, out var instance)) {
|
||||||
await instance.Reconfigure(configuration, launcher, shutdownCancellationToken);
|
await instance.Reconfigure(configuration, launcher, shutdownCancellationToken);
|
||||||
Logger.Information("Reconfigured instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, configuration.InstanceGuid);
|
Logger.Information("Reconfigured instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, configuration.InstanceGuid);
|
||||||
|
|
||||||
if (alwaysReportStatus) {
|
if (alwaysReportStatus) {
|
||||||
instance.ReportLastStatus();
|
instance.ReportLastStatus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
instances[instanceGuid] = instance = new Instance(instanceServices, configuration, launcher);
|
instances[instanceGuid] = instance = new Instance(GetInstanceLoggerName(instanceGuid), instanceServices, configuration, launcher);
|
||||||
Logger.Information("Created instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, configuration.InstanceGuid);
|
Logger.Information("Created instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, configuration.InstanceGuid);
|
||||||
|
|
||||||
instance.ReportLastStatus();
|
instance.ReportLastStatus();
|
||||||
instance.IsRunningChanged += OnInstanceIsRunningChanged;
|
instance.IsRunningChanged += OnInstanceIsRunningChanged;
|
||||||
}
|
}
|
||||||
@ -124,6 +126,11 @@ sealed class InstanceSessionManager : IDisposable {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetInstanceLoggerName(Guid guid) {
|
||||||
|
var prefix = guid.ToString();
|
||||||
|
return prefix[..prefix.IndexOf('-')] + "/" + Interlocked.Increment(ref instanceLoggerSequenceId);
|
||||||
|
}
|
||||||
|
|
||||||
private ImmutableArray<Instance> GetRunningInstancesInternal() {
|
private ImmutableArray<Instance> GetRunningInstancesInternal() {
|
||||||
return instances.Values.Where(static instance => instance.IsRunning).ToImmutableArray();
|
return instances.Values.Where(static instance => instance.IsRunning).ToImmutableArray();
|
||||||
}
|
}
|
||||||
@ -167,38 +174,23 @@ sealed class InstanceSessionManager : IDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Task<InstanceActionResult<StopInstanceResult>> Stop(Guid instanceGuid, MinecraftStopStrategy stopStrategy) {
|
public Task<InstanceActionResult<StopInstanceResult>> Stop(Guid instanceGuid, MinecraftStopStrategy stopStrategy) {
|
||||||
return AcquireSemaphoreAndRunWithInstance(instanceGuid, instance => instance.Stop(stopStrategy));
|
return AcquireSemaphoreAndRunWithInstance(instanceGuid, instance => instance.Stop(stopStrategy, shutdownCancellationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommand(Guid instanceGuid, string command) {
|
public Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommand(Guid instanceGuid, string command) {
|
||||||
return AcquireSemaphoreAndRunWithInstance(instanceGuid, async instance => await instance.SendCommand(command, shutdownCancellationToken) ? SendCommandToInstanceResult.Success : SendCommandToInstanceResult.UnknownError);
|
return AcquireSemaphoreAndRunWithInstance(instanceGuid, async instance => await instance.SendCommand(command, shutdownCancellationToken) ? SendCommandToInstanceResult.Success : SendCommandToInstanceResult.UnknownError);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StopAll() {
|
public async ValueTask DisposeAsync() {
|
||||||
shutdownCancellationTokenSource.Cancel();
|
|
||||||
|
|
||||||
Logger.Information("Stopping all instances...");
|
Logger.Information("Stopping all instances...");
|
||||||
|
|
||||||
|
shutdownCancellationTokenSource.Cancel();
|
||||||
|
|
||||||
await semaphore.WaitAsync(CancellationToken.None);
|
await semaphore.WaitAsync(CancellationToken.None);
|
||||||
try {
|
await Task.WhenAll(instances.Values.Select(static instance => instance.DisposeAsync().AsTask()));
|
||||||
await Task.WhenAll(instances.Values.Select(static instance => instance.StopAndWait(TimeSpan.FromSeconds(30))));
|
instances.Clear();
|
||||||
DisposeAllInstances();
|
|
||||||
} finally {
|
|
||||||
semaphore.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose() {
|
|
||||||
DisposeAllInstances();
|
|
||||||
shutdownCancellationTokenSource.Dispose();
|
shutdownCancellationTokenSource.Dispose();
|
||||||
semaphore.Dispose();
|
semaphore.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DisposeAllInstances() {
|
|
||||||
foreach (var (_, instance) in instances) {
|
|
||||||
instance.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
instances.Clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
using Phantom.Agent.Services.Instances.States;
|
||||||
|
|
||||||
|
namespace Phantom.Agent.Services.Instances.Procedures;
|
||||||
|
|
||||||
|
interface IInstanceProcedure {
|
||||||
|
Task<IInstanceState?> Run(IInstanceContext context, CancellationToken cancellationToken);
|
||||||
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
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(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(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.InvalidJvmArguments) {
|
||||||
|
throw new LaunchFailureException(InstanceLaunchFailReason.InvalidJvmArguments, "Session failed to launch, invalid JVM arguments.");
|
||||||
|
}
|
||||||
|
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.LaunchSucceded);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
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,110 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
sealed record StopInstanceProcedure(MinecraftStopStrategy StopStrategy) : IInstanceProcedure {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
var process = runningState.Process;
|
||||||
|
|
||||||
|
runningState.IsStopping = true;
|
||||||
|
context.SetStatus(InstanceStatus.Stopping);
|
||||||
|
|
||||||
|
var seconds = StopStrategy.Seconds;
|
||||||
|
if (seconds > 0) {
|
||||||
|
try {
|
||||||
|
await CountDownWithAnnouncements(context, process, seconds, cancellationToken);
|
||||||
|
} catch (OperationCanceledException) {
|
||||||
|
runningState.IsStopping = false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Too late to cancel the stop procedure now.
|
||||||
|
if (!process.HasEnded) {
|
||||||
|
await DoStop(context, process);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
context.Logger.Information("Session stopped.");
|
||||||
|
context.SetStatus(InstanceStatus.NotRunning);
|
||||||
|
context.ReportEvent(InstanceEvent.Stopped);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new InstanceNotRunningState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CountDownWithAnnouncements(IInstanceContext context, InstanceProcess process, ushort seconds, CancellationToken cancellationToken) {
|
||||||
|
context.Logger.Information("Session stopping in {Seconds} seconds.", seconds);
|
||||||
|
|
||||||
|
foreach (var stop in Stops) {
|
||||||
|
// TODO change to event-based cancellation
|
||||||
|
if (process.HasEnded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seconds > stop) {
|
||||||
|
await process.SendCommand(GetCountDownAnnouncementCommand(seconds), cancellationToken);
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(seconds - stop), cancellationToken);
|
||||||
|
seconds = stop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetCountDownAnnouncementCommand(ushort seconds) {
|
||||||
|
return MinecraftCommand.Say("Server shutting down in " + seconds + (seconds == 1 ? " second." : " seconds."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DoStop(IInstanceContext context, InstanceProcess process) {
|
||||||
|
context.Logger.Information("Session stopping now.");
|
||||||
|
|
||||||
|
// Do not release the semaphore after this point.
|
||||||
|
if (!await process.BackupSemaphore.CancelAndWait(TimeSpan.FromSeconds(1))) {
|
||||||
|
context.Logger.Information("Waiting for backup to finish...");
|
||||||
|
await process.BackupSemaphore.CancelAndWait(Timeout.InfiniteTimeSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Logger.Information("Sending stop command...");
|
||||||
|
await TrySendStopCommand(context, process);
|
||||||
|
|
||||||
|
context.Logger.Information("Waiting for session to end...");
|
||||||
|
await WaitForSessionToEnd(context, process);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TrySendStopCommand(IInstanceContext context, InstanceProcess process) {
|
||||||
|
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||||
|
try {
|
||||||
|
await process.SendCommand(MinecraftCommand.Stop, timeout.Token);
|
||||||
|
} catch (OperationCanceledException) {
|
||||||
|
// Ignore.
|
||||||
|
} catch (ObjectDisposedException e) when (e.ObjectName == typeof(Process).FullName && process.HasEnded) {
|
||||||
|
// 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));
|
||||||
|
try {
|
||||||
|
await process.WaitForExit(timeout.Token);
|
||||||
|
} catch (OperationCanceledException) {
|
||||||
|
try {
|
||||||
|
context.Logger.Warning("Waiting timed out, killing session...");
|
||||||
|
process.Kill();
|
||||||
|
} catch (Exception e) {
|
||||||
|
context.Logger.Error(e, "Caught exception while killing session.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,28 +0,0 @@
|
|||||||
using Phantom.Agent.Minecraft.Instance;
|
|
||||||
|
|
||||||
namespace Phantom.Agent.Services.Instances.Sessions;
|
|
||||||
|
|
||||||
sealed class InstanceSession : IDisposable {
|
|
||||||
private readonly InstanceProcess process;
|
|
||||||
private readonly InstanceContext context;
|
|
||||||
private readonly InstanceLogSender logSender;
|
|
||||||
|
|
||||||
public InstanceSession(InstanceProcess process, InstanceContext context) {
|
|
||||||
this.process = process;
|
|
||||||
this.context = context;
|
|
||||||
this.logSender = new InstanceLogSender(context.Services.TaskManager, context.Configuration.InstanceGuid, context.ShortName);
|
|
||||||
|
|
||||||
this.process.AddOutputListener(SessionOutput);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SessionOutput(object? sender, string line) {
|
|
||||||
context.Logger.Debug("[Server] {Line}", line);
|
|
||||||
logSender.Enqueue(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose() {
|
|
||||||
logSender.Stop();
|
|
||||||
process.Dispose();
|
|
||||||
context.Services.PortManager.Release(context.Configuration);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +1,6 @@
|
|||||||
using Phantom.Common.Data.Minecraft;
|
namespace Phantom.Agent.Services.Instances.States;
|
||||||
using Phantom.Common.Data.Replies;
|
|
||||||
|
|
||||||
namespace Phantom.Agent.Services.Instances.States;
|
|
||||||
|
|
||||||
interface IInstanceState {
|
interface IInstanceState {
|
||||||
void Initialize();
|
void Initialize();
|
||||||
(IInstanceState, LaunchInstanceResult) Launch(InstanceContext context);
|
|
||||||
(IInstanceState, StopInstanceResult) Stop(MinecraftStopStrategy stopStrategy);
|
|
||||||
Task<bool> SendCommand(string command, CancellationToken cancellationToken);
|
Task<bool> SendCommand(string command, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
@ -1,127 +0,0 @@
|
|||||||
using Phantom.Agent.Minecraft.Instance;
|
|
||||||
using Phantom.Agent.Minecraft.Launcher;
|
|
||||||
using Phantom.Agent.Minecraft.Server;
|
|
||||||
using Phantom.Agent.Services.Instances.Sessions;
|
|
||||||
using Phantom.Common.Data.Instance;
|
|
||||||
using Phantom.Common.Data.Minecraft;
|
|
||||||
using Phantom.Common.Data.Replies;
|
|
||||||
|
|
||||||
namespace Phantom.Agent.Services.Instances.States;
|
|
||||||
|
|
||||||
sealed class InstanceLaunchingState : IInstanceState, IDisposable {
|
|
||||||
private readonly InstanceContext context;
|
|
||||||
private readonly CancellationTokenSource cancellationTokenSource = new ();
|
|
||||||
private byte lastDownloadProgress = byte.MaxValue;
|
|
||||||
|
|
||||||
public InstanceLaunchingState(InstanceContext context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Initialize() {
|
|
||||||
context.Logger.Information("Session starting...");
|
|
||||||
|
|
||||||
var launchTask = context.Services.TaskManager.Run("Launch procedure for instance " + context.ShortName, DoLaunch);
|
|
||||||
launchTask.ContinueWith(OnLaunchSuccess, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default);
|
|
||||||
launchTask.ContinueWith(OnLaunchFailure, CancellationToken.None, TaskContinuationOptions.NotOnRanToCompletion, TaskScheduler.Default);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<InstanceProcess> DoLaunch() {
|
|
||||||
var cancellationToken = cancellationTokenSource.Token;
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
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 context.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.InvalidJvmArguments) {
|
|
||||||
throw new LaunchFailureException(InstanceLaunchFailReason.InvalidJvmArguments, "Session failed to launch, invalid JVM arguments.");
|
|
||||||
}
|
|
||||||
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.Launching);
|
|
||||||
return launchSuccess.Process;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnLaunchSuccess(Task<InstanceProcess> task) {
|
|
||||||
context.TransitionState(() => {
|
|
||||||
context.ReportEvent(InstanceEvent.LaunchSucceded);
|
|
||||||
|
|
||||||
var process = task.Result;
|
|
||||||
var session = new InstanceSession(process, context);
|
|
||||||
|
|
||||||
if (cancellationTokenSource.IsCancellationRequested) {
|
|
||||||
return (new InstanceStoppingState(context, process, session), InstanceStatus.Stopping);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return (new InstanceRunningState(context, process, session), null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnLaunchFailure(Task task) {
|
|
||||||
if (task.IsFaulted) {
|
|
||||||
if (task.Exception is { InnerException: LaunchFailureException e }) {
|
|
||||||
context.Logger.Error(e.LogMessage);
|
|
||||||
context.SetLaunchFailedStatusAndReportEvent(e.Reason);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
context.Logger.Error(task.Exception, "Caught exception while launching instance.");
|
|
||||||
context.SetLaunchFailedStatusAndReportEvent(InstanceLaunchFailReason.UnknownError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Services.PortManager.Release(context.Configuration);
|
|
||||||
context.TransitionState(new InstanceNotRunningState());
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public (IInstanceState, LaunchInstanceResult) Launch(InstanceContext context) {
|
|
||||||
return (this, LaunchInstanceResult.InstanceAlreadyLaunching);
|
|
||||||
}
|
|
||||||
|
|
||||||
public (IInstanceState, StopInstanceResult) Stop(MinecraftStopStrategy stopStrategy) {
|
|
||||||
cancellationTokenSource.Cancel();
|
|
||||||
return (this, StopInstanceResult.StopInitiated);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<bool> SendCommand(string command, CancellationToken cancellationToken) {
|
|
||||||
return Task.FromResult(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose() {
|
|
||||||
cancellationTokenSource.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +1,8 @@
|
|||||||
using Phantom.Common.Data.Instance;
|
namespace Phantom.Agent.Services.Instances.States;
|
||||||
using Phantom.Common.Data.Minecraft;
|
|
||||||
using Phantom.Common.Data.Replies;
|
|
||||||
|
|
||||||
namespace Phantom.Agent.Services.Instances.States;
|
|
||||||
|
|
||||||
sealed class InstanceNotRunningState : IInstanceState {
|
sealed class InstanceNotRunningState : IInstanceState {
|
||||||
public void Initialize() {}
|
public void Initialize() {}
|
||||||
|
|
||||||
public (IInstanceState, LaunchInstanceResult) Launch(InstanceContext context) {
|
|
||||||
InstanceLaunchFailReason? failReason = context.Services.PortManager.Reserve(context.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 (this, LaunchInstanceResult.LaunchInitiated);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.SetStatus(InstanceStatus.Launching);
|
|
||||||
return (new InstanceLaunchingState(context), LaunchInstanceResult.LaunchInitiated);
|
|
||||||
}
|
|
||||||
|
|
||||||
public (IInstanceState, StopInstanceResult) Stop(MinecraftStopStrategy stopStrategy) {
|
|
||||||
return (this, StopInstanceResult.InstanceAlreadyStopped);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<bool> SendCommand(string command, CancellationToken cancellationToken) {
|
public Task<bool> SendCommand(string command, CancellationToken cancellationToken) {
|
||||||
return Task.FromResult(false);
|
return Task.FromResult(false);
|
||||||
}
|
}
|
||||||
|
@ -1,133 +1,81 @@
|
|||||||
using Phantom.Agent.Minecraft.Command;
|
using Phantom.Agent.Minecraft.Instance;
|
||||||
using Phantom.Agent.Minecraft.Instance;
|
using Phantom.Agent.Minecraft.Launcher;
|
||||||
using Phantom.Agent.Services.Backups;
|
using Phantom.Agent.Services.Backups;
|
||||||
using Phantom.Agent.Services.Instances.Sessions;
|
using Phantom.Agent.Services.Instances.Procedures;
|
||||||
using Phantom.Common.Data.Backups;
|
using Phantom.Common.Data.Backups;
|
||||||
using Phantom.Common.Data.Instance;
|
using Phantom.Common.Data.Instance;
|
||||||
using Phantom.Common.Data.Minecraft;
|
|
||||||
using Phantom.Common.Data.Replies;
|
|
||||||
|
|
||||||
namespace Phantom.Agent.Services.Instances.States;
|
namespace Phantom.Agent.Services.Instances.States;
|
||||||
|
|
||||||
sealed class InstanceRunningState : IInstanceState {
|
sealed class InstanceRunningState : IInstanceState, IDisposable {
|
||||||
private readonly InstanceContext context;
|
public InstanceProcess Process { get; }
|
||||||
private readonly InstanceProcess process;
|
|
||||||
private readonly BackupScheduler backupScheduler;
|
|
||||||
private readonly RunningSessionDisposer runningSessionDisposer;
|
|
||||||
|
|
||||||
private readonly CancellationTokenSource delayedStopCancellationTokenSource = new ();
|
|
||||||
private bool stateOwnsDelayedStopCancellationTokenSource = true;
|
|
||||||
private bool isStopping;
|
|
||||||
|
|
||||||
public InstanceRunningState(InstanceContext context, InstanceProcess process, InstanceSession session) {
|
internal bool IsStopping { get; set; }
|
||||||
|
|
||||||
|
private readonly InstanceConfiguration configuration;
|
||||||
|
private readonly IServerLauncher launcher;
|
||||||
|
private readonly IInstanceContext context;
|
||||||
|
|
||||||
|
private readonly InstanceLogSender logSender;
|
||||||
|
private readonly BackupScheduler backupScheduler;
|
||||||
|
|
||||||
|
private bool isDisposed;
|
||||||
|
|
||||||
|
public InstanceRunningState(InstanceConfiguration configuration, IServerLauncher launcher, InstanceProcess process, IInstanceContext context) {
|
||||||
|
this.configuration = configuration;
|
||||||
|
this.launcher = launcher;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.process = process;
|
this.Process = process;
|
||||||
this.backupScheduler = new BackupScheduler(context.Services.TaskManager, context.Services.BackupManager, process, context.Configuration.ServerPort, context.ShortName);
|
|
||||||
|
this.logSender = new InstanceLogSender(context.Services.TaskManager, configuration.InstanceGuid, context.ShortName);
|
||||||
|
|
||||||
|
this.backupScheduler = new BackupScheduler(context.Services.TaskManager, context.Services.BackupManager, process, configuration.ServerPort, context.ShortName);
|
||||||
this.backupScheduler.BackupCompleted += OnScheduledBackupCompleted;
|
this.backupScheduler.BackupCompleted += OnScheduledBackupCompleted;
|
||||||
this.runningSessionDisposer = new RunningSessionDisposer(this, session);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Initialize() {
|
public void Initialize() {
|
||||||
process.Ended += ProcessEnded;
|
Process.Ended += ProcessEnded;
|
||||||
|
|
||||||
if (process.HasEnded) {
|
if (Process.HasEnded) {
|
||||||
if (runningSessionDisposer.Dispose()) {
|
if (TryDispose()) {
|
||||||
context.Logger.Warning("Session ended immediately after it was started.");
|
context.Logger.Warning("Session ended immediately after it was started.");
|
||||||
context.ReportEvent(InstanceEvent.Stopped);
|
context.EnqueueProcedure(new SetInstanceToNotRunningStateProcedure(InstanceStatus.Failed(InstanceLaunchFailReason.UnknownError)), immediate: true);
|
||||||
context.Services.TaskManager.Run("Transition state of instance " + context.ShortName + " to not running", () => context.TransitionState(new InstanceNotRunningState(), InstanceStatus.Failed(InstanceLaunchFailReason.UnknownError)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
context.SetStatus(InstanceStatus.Running);
|
|
||||||
context.Logger.Information("Session started.");
|
context.Logger.Information("Session started.");
|
||||||
|
Process.AddOutputListener(SessionOutput);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SessionOutput(object? sender, string line) {
|
||||||
|
context.Logger.Debug("[Server] {Line}", line);
|
||||||
|
logSender.Enqueue(line);
|
||||||
|
}
|
||||||
|
|
||||||
private void ProcessEnded(object? sender, EventArgs e) {
|
private void ProcessEnded(object? sender, EventArgs e) {
|
||||||
if (!runningSessionDisposer.Dispose()) {
|
if (!TryDispose()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isStopping) {
|
if (IsStopping) {
|
||||||
context.Logger.Information("Session ended.");
|
context.EnqueueProcedure(new SetInstanceToNotRunningStateProcedure(InstanceStatus.NotRunning), immediate: true);
|
||||||
context.ReportEvent(InstanceEvent.Stopped);
|
|
||||||
context.TransitionState(new InstanceNotRunningState(), InstanceStatus.NotRunning);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
context.Logger.Information("Session ended unexpectedly, restarting...");
|
context.Logger.Information("Session ended unexpectedly, restarting...");
|
||||||
context.ReportEvent(InstanceEvent.Crashed);
|
context.ReportEvent(InstanceEvent.Crashed);
|
||||||
context.TransitionState(new InstanceLaunchingState(context), InstanceStatus.Restarting);
|
context.EnqueueProcedure(new LaunchInstanceProcedure(configuration, launcher, IsRestarting: true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public (IInstanceState, LaunchInstanceResult) Launch(InstanceContext context) {
|
private void OnScheduledBackupCompleted(object? sender, BackupCreationResult e) {
|
||||||
return (this, LaunchInstanceResult.InstanceAlreadyRunning);
|
context.ReportEvent(new InstanceBackupCompletedEvent(e.Kind, e.Warnings));
|
||||||
}
|
|
||||||
|
|
||||||
public (IInstanceState, StopInstanceResult) Stop(MinecraftStopStrategy stopStrategy) {
|
|
||||||
if (stopStrategy == MinecraftStopStrategy.Instant) {
|
|
||||||
CancelDelayedStop();
|
|
||||||
return (PrepareStoppedState(), StopInstanceResult.StopInitiated);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isStopping) {
|
|
||||||
// TODO change delay or something
|
|
||||||
return (this, StopInstanceResult.InstanceAlreadyStopping);
|
|
||||||
}
|
|
||||||
|
|
||||||
isStopping = true;
|
|
||||||
context.Services.TaskManager.Run("Delayed stop timer for instance " + context.ShortName, () => StopLater(stopStrategy.Seconds));
|
|
||||||
return (this, StopInstanceResult.StopInitiated);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IInstanceState PrepareStoppedState() {
|
|
||||||
process.Ended -= ProcessEnded;
|
|
||||||
backupScheduler.Stop();
|
|
||||||
return new InstanceStoppingState(context, process, runningSessionDisposer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CancelDelayedStop() {
|
|
||||||
try {
|
|
||||||
delayedStopCancellationTokenSource.Cancel();
|
|
||||||
} catch (ObjectDisposedException) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task StopLater(int seconds) {
|
|
||||||
var cancellationToken = delayedStopCancellationTokenSource.Token;
|
|
||||||
|
|
||||||
try {
|
|
||||||
stateOwnsDelayedStopCancellationTokenSource = false;
|
|
||||||
|
|
||||||
int[] stops = { 60, 30, 10, 5, 4, 3, 2, 1, 0 };
|
|
||||||
|
|
||||||
foreach (var stop in stops) {
|
|
||||||
if (seconds > stop) {
|
|
||||||
await SendCommand(MinecraftCommand.Say("Server shutting down in " + seconds + (seconds == 1 ? " second." : " seconds.")), cancellationToken);
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(seconds - stop), cancellationToken);
|
|
||||||
seconds = stop;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (OperationCanceledException) {
|
|
||||||
context.Logger.Debug("Cancelled delayed stop.");
|
|
||||||
return;
|
|
||||||
} catch (ObjectDisposedException) {
|
|
||||||
return;
|
|
||||||
} catch (Exception e) {
|
|
||||||
context.Logger.Warning(e, "Caught exception during delayed stop.");
|
|
||||||
return;
|
|
||||||
} finally {
|
|
||||||
delayedStopCancellationTokenSource.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
context.TransitionState(PrepareStoppedState());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> SendCommand(string command, CancellationToken cancellationToken) {
|
public async Task<bool> SendCommand(string command, CancellationToken cancellationToken) {
|
||||||
try {
|
try {
|
||||||
context.Logger.Information("Sending command: {Command}", command);
|
context.Logger.Information("Sending command: {Command}", command);
|
||||||
await process.SendCommand(command, cancellationToken);
|
await Process.SendCommand(command, cancellationToken);
|
||||||
return true;
|
return true;
|
||||||
} catch (OperationCanceledException) {
|
} catch (OperationCanceledException) {
|
||||||
return false;
|
return false;
|
||||||
@ -137,42 +85,25 @@ sealed class InstanceRunningState : IInstanceState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnScheduledBackupCompleted(object? sender, BackupCreationResult e) {
|
private bool TryDispose() {
|
||||||
context.ReportEvent(new InstanceBackupCompletedEvent(e.Kind, e.Warnings));
|
lock (this) {
|
||||||
|
if (isDisposed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isDisposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
logSender.Stop();
|
||||||
|
backupScheduler.Stop();
|
||||||
|
|
||||||
|
Process.Dispose();
|
||||||
|
context.Services.PortManager.Release(configuration);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class RunningSessionDisposer : IDisposable {
|
public void Dispose() {
|
||||||
private readonly InstanceRunningState state;
|
TryDispose();
|
||||||
private readonly InstanceSession session;
|
|
||||||
private bool isDisposed;
|
|
||||||
|
|
||||||
public RunningSessionDisposer(InstanceRunningState state, InstanceSession session) {
|
|
||||||
this.state = state;
|
|
||||||
this.session = session;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Dispose() {
|
|
||||||
lock (this) {
|
|
||||||
if (isDisposed) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
isDisposed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.stateOwnsDelayedStopCancellationTokenSource) {
|
|
||||||
state.delayedStopCancellationTokenSource.Dispose();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
state.CancelDelayedStop();
|
|
||||||
}
|
|
||||||
|
|
||||||
session.Dispose();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void IDisposable.Dispose() {
|
|
||||||
Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,89 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using Phantom.Agent.Minecraft.Command;
|
|
||||||
using Phantom.Agent.Minecraft.Instance;
|
|
||||||
using Phantom.Common.Data.Instance;
|
|
||||||
using Phantom.Common.Data.Minecraft;
|
|
||||||
using Phantom.Common.Data.Replies;
|
|
||||||
|
|
||||||
namespace Phantom.Agent.Services.Instances.States;
|
|
||||||
|
|
||||||
sealed class InstanceStoppingState : IInstanceState, IDisposable {
|
|
||||||
private readonly InstanceContext context;
|
|
||||||
private readonly InstanceProcess process;
|
|
||||||
private readonly IDisposable sessionDisposer;
|
|
||||||
|
|
||||||
public InstanceStoppingState(InstanceContext context, InstanceProcess process, IDisposable sessionDisposer) {
|
|
||||||
this.context = context;
|
|
||||||
this.process = process;
|
|
||||||
this.sessionDisposer = sessionDisposer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Initialize() {
|
|
||||||
context.Logger.Information("Session stopping.");
|
|
||||||
context.SetStatus(InstanceStatus.Stopping);
|
|
||||||
context.Services.TaskManager.Run("Stop procedure for instance " + context.ShortName, DoStop);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DoStop() {
|
|
||||||
try {
|
|
||||||
// Do not release the semaphore after this point.
|
|
||||||
if (!await process.BackupSemaphore.CancelAndWait(TimeSpan.FromSeconds(1))) {
|
|
||||||
context.Logger.Information("Waiting for backup to finish...");
|
|
||||||
await process.BackupSemaphore.CancelAndWait(Timeout.InfiniteTimeSpan);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Logger.Information("Sending stop command...");
|
|
||||||
await DoSendStopCommand();
|
|
||||||
|
|
||||||
context.Logger.Information("Waiting for session to end...");
|
|
||||||
await DoWaitForSessionToEnd();
|
|
||||||
} finally {
|
|
||||||
context.Logger.Information("Session stopped.");
|
|
||||||
context.ReportEvent(InstanceEvent.Stopped);
|
|
||||||
context.TransitionState(new InstanceNotRunningState(), InstanceStatus.NotRunning);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DoSendStopCommand() {
|
|
||||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
||||||
try {
|
|
||||||
await process.SendCommand(MinecraftCommand.Stop, timeout.Token);
|
|
||||||
} catch (OperationCanceledException) {
|
|
||||||
// ignore
|
|
||||||
} catch (ObjectDisposedException e) when (e.ObjectName == typeof(Process).FullName && process.HasEnded) {
|
|
||||||
// ignore
|
|
||||||
} catch (Exception e) {
|
|
||||||
context.Logger.Warning(e, "Caught exception while sending stop command.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DoWaitForSessionToEnd() {
|
|
||||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(55));
|
|
||||||
try {
|
|
||||||
await process.WaitForExit(timeout.Token);
|
|
||||||
} catch (OperationCanceledException) {
|
|
||||||
try {
|
|
||||||
context.Logger.Warning("Waiting timed out, killing session...");
|
|
||||||
process.Kill();
|
|
||||||
} catch (Exception e) {
|
|
||||||
context.Logger.Error(e, "Caught exception while killing session.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public (IInstanceState, LaunchInstanceResult) Launch(InstanceContext context) {
|
|
||||||
return (this, LaunchInstanceResult.InstanceIsStopping);
|
|
||||||
}
|
|
||||||
|
|
||||||
public (IInstanceState, StopInstanceResult) Stop(MinecraftStopStrategy stopStrategy) {
|
|
||||||
return (this, StopInstanceResult.InstanceAlreadyStopping); // TODO maybe provide a way to kill?
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<bool> SendCommand(string command, CancellationToken cancellationToken) {
|
|
||||||
return Task.FromResult(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose() {
|
|
||||||
sessionDisposer.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +1,11 @@
|
|||||||
namespace Phantom.Common.Data.Replies;
|
namespace Phantom.Common.Data.Replies;
|
||||||
|
|
||||||
public enum LaunchInstanceResult : byte {
|
public enum LaunchInstanceResult : byte {
|
||||||
UnknownError,
|
LaunchInitiated = 1,
|
||||||
LaunchInitiated,
|
InstanceAlreadyLaunching = 2,
|
||||||
InstanceAlreadyLaunching,
|
InstanceAlreadyRunning = 3,
|
||||||
InstanceAlreadyRunning,
|
InstanceLimitExceeded = 4,
|
||||||
InstanceIsStopping,
|
MemoryLimitExceeded = 5
|
||||||
InstanceLimitExceeded,
|
|
||||||
MemoryLimitExceeded
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class LaunchInstanceResultExtensions {
|
public static class LaunchInstanceResultExtensions {
|
||||||
@ -16,7 +14,6 @@ public static class LaunchInstanceResultExtensions {
|
|||||||
LaunchInstanceResult.LaunchInitiated => "Launch initiated.",
|
LaunchInstanceResult.LaunchInitiated => "Launch initiated.",
|
||||||
LaunchInstanceResult.InstanceAlreadyLaunching => "Instance is already launching.",
|
LaunchInstanceResult.InstanceAlreadyLaunching => "Instance is already launching.",
|
||||||
LaunchInstanceResult.InstanceAlreadyRunning => "Instance is already running.",
|
LaunchInstanceResult.InstanceAlreadyRunning => "Instance is already running.",
|
||||||
LaunchInstanceResult.InstanceIsStopping => "Instance is stopping.",
|
|
||||||
LaunchInstanceResult.InstanceLimitExceeded => "Agent does not have any more available instances.",
|
LaunchInstanceResult.InstanceLimitExceeded => "Agent does not have any more available instances.",
|
||||||
LaunchInstanceResult.MemoryLimitExceeded => "Agent does not have enough available memory.",
|
LaunchInstanceResult.MemoryLimitExceeded => "Agent does not have enough available memory.",
|
||||||
_ => "Unknown error."
|
_ => "Unknown error."
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
namespace Phantom.Common.Data.Replies;
|
namespace Phantom.Common.Data.Replies;
|
||||||
|
|
||||||
public enum StopInstanceResult : byte {
|
public enum StopInstanceResult : byte {
|
||||||
UnknownError,
|
StopInitiated = 1,
|
||||||
StopInitiated,
|
InstanceAlreadyStopping = 2,
|
||||||
InstanceAlreadyStopping,
|
InstanceAlreadyStopped = 3
|
||||||
InstanceAlreadyStopped
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class StopInstanceResultExtensions {
|
public static class StopInstanceResultExtensions {
|
||||||
|
39
Utils/Phantom.Utils.Collections/ThreadSafeLinkedList.cs
Normal file
39
Utils/Phantom.Utils.Collections/ThreadSafeLinkedList.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
namespace Phantom.Utils.Collections;
|
||||||
|
|
||||||
|
public sealed class ThreadSafeLinkedList<T> : IDisposable {
|
||||||
|
private readonly LinkedList<T> list = new ();
|
||||||
|
private readonly SemaphoreSlim semaphore = new (1, 1);
|
||||||
|
|
||||||
|
public async Task Add(T item, bool toFront, CancellationToken cancellationToken) {
|
||||||
|
await semaphore.WaitAsync(cancellationToken);
|
||||||
|
try {
|
||||||
|
if (toFront) {
|
||||||
|
list.AddFirst(item);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
list.AddLast(item);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<T?> TryTakeFromFront(CancellationToken cancellationToken) {
|
||||||
|
await semaphore.WaitAsync(cancellationToken);
|
||||||
|
try {
|
||||||
|
var firstNode = list.First;
|
||||||
|
if (firstNode == null) {
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.RemoveFirst();
|
||||||
|
return firstNode.Value;
|
||||||
|
} finally {
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
semaphore.Dispose();
|
||||||
|
}
|
||||||
|
}
|
28
Utils/Phantom.Utils.Runtime/ThreadSafeStructRef.cs
Normal file
28
Utils/Phantom.Utils.Runtime/ThreadSafeStructRef.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
namespace Phantom.Utils.Runtime;
|
||||||
|
|
||||||
|
public sealed class ThreadSafeStructRef<T> : IDisposable where T : struct {
|
||||||
|
private T? value;
|
||||||
|
private readonly SemaphoreSlim semaphore = new (1, 1);
|
||||||
|
|
||||||
|
public async Task<T?> Get(CancellationToken cancellationToken) {
|
||||||
|
await semaphore.WaitAsync(cancellationToken);
|
||||||
|
try {
|
||||||
|
return value;
|
||||||
|
} finally {
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Set(T? value, CancellationToken cancellationToken) {
|
||||||
|
await semaphore.WaitAsync(cancellationToken);
|
||||||
|
try {
|
||||||
|
this.value = value;
|
||||||
|
} finally {
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
semaphore.Dispose();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user