mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2024-11-25 07:42:58 +01:00
210 lines
6.9 KiB
C#
210 lines
6.9 KiB
C#
using Phantom.Agent.Minecraft.Launcher;
|
|
using Phantom.Agent.Rpc;
|
|
using Phantom.Agent.Services.Instances.States;
|
|
using Phantom.Common.Data.Instance;
|
|
using Phantom.Common.Data.Minecraft;
|
|
using Phantom.Common.Data.Replies;
|
|
using Phantom.Common.Logging;
|
|
using Phantom.Common.Messages.ToServer;
|
|
using Serilog;
|
|
|
|
namespace Phantom.Agent.Services.Instances;
|
|
|
|
sealed class Instance : IDisposable {
|
|
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; }
|
|
|
|
public InstanceConfiguration Configuration { get; private set; }
|
|
private IServerLauncher Launcher { get; set; }
|
|
|
|
private readonly string shortName;
|
|
private readonly ILogger logger;
|
|
|
|
private IInstanceStatus currentStatus;
|
|
private int statusUpdateCounter;
|
|
|
|
private IInstanceState currentState;
|
|
private readonly SemaphoreSlim stateTransitioningActionSemaphore = new (1, 1);
|
|
|
|
public bool IsRunning => currentState is not InstanceNotRunningState;
|
|
|
|
public event EventHandler? IsRunningChanged;
|
|
|
|
public Instance(InstanceServices services, InstanceConfiguration configuration, IServerLauncher launcher) {
|
|
this.shortName = GetLoggerName(configuration.InstanceGuid);
|
|
this.logger = PhantomLogger.Create<Instance>(shortName);
|
|
|
|
this.Services = services;
|
|
this.Configuration = configuration;
|
|
this.Launcher = launcher;
|
|
|
|
this.currentState = new InstanceNotRunningState();
|
|
this.currentStatus = InstanceStatus.NotRunning;
|
|
}
|
|
|
|
private void TryUpdateStatus(string taskName, Func<Task> getUpdateTask) {
|
|
int myStatusUpdateCounter = Interlocked.Increment(ref statusUpdateCounter);
|
|
|
|
Services.TaskManager.Run(taskName, async () => {
|
|
if (myStatusUpdateCounter == statusUpdateCounter) {
|
|
await getUpdateTask();
|
|
}
|
|
});
|
|
}
|
|
|
|
public void ReportLastStatus() {
|
|
TryUpdateStatus("Report last status of instance " + shortName, async () => {
|
|
await ServerMessaging.Send(new ReportInstanceStatusMessage(Configuration.InstanceGuid, currentStatus));
|
|
});
|
|
}
|
|
|
|
private void ReportAndSetStatus(IInstanceStatus status) {
|
|
TryUpdateStatus("Report status of instance " + shortName + " as " + status.GetType().Name, async () => {
|
|
currentStatus = status;
|
|
await ServerMessaging.Send(new ReportInstanceStatusMessage(Configuration.InstanceGuid, status));
|
|
});
|
|
}
|
|
|
|
private void ReportEvent(IInstanceEvent instanceEvent) {
|
|
var message = new ReportInstanceEventMessage(Guid.NewGuid(), DateTime.UtcNow, Configuration.InstanceGuid, instanceEvent);
|
|
Services.TaskManager.Run("Report event for instance " + shortName, async () => await ServerMessaging.Send(message));
|
|
}
|
|
|
|
private 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);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
await stateTransitioningActionSemaphore.WaitAsync(cancellationToken);
|
|
try {
|
|
Configuration = configuration;
|
|
Launcher = launcher;
|
|
} finally {
|
|
stateTransitioningActionSemaphore.Release();
|
|
}
|
|
}
|
|
|
|
public async Task<LaunchInstanceResult> Launch(CancellationToken shutdownCancellationToken) {
|
|
await stateTransitioningActionSemaphore.WaitAsync(shutdownCancellationToken);
|
|
try {
|
|
return TransitionStateAndReturn(currentState.Launch(new InstanceContextImpl(this, shutdownCancellationToken)));
|
|
} catch (Exception e) {
|
|
logger.Error(e, "Caught exception while launching instance.");
|
|
return LaunchInstanceResult.UnknownError;
|
|
} finally {
|
|
stateTransitioningActionSemaphore.Release();
|
|
}
|
|
}
|
|
|
|
public async Task<StopInstanceResult> Stop(MinecraftStopStrategy stopStrategy) {
|
|
await stateTransitioningActionSemaphore.WaitAsync();
|
|
try {
|
|
return TransitionStateAndReturn(currentState.Stop(stopStrategy));
|
|
} catch (Exception e) {
|
|
logger.Error(e, "Caught exception while stopping instance.");
|
|
return StopInstanceResult.UnknownError;
|
|
} finally {
|
|
stateTransitioningActionSemaphore.Release();
|
|
}
|
|
}
|
|
|
|
public async Task StopAndWait(TimeSpan waitTime) {
|
|
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);
|
|
}
|
|
}
|
|
|
|
public async Task<bool> SendCommand(string command, CancellationToken cancellationToken) {
|
|
return await currentState.SendCommand(command, cancellationToken);
|
|
}
|
|
|
|
private sealed class InstanceContextImpl : InstanceContext {
|
|
private readonly Instance instance;
|
|
private readonly CancellationToken shutdownCancellationToken;
|
|
|
|
public InstanceContextImpl(Instance instance, CancellationToken shutdownCancellationToken) : base(instance.Services, instance.Configuration, instance.Launcher) {
|
|
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) {
|
|
disposable.Dispose();
|
|
}
|
|
}
|
|
}
|