Implemented server killing. Updated server manage ui. Added latest tailwind stuff. Added internal error handling

This commit is contained in:
2025-02-15 20:26:10 +01:00
parent 1fbf1ae9ec
commit 56d4313fa8
12 changed files with 294 additions and 109 deletions

View File

@@ -212,6 +212,24 @@ public class ServersController : Controller
} }
} }
[HttpPost("{serverId:int}/kill")]
[Authorize]
public async Task Kill([FromRoute] int serverId)
{
var server = await GetServerWithPermCheck(serverId);
using var apiClient = await NodeService.CreateApiClient(server.Node);
try
{
await apiClient.Post($"api/servers/{server.Id}/kill");
}
catch (HttpRequestException e)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
private async Task<Server> GetServerWithPermCheck(int serverId, private async Task<Server> GetServerWithPermCheck(int serverId,
Func<IQueryable<Server>, IQueryable<Server>>? queryModifier = null) Func<IQueryable<Server>, IQueryable<Server>>? queryModifier = null)
{ {

View File

@@ -31,4 +31,11 @@ public partial class Server
await Destroy(); await Destroy();
} }
public async Task InternalError()
{
await LogToConsole("An unhandled error occured performing action");
Logger.LogInformation("Reporting or smth");
}
} }

View File

@@ -35,31 +35,36 @@ public partial class Server
// Setup transitions // Setup transitions
StateMachine.Configure(ServerState.Offline) StateMachine.Configure(ServerState.Offline)
.Permit(ServerTrigger.Start, ServerState.Starting) .Permit(ServerTrigger.Start, ServerState.Starting) // Allow to start
.Permit(ServerTrigger.Reinstall, ServerState.Installing); .Permit(ServerTrigger.Reinstall, ServerState.Installing) // Allow to install
.OnEntryFromAsync(ServerTrigger.NotifyInternalError, InternalError); // Handle unhandled errors
StateMachine.Configure(ServerState.Starting) StateMachine.Configure(ServerState.Starting)
.Permit(ServerTrigger.NotifyRuntimeContainerDied, ServerState.Offline) .Permit(ServerTrigger.Stop, ServerState.Stopping) // Allow stopping
.Permit(ServerTrigger.NotifyOnline, ServerState.Online) .Permit(ServerTrigger.NotifyOnline, ServerState.Online) // Allow the server to report as online
.Permit(ServerTrigger.Stop, ServerState.Stopping) .Permit(ServerTrigger.NotifyRuntimeContainerDied, ServerState.Offline) // Allow server to handle container death
.OnEntryAsync(InternalStart) .Permit(ServerTrigger.NotifyInternalError, ServerState.Offline) // Error handling for the action below
.OnExitFromAsync(ServerTrigger.NotifyRuntimeContainerDied, InternalCrash); .OnEntryAsync(InternalStart) // Perform start action
.OnExitFromAsync(ServerTrigger.NotifyRuntimeContainerDied, InternalCrash); // Define a runtime container death as a crash
StateMachine.Configure(ServerState.Online) StateMachine.Configure(ServerState.Online)
.Permit(ServerTrigger.NotifyRuntimeContainerDied, ServerState.Offline) .Permit(ServerTrigger.Stop, ServerState.Stopping) // Allow stopping
.Permit(ServerTrigger.Stop, ServerState.Stopping) .Permit(ServerTrigger.NotifyRuntimeContainerDied, ServerState.Offline) // Allow server to handle container death
.OnExitFromAsync(ServerTrigger.NotifyRuntimeContainerDied, InternalCrash); .OnExitFromAsync(ServerTrigger.NotifyRuntimeContainerDied, InternalCrash); // Define a runtime container death as a crash
StateMachine.Configure(ServerState.Stopping) StateMachine.Configure(ServerState.Stopping)
.Permit(ServerTrigger.NotifyRuntimeContainerDied, ServerState.Offline) .PermitReentry(ServerTrigger.Kill) // Allow killing, will return to stopping to trigger kill and handle the death correctly
.Permit(ServerTrigger.Kill, ServerState.Offline) .Permit(ServerTrigger.NotifyRuntimeContainerDied, ServerState.Offline) // Allow server to handle container death
.OnEntryAsync(InternalStop) .Permit(ServerTrigger.NotifyInternalError, ServerState.Offline) // Error handling for the actions below
.OnExitFromAsync(ServerTrigger.NotifyRuntimeContainerDied, InternalFinishStop); .OnEntryFromAsync(ServerTrigger.Stop, InternalStop) // Perform stop action
.OnEntryFromAsync(ServerTrigger.Kill, InternalKill) // Perform kill action
.OnExitFromAsync(ServerTrigger.NotifyRuntimeContainerDied, InternalFinishStop); // Define a runtime container death as a successful stop
StateMachine.Configure(ServerState.Installing) StateMachine.Configure(ServerState.Installing)
.Permit(ServerTrigger.NotifyInstallationContainerDied, ServerState.Offline) .Permit(ServerTrigger.NotifyInstallationContainerDied, ServerState.Offline) // Allow server to handle container death
.OnEntryAsync(InternalInstall) .Permit(ServerTrigger.NotifyInternalError, ServerState.Offline) // Error handling for the action below
.OnExitAsync(InternalFinishInstall); .OnEntryAsync(InternalInstall) // Perform install action
.OnExitFromAsync(ServerTrigger.NotifyInstallationContainerDied, InternalFinishInstall); // Define the death of the installation container as successful
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -13,6 +13,8 @@ public partial class Server
public async Task Install() => await StateMachine.FireAsync(ServerTrigger.Reinstall); public async Task Install() => await StateMachine.FireAsync(ServerTrigger.Reinstall);
private async Task InternalInstall() private async Task InternalInstall()
{
try
{ {
// TODO: Consider if checking for existing install containers is actually useful, because // TODO: Consider if checking for existing install containers is actually useful, because
// when the daemon is starting and a installation is still ongoing it will reattach anyways // when the daemon is starting and a installation is still ongoing it will reattach anyways
@@ -24,7 +26,9 @@ public partial class Server
var remoteService = ServiceProvider.GetRequiredService<RemoteService>(); var remoteService = ServiceProvider.GetRequiredService<RemoteService>();
using var remoteHttpClient = await remoteService.CreateHttpClient(); using var remoteHttpClient = await remoteService.CreateHttpClient();
var installData = await remoteHttpClient.GetJson<ServerInstallDataResponse>($"api/servers/remote/servers/{Configuration.Id}/install"); var installData =
await remoteHttpClient.GetJson<ServerInstallDataResponse>(
$"api/servers/remote/servers/{Configuration.Id}/install");
var dockerImageService = ServiceProvider.GetRequiredService<DockerImageService>(); var dockerImageService = ServiceProvider.GetRequiredService<DockerImageService>();
@@ -78,6 +82,12 @@ public partial class Server
await dockerClient.Containers.StartContainerAsync(InstallationContainerId, new()); await dockerClient.Containers.StartContainerAsync(InstallationContainerId, new());
} }
catch (Exception e)
{
Logger.LogError("An error occured while performing install trigger: {e}", e);
await StateMachine.FireAsync(ServerTrigger.NotifyInternalError);
}
}
private async Task InternalFinishInstall() private async Task InternalFinishInstall()
{ {

View File

@@ -0,0 +1,28 @@
using Docker.DotNet;
using MoonlightServers.Daemon.Enums;
namespace MoonlightServers.Daemon.Abstractions;
public partial class Server
{
public async Task Kill() => await StateMachine.FireAsync(ServerTrigger.Kill);
private async Task InternalKill()
{
try
{
if (RuntimeContainerId == null)
return;
await LogToConsole("Killing container");
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
await dockerClient.Containers.KillContainerAsync(RuntimeContainerId, new());
}
catch (Exception e)
{
Logger.LogError("An error occured while performing stop trigger: {e}", e);
await StateMachine.FireAsync(ServerTrigger.NotifyInternalError);
}
}
}

View File

@@ -8,6 +8,8 @@ public partial class Server
public async Task Start() => await StateMachine.FireAsync(ServerTrigger.Start); public async Task Start() => await StateMachine.FireAsync(ServerTrigger.Start);
private async Task InternalStart() private async Task InternalStart()
{
try
{ {
await ReCreate(); await ReCreate();
@@ -20,4 +22,10 @@ public partial class Server
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>(); var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
await dockerClient.Containers.StartContainerAsync(RuntimeContainerId, new()); await dockerClient.Containers.StartContainerAsync(RuntimeContainerId, new());
} }
catch (Exception e)
{
Logger.LogError("An error occured while performing start trigger: {e}", e);
await StateMachine.FireAsync(ServerTrigger.NotifyInternalError);
}
}
} }

View File

@@ -7,9 +7,17 @@ public partial class Server
public async Task Stop() => await StateMachine.FireAsync(ServerTrigger.Stop); public async Task Stop() => await StateMachine.FireAsync(ServerTrigger.Stop);
private async Task InternalStop() private async Task InternalStop()
{
try
{ {
await Console.WriteToInput($"{Configuration.StopCommand}\n\r"); await Console.WriteToInput($"{Configuration.StopCommand}\n\r");
} }
catch (Exception e)
{
Logger.LogError("An error occured while performing stop trigger: {e}", e);
await StateMachine.FireAsync(ServerTrigger.NotifyInternalError);
}
}
private async Task InternalFinishStop() private async Task InternalFinishStop()
{ {

View File

@@ -9,5 +9,6 @@ public enum ServerTrigger
Reinstall = 4, Reinstall = 4,
NotifyOnline = 5, NotifyOnline = 5,
NotifyRuntimeContainerDied = 6, NotifyRuntimeContainerDied = 6,
NotifyInstallationContainerDied = 7 NotifyInstallationContainerDied = 7,
NotifyInternalError = 8
} }

View File

@@ -17,7 +17,7 @@ public class ServerPowerController : Controller
} }
[HttpPost("{serverId:int}/start")] [HttpPost("{serverId:int}/start")]
public async Task Start(int serverId, [FromQuery] bool runAsync = true) public async Task Start(int serverId)
{ {
var server = ServerService.GetServer(serverId); var server = ServerService.GetServer(serverId);
@@ -28,7 +28,7 @@ public class ServerPowerController : Controller
} }
[HttpPost("{serverId:int}/stop")] [HttpPost("{serverId:int}/stop")]
public async Task Stop(int serverId, [FromQuery] bool runAsync = true) public async Task Stop(int serverId)
{ {
var server = ServerService.GetServer(serverId); var server = ServerService.GetServer(serverId);
@@ -39,7 +39,7 @@ public class ServerPowerController : Controller
} }
[HttpPost("{serverId:int}/install")] [HttpPost("{serverId:int}/install")]
public async Task Install(int serverId, [FromQuery] bool runAsync = true) public async Task Install(int serverId)
{ {
var server = ServerService.GetServer(serverId); var server = ServerService.GetServer(serverId);
@@ -48,4 +48,15 @@ public class ServerPowerController : Controller
await server.Install(); await server.Install();
} }
[HttpPost("{serverId:int}/kill")]
public async Task Kill(int serverId)
{
var server = ServerService.GetServer(serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
await server.Kill();
}
} }

View File

@@ -26,31 +26,31 @@
/* Colors */ /* Colors */
.btn-primary { .btn-primary {
@apply bg-primary-600 hover:bg-primary-500 focus-visible:outline-primary-600; @apply bg-primary-600 hover:bg-primary-500 focus-visible:outline-primary-600 text-diffcolor;
} }
.btn-secondary { .btn-secondary {
@apply bg-secondary-800 hover:bg-secondary-700 focus-visible:outline-secondary-800; @apply bg-secondary-800 hover:bg-secondary-700 focus-visible:outline-secondary-800 text-diffcolor;
} }
.btn-tertiary { .btn-tertiary {
@apply bg-tertiary-600 hover:bg-tertiary-500 focus-visible:outline-tertiary-600; @apply bg-tertiary-600 hover:bg-tertiary-500 focus-visible:outline-tertiary-600 text-diffcolor;
} }
.btn-danger { .btn-danger {
@apply bg-danger-600 hover:bg-danger-500 focus-visible:outline-danger-600; @apply bg-danger-600 hover:bg-danger-500 focus-visible:outline-danger-600 text-diffcolor;
} }
.btn-warning { .btn-warning {
@apply bg-warning-500 hover:bg-warning-400 focus-visible:outline-warning-500; @apply bg-warning-500 hover:bg-warning-400 focus-visible:outline-warning-500 text-diffcolor;
} }
.btn-info { .btn-info {
@apply bg-info-600 hover:bg-info-500 focus-visible:outline-info-600; @apply bg-info-600 hover:bg-info-500 focus-visible:outline-info-600 text-diffcolor;
} }
.btn-success { .btn-success {
@apply bg-success-600 hover:bg-success-500 focus-visible:outline-success-600; @apply bg-success-600 hover:bg-success-500 focus-visible:outline-success-600 text-diffcolor;
} }
/* Outline */ /* Outline */
@@ -78,3 +78,68 @@
.btn-outline-success { .btn-outline-success {
@apply bg-gray-800 hover:border-gray-600 text-success-500; @apply bg-gray-800 hover:border-gray-600 text-success-500;
} }
/* Disabled Buttons */
.btn:disabled,
.btn-lg:disabled,
.btn-sm:disabled,
.btn-xs:disabled {
@apply opacity-50 cursor-not-allowed pointer-events-none;
}
/* Colors for Disabled States */
.btn-primary:disabled {
@apply bg-primary-600 text-gray-300;
}
.btn-secondary:disabled {
@apply bg-secondary-800 text-gray-400;
}
.btn-tertiary:disabled {
@apply bg-tertiary-600 text-gray-300;
}
.btn-danger:disabled {
@apply bg-danger-600 text-gray-300;
}
.btn-warning:disabled {
@apply bg-warning-500 text-gray-400;
}
.btn-info:disabled {
@apply bg-info-600 text-gray-300;
}
.btn-success:disabled {
@apply bg-success-600 text-gray-300;
}
/* Outline Disabled States */
.btn-outline-primary:disabled {
@apply bg-gray-800 border-gray-700 text-gray-500;
}
.btn-outline-tertiary:disabled {
@apply bg-gray-800 border-gray-700 text-gray-500;
}
.btn-outline-danger:disabled {
@apply bg-gray-800 border-gray-700 text-gray-500;
}
.btn-outline-warning:disabled {
@apply bg-gray-800 border-gray-700 text-gray-500;
}
.btn-outline-info:disabled {
@apply bg-gray-800 border-gray-700 text-gray-500;
}
.btn-outline-success:disabled {
@apply bg-gray-800 border-gray-700 text-gray-500;
}

View File

@@ -119,6 +119,9 @@ module.exports = {
950: '#0e121c', 950: '#0e121c',
} }
}, },
textColor:{
diffcolor: 'rgb(var(--color-diffcolor, var(--color-light)))'
},
animation: { animation: {
'shimmer': 'shimmer 2s linear infinite', 'shimmer': 'shimmer 2s linear infinite',
} }

View File

@@ -74,17 +74,39 @@
<span class="align-middle">Start</span> <span class="align-middle">Start</span>
</button> </button>
} }
@if (State == ServerState.Online)
{
<button type="button" class="btn btn-primary"> <button type="button" class="btn btn-primary">
<i class="icon-rotate-ccw me-1 align-middle"></i> <i class="icon-rotate-ccw me-1 align-middle"></i>
<span class="align-middle">Restart</span> <span class="align-middle">Restart</span>
</button> </button>
@if (State == ServerState.Starting || State == ServerState.Online) }
else
{
<button type="button" class="btn btn-primary" disabled="disabled">
<i class="icon-rotate-ccw me-1 align-middle"></i>
<span class="align-middle">Restart</span>
</button>
}
@if (State == ServerState.Starting || State == ServerState.Online || State == ServerState.Stopping)
{
if (State == ServerState.Stopping)
{
<WButton CssClasses="btn btn-danger" OnClick="_ => Kill()">
<i class="icon-bomb me-1 align-middle"></i>
<span class="align-middle">Kill</span>
</WButton>
}
else
{ {
<WButton CssClasses="btn btn-danger" OnClick="_ => Stop()"> <WButton CssClasses="btn btn-danger" OnClick="_ => Stop()">
<i class="icon-squircle me-1 align-middle"></i> <i class="icon-squircle me-1 align-middle"></i>
<span class="align-middle">Stop</span> <span class="align-middle">Stop</span>
</WButton> </WButton>
} }
}
else else
{ {
<button type="button" class="btn btn-danger" disabled="disabled"> <button type="button" class="btn btn-danger" disabled="disabled">
@@ -230,14 +252,13 @@
} }
private async Task Start() private async Task Start()
{ => await ApiClient.Post($"api/servers/{Server.Id}/start");
await ApiClient.Post($"api/servers/{Server.Id}/start");
}
private async Task Stop() private async Task Stop()
{ => await ApiClient.Post($"api/servers/{Server.Id}/stop");
await ApiClient.Post($"api/servers/{Server.Id}/stop");
} private async Task Kill()
=> await ApiClient.Post($"api/servers/{Server.Id}/kill");
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {