Re-implemented server state machine. Cleaned up code
TODO: Handle trigger errors
This commit is contained in:
166
MoonlightServers.Daemon/Abstractions/Server.Initialize.cs
Normal file
166
MoonlightServers.Daemon/Abstractions/Server.Initialize.cs
Normal file
@@ -0,0 +1,166 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Docker.DotNet.Models;
|
||||
using MoonlightServers.Daemon.Enums;
|
||||
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.NotifyContainerDied, ServerState.Offline)
|
||||
.Permit(ServerTrigger.NotifyOnline, ServerState.Online)
|
||||
.Permit(ServerTrigger.Stop, ServerState.Stopping)
|
||||
.OnEntryAsync(InternalStart);
|
||||
|
||||
StateMachine.Configure(ServerState.Online)
|
||||
.Permit(ServerTrigger.NotifyContainerDied, ServerState.Offline)
|
||||
.Permit(ServerTrigger.Stop, ServerState.Stopping);
|
||||
|
||||
StateMachine.Configure(ServerState.Stopping)
|
||||
.Permit(ServerTrigger.NotifyContainerDied, ServerState.Offline)
|
||||
.Permit(ServerTrigger.Kill, ServerState.Offline)
|
||||
.OnEntryAsync(InternalStop);
|
||||
|
||||
StateMachine.Configure(ServerState.Installing)
|
||||
.Permit(ServerTrigger.NotifyInstallContainerDied, ServerState.Offline);
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user