Implemented server killing. Updated server manage ui. Added latest tailwind stuff. Added internal error handling
This commit is contained in:
@@ -211,6 +211,24 @@ public class ServersController : Controller
|
||||
throw new HttpApiException("Unable to access the node the server is running on", 502);
|
||||
}
|
||||
}
|
||||
|
||||
[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,
|
||||
Func<IQueryable<Server>, IQueryable<Server>>? queryModifier = null)
|
||||
|
||||
@@ -31,4 +31,11 @@ public partial class Server
|
||||
|
||||
await Destroy();
|
||||
}
|
||||
|
||||
public async Task InternalError()
|
||||
{
|
||||
await LogToConsole("An unhandled error occured performing action");
|
||||
|
||||
Logger.LogInformation("Reporting or smth");
|
||||
}
|
||||
}
|
||||
@@ -35,31 +35,36 @@ public partial class Server
|
||||
|
||||
// Setup transitions
|
||||
StateMachine.Configure(ServerState.Offline)
|
||||
.Permit(ServerTrigger.Start, ServerState.Starting)
|
||||
.Permit(ServerTrigger.Reinstall, ServerState.Installing);
|
||||
.Permit(ServerTrigger.Start, ServerState.Starting) // Allow to start
|
||||
.Permit(ServerTrigger.Reinstall, ServerState.Installing) // Allow to install
|
||||
.OnEntryFromAsync(ServerTrigger.NotifyInternalError, InternalError); // Handle unhandled errors
|
||||
|
||||
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);
|
||||
.Permit(ServerTrigger.Stop, ServerState.Stopping) // Allow stopping
|
||||
.Permit(ServerTrigger.NotifyOnline, ServerState.Online) // Allow the server to report as online
|
||||
.Permit(ServerTrigger.NotifyRuntimeContainerDied, ServerState.Offline) // Allow server to handle container death
|
||||
.Permit(ServerTrigger.NotifyInternalError, ServerState.Offline) // Error handling for the action below
|
||||
.OnEntryAsync(InternalStart) // Perform start action
|
||||
.OnExitFromAsync(ServerTrigger.NotifyRuntimeContainerDied, InternalCrash); // Define a runtime container death as a crash
|
||||
|
||||
StateMachine.Configure(ServerState.Online)
|
||||
.Permit(ServerTrigger.NotifyRuntimeContainerDied, ServerState.Offline)
|
||||
.Permit(ServerTrigger.Stop, ServerState.Stopping)
|
||||
.OnExitFromAsync(ServerTrigger.NotifyRuntimeContainerDied, InternalCrash);
|
||||
.Permit(ServerTrigger.Stop, ServerState.Stopping) // Allow stopping
|
||||
.Permit(ServerTrigger.NotifyRuntimeContainerDied, ServerState.Offline) // Allow server to handle container death
|
||||
.OnExitFromAsync(ServerTrigger.NotifyRuntimeContainerDied, InternalCrash); // Define a runtime container death as a crash
|
||||
|
||||
StateMachine.Configure(ServerState.Stopping)
|
||||
.Permit(ServerTrigger.NotifyRuntimeContainerDied, ServerState.Offline)
|
||||
.Permit(ServerTrigger.Kill, ServerState.Offline)
|
||||
.OnEntryAsync(InternalStop)
|
||||
.OnExitFromAsync(ServerTrigger.NotifyRuntimeContainerDied, InternalFinishStop);
|
||||
.PermitReentry(ServerTrigger.Kill) // Allow killing, will return to stopping to trigger kill and handle the death correctly
|
||||
.Permit(ServerTrigger.NotifyRuntimeContainerDied, ServerState.Offline) // Allow server to handle container death
|
||||
.Permit(ServerTrigger.NotifyInternalError, ServerState.Offline) // Error handling for the actions below
|
||||
.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)
|
||||
.Permit(ServerTrigger.NotifyInstallationContainerDied, ServerState.Offline)
|
||||
.OnEntryAsync(InternalInstall)
|
||||
.OnExitAsync(InternalFinishInstall);
|
||||
.Permit(ServerTrigger.NotifyInstallationContainerDied, ServerState.Offline) // Allow server to handle container death
|
||||
.Permit(ServerTrigger.NotifyInternalError, ServerState.Offline) // Error handling for the action below
|
||||
.OnEntryAsync(InternalInstall) // Perform install action
|
||||
.OnExitFromAsync(ServerTrigger.NotifyInstallationContainerDied, InternalFinishInstall); // Define the death of the installation container as successful
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -14,69 +14,79 @@ public partial class Server
|
||||
|
||||
private async Task InternalInstall()
|
||||
{
|
||||
// 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
|
||||
// and the container has the auto remove flag enabled by default (maybe also consider this for the normal runtime container)
|
||||
|
||||
await LogToConsole("Fetching installation configuration");
|
||||
|
||||
// Fetching remote configuration
|
||||
var remoteService = ServiceProvider.GetRequiredService<RemoteService>();
|
||||
using var remoteHttpClient = await remoteService.CreateHttpClient();
|
||||
|
||||
var installData = await remoteHttpClient.GetJson<ServerInstallDataResponse>($"api/servers/remote/servers/{Configuration.Id}/install");
|
||||
|
||||
var dockerImageService = ServiceProvider.GetRequiredService<DockerImageService>();
|
||||
|
||||
// We call an external service for that, as we want to have a central management point of images
|
||||
// for analytics and automatic deletion
|
||||
await dockerImageService.Ensure(installData.DockerImage, async message => { await LogToConsole(message); });
|
||||
|
||||
// Ensuring storage configuration
|
||||
var installationHostPath = await EnsureInstallationVolume();
|
||||
var runtimeHostPath = await EnsureRuntimeVolume();
|
||||
|
||||
// Write installation script to path
|
||||
var content = installData.Script.Replace("\r\n", "\n");
|
||||
await File.WriteAllTextAsync(PathBuilder.File(installationHostPath, "install.sh"), content);
|
||||
|
||||
// Creating container configuration
|
||||
var parameters = Configuration.ToInstallationCreateParameters(
|
||||
runtimeHostPath,
|
||||
installationHostPath,
|
||||
InstallationContainerName,
|
||||
installData.DockerImage,
|
||||
installData.Shell
|
||||
);
|
||||
|
||||
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
|
||||
|
||||
// Ensure we can actually spawn the container
|
||||
|
||||
try
|
||||
{
|
||||
var existingContainer = await dockerClient.Containers.InspectContainerAsync(InstallationContainerName);
|
||||
// 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
|
||||
// and the container has the auto remove flag enabled by default (maybe also consider this for the normal runtime container)
|
||||
|
||||
await LogToConsole("Fetching installation configuration");
|
||||
|
||||
// Fetching remote configuration
|
||||
var remoteService = ServiceProvider.GetRequiredService<RemoteService>();
|
||||
using var remoteHttpClient = await remoteService.CreateHttpClient();
|
||||
|
||||
var installData =
|
||||
await remoteHttpClient.GetJson<ServerInstallDataResponse>(
|
||||
$"api/servers/remote/servers/{Configuration.Id}/install");
|
||||
|
||||
var dockerImageService = ServiceProvider.GetRequiredService<DockerImageService>();
|
||||
|
||||
// We call an external service for that, as we want to have a central management point of images
|
||||
// for analytics and automatic deletion
|
||||
await dockerImageService.Ensure(installData.DockerImage, async message => { await LogToConsole(message); });
|
||||
|
||||
// Ensuring storage configuration
|
||||
var installationHostPath = await EnsureInstallationVolume();
|
||||
var runtimeHostPath = await EnsureRuntimeVolume();
|
||||
|
||||
// Write installation script to path
|
||||
var content = installData.Script.Replace("\r\n", "\n");
|
||||
await File.WriteAllTextAsync(PathBuilder.File(installationHostPath, "install.sh"), content);
|
||||
|
||||
// Creating container configuration
|
||||
var parameters = Configuration.ToInstallationCreateParameters(
|
||||
runtimeHostPath,
|
||||
installationHostPath,
|
||||
InstallationContainerName,
|
||||
installData.DockerImage,
|
||||
installData.Shell
|
||||
);
|
||||
|
||||
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
|
||||
|
||||
// Ensure we can actually spawn the container
|
||||
|
||||
try
|
||||
{
|
||||
var existingContainer = await dockerClient.Containers.InspectContainerAsync(InstallationContainerName);
|
||||
|
||||
// Perform automatic cleanup / restore
|
||||
|
||||
if (existingContainer.State.Running)
|
||||
await dockerClient.Containers.KillContainerAsync(existingContainer.ID, new());
|
||||
|
||||
await dockerClient.Containers.RemoveContainerAsync(existingContainer.ID, new());
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
|
||||
// Perform automatic cleanup / restore
|
||||
|
||||
if (existingContainer.State.Running)
|
||||
await dockerClient.Containers.KillContainerAsync(existingContainer.ID, new());
|
||||
|
||||
await dockerClient.Containers.RemoveContainerAsync(existingContainer.ID, new());
|
||||
// Spawn the container
|
||||
|
||||
var container = await dockerClient.Containers.CreateContainerAsync(parameters);
|
||||
InstallationContainerId = container.ID;
|
||||
|
||||
await AttachConsole(InstallationContainerId);
|
||||
|
||||
await dockerClient.Containers.StartContainerAsync(InstallationContainerId, new());
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
catch (Exception e)
|
||||
{
|
||||
// Ignored
|
||||
Logger.LogError("An error occured while performing install trigger: {e}", e);
|
||||
await StateMachine.FireAsync(ServerTrigger.NotifyInternalError);
|
||||
}
|
||||
|
||||
// Spawn the container
|
||||
|
||||
var container = await dockerClient.Containers.CreateContainerAsync(parameters);
|
||||
InstallationContainerId = container.ID;
|
||||
|
||||
await AttachConsole(InstallationContainerId);
|
||||
|
||||
await dockerClient.Containers.StartContainerAsync(InstallationContainerId, new());
|
||||
}
|
||||
|
||||
private async Task InternalFinishInstall()
|
||||
|
||||
28
MoonlightServers.Daemon/Abstractions/Server.Kill.cs
Normal file
28
MoonlightServers.Daemon/Abstractions/Server.Kill.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,15 +9,23 @@ public partial class Server
|
||||
|
||||
private async Task InternalStart()
|
||||
{
|
||||
await ReCreate();
|
||||
try
|
||||
{
|
||||
await ReCreate();
|
||||
|
||||
await LogToConsole("Starting container");
|
||||
await LogToConsole("Starting container");
|
||||
|
||||
// We can disable the null check for the runtime container id, as we set it by calling ReCreate();
|
||||
await AttachConsole(RuntimeContainerId!);
|
||||
|
||||
// Start container
|
||||
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
|
||||
await dockerClient.Containers.StartContainerAsync(RuntimeContainerId, new());
|
||||
// We can disable the null check for the runtime container id, as we set it by calling ReCreate();
|
||||
await AttachConsole(RuntimeContainerId!);
|
||||
|
||||
// Start container
|
||||
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,15 @@ public partial class Server
|
||||
|
||||
private async Task InternalStop()
|
||||
{
|
||||
await Console.WriteToInput($"{Configuration.StopCommand}\n\r");
|
||||
try
|
||||
{
|
||||
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()
|
||||
|
||||
@@ -9,5 +9,6 @@ public enum ServerTrigger
|
||||
Reinstall = 4,
|
||||
NotifyOnline = 5,
|
||||
NotifyRuntimeContainerDied = 6,
|
||||
NotifyInstallationContainerDied = 7
|
||||
NotifyInstallationContainerDied = 7,
|
||||
NotifyInternalError = 8
|
||||
}
|
||||
@@ -17,7 +17,7 @@ public class ServerPowerController : Controller
|
||||
}
|
||||
|
||||
[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);
|
||||
|
||||
@@ -28,7 +28,7 @@ public class ServerPowerController : Controller
|
||||
}
|
||||
|
||||
[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);
|
||||
|
||||
@@ -39,7 +39,7 @@ public class ServerPowerController : Controller
|
||||
}
|
||||
|
||||
[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);
|
||||
|
||||
@@ -48,4 +48,15 @@ public class ServerPowerController : Controller
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -26,31 +26,31 @@
|
||||
/* Colors */
|
||||
|
||||
.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 {
|
||||
@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 {
|
||||
@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 {
|
||||
@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 {
|
||||
@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 {
|
||||
@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 {
|
||||
@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 */
|
||||
@@ -77,4 +77,69 @@
|
||||
|
||||
.btn-outline-success {
|
||||
@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;
|
||||
}
|
||||
@@ -119,6 +119,9 @@ module.exports = {
|
||||
950: '#0e121c',
|
||||
}
|
||||
},
|
||||
textColor:{
|
||||
diffcolor: 'rgb(var(--color-diffcolor, var(--color-light)))'
|
||||
},
|
||||
animation: {
|
||||
'shimmer': 'shimmer 2s linear infinite',
|
||||
}
|
||||
|
||||
@@ -74,16 +74,38 @@
|
||||
<span class="align-middle">Start</span>
|
||||
</button>
|
||||
}
|
||||
<button type="button" class="btn btn-primary">
|
||||
<i class="icon-rotate-ccw me-1 align-middle"></i>
|
||||
<span class="align-middle">Restart</span>
|
||||
</button>
|
||||
@if (State == ServerState.Starting || State == ServerState.Online)
|
||||
|
||||
@if (State == ServerState.Online)
|
||||
{
|
||||
<WButton CssClasses="btn btn-danger" OnClick="_ => Stop()">
|
||||
<i class="icon-squircle me-1 align-middle"></i>
|
||||
<span class="align-middle">Stop</span>
|
||||
</WButton>
|
||||
<button type="button" class="btn btn-primary">
|
||||
<i class="icon-rotate-ccw me-1 align-middle"></i>
|
||||
<span class="align-middle">Restart</span>
|
||||
</button>
|
||||
}
|
||||
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()">
|
||||
<i class="icon-squircle me-1 align-middle"></i>
|
||||
<span class="align-middle">Stop</span>
|
||||
</WButton>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -230,14 +252,13 @@
|
||||
}
|
||||
|
||||
private async Task Start()
|
||||
{
|
||||
await ApiClient.Post($"api/servers/{Server.Id}/start");
|
||||
}
|
||||
=> await ApiClient.Post($"api/servers/{Server.Id}/start");
|
||||
|
||||
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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user