172 lines
6.0 KiB
C#
172 lines
6.0 KiB
C#
using System.Text.RegularExpressions;
|
|
using Docker.DotNet.Models;
|
|
using MoonlightServers.Daemon.Enums;
|
|
using MoonlightServers.Daemon.Extensions;
|
|
using Stateless;
|
|
|
|
namespace MoonlightServers.Daemon.Abstractions;
|
|
|
|
public partial class Server
|
|
{
|
|
// We are expecting a list of running containers, as we don't wont to inspect every possible container just to check if it exists.
|
|
// If none are provided, we skip the checks. Use this overload if you are creating a new server which didn't exist before
|
|
public async Task Initialize(IList<ContainerListResponse>? runningContainers = null)
|
|
{
|
|
if (runningContainers != null)
|
|
{
|
|
var reAttachSuccessful = await ReAttach(runningContainers);
|
|
|
|
// If we weren't able to reattach with the current running containers, we initialize the
|
|
// state machine as offline
|
|
if(!reAttachSuccessful)
|
|
await InitializeStateMachine(ServerState.Offline);
|
|
}
|
|
else
|
|
await InitializeStateMachine(ServerState.Offline);
|
|
|
|
// And at last we initialize all events, so we can react to certain state changes and outputs.
|
|
// We need to do this regardless if the server was reattached or not, as it hasn't been initialized yet
|
|
await InitializeEvents();
|
|
}
|
|
|
|
private Task InitializeStateMachine(ServerState initialState)
|
|
{
|
|
StateMachine = new StateMachine<ServerState, ServerTrigger>(initialState);
|
|
|
|
// Setup transitions
|
|
StateMachine.Configure(ServerState.Offline)
|
|
.Permit(ServerTrigger.Start, ServerState.Starting)
|
|
.Permit(ServerTrigger.Reinstall, ServerState.Installing);
|
|
|
|
StateMachine.Configure(ServerState.Starting)
|
|
.Permit(ServerTrigger.NotifyRuntimeContainerDied, ServerState.Offline)
|
|
.Permit(ServerTrigger.NotifyOnline, ServerState.Online)
|
|
.Permit(ServerTrigger.Stop, ServerState.Stopping)
|
|
.OnEntryAsync(InternalStart)
|
|
.OnExitFromAsync(ServerTrigger.NotifyRuntimeContainerDied, InternalCrash);
|
|
|
|
StateMachine.Configure(ServerState.Online)
|
|
.Permit(ServerTrigger.NotifyRuntimeContainerDied, ServerState.Offline)
|
|
.Permit(ServerTrigger.Stop, ServerState.Stopping)
|
|
.OnExitFromAsync(ServerTrigger.NotifyRuntimeContainerDied, InternalCrash);
|
|
|
|
StateMachine.Configure(ServerState.Stopping)
|
|
.Permit(ServerTrigger.NotifyRuntimeContainerDied, ServerState.Offline)
|
|
.Permit(ServerTrigger.Kill, ServerState.Offline)
|
|
.OnEntryAsync(InternalStop)
|
|
.OnExitFromAsync(ServerTrigger.NotifyRuntimeContainerDied, InternalFinishStop);
|
|
|
|
StateMachine.Configure(ServerState.Installing)
|
|
.Permit(ServerTrigger.NotifyInstallationContainerDied, ServerState.Offline)
|
|
.OnEntryAsync(InternalInstall)
|
|
.OnExitAsync(InternalFinishInstall);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private Task InitializeEvents()
|
|
{
|
|
Console.OnOutput += async content =>
|
|
{
|
|
if (StateMachine.State == ServerState.Starting)
|
|
{
|
|
if (Regex.Matches(content, Configuration.OnlineDetection).Count > 0)
|
|
await StateMachine.FireAsync(ServerTrigger.NotifyOnline);
|
|
}
|
|
};
|
|
|
|
StateMachine.OnTransitioned(transition =>
|
|
{
|
|
Logger.LogInformation(
|
|
"{source} => {destination} ({trigger})",
|
|
transition.Source,
|
|
transition.Destination,
|
|
transition.Trigger
|
|
);
|
|
});
|
|
|
|
StateMachine.OnTransitionCompleted(transition =>
|
|
{
|
|
Logger.LogInformation("State: {state}", transition.Destination);
|
|
});
|
|
|
|
// Proxy the events so outside subscribes can react to it
|
|
StateMachine.OnTransitionCompletedAsync(async transition =>
|
|
{
|
|
if (OnStateChanged != null)
|
|
{
|
|
await OnStateChanged(transition.Destination);
|
|
}
|
|
});
|
|
|
|
Console.OnOutput += (async message =>
|
|
{
|
|
if (OnConsoleOutput != null)
|
|
{
|
|
await OnConsoleOutput(message);
|
|
}
|
|
});
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
#region Reattaching & reattach strategies
|
|
|
|
private async Task<bool> ReAttach(IList<ContainerListResponse> runningContainers)
|
|
{
|
|
// Docker container names are starting with a / when returned in the docker container list api endpoint,
|
|
// so we trim it from the name when searching
|
|
|
|
var existingRuntimeContainer = runningContainers.FirstOrDefault(
|
|
x => x.Names.Any(y => y.TrimStart('/') == RuntimeContainerName)
|
|
);
|
|
|
|
if (existingRuntimeContainer != null)
|
|
{
|
|
await ReAttachToRuntime(existingRuntimeContainer);
|
|
return true;
|
|
}
|
|
|
|
var existingInstallContainer = runningContainers.FirstOrDefault(
|
|
x => x.Names.Any(y => y.TrimStart('/') == InstallationContainerName)
|
|
);
|
|
|
|
if (existingInstallContainer != null)
|
|
{
|
|
await ReAttachToInstallation(existingInstallContainer);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private async Task ReAttachToRuntime(ContainerListResponse runtimeContainer)
|
|
{
|
|
if (runtimeContainer.State == "running")
|
|
{
|
|
RuntimeContainerId = runtimeContainer.ID;
|
|
|
|
await InitializeStateMachine(ServerState.Online);
|
|
|
|
await AttachConsole(runtimeContainer.ID);
|
|
}
|
|
else
|
|
await InitializeStateMachine(ServerState.Offline);
|
|
}
|
|
|
|
private async Task ReAttachToInstallation(ContainerListResponse installationContainer)
|
|
{
|
|
if (installationContainer.State == "running")
|
|
{
|
|
InstallationContainerId = installationContainer.ID;
|
|
|
|
await InitializeStateMachine(ServerState.Installing);
|
|
|
|
await AttachConsole(installationContainer.ID);
|
|
}
|
|
else
|
|
await InitializeStateMachine(ServerState.Offline);
|
|
}
|
|
|
|
#endregion
|
|
} |