Re-implemented server state machine. Cleaned up code
TODO: Handle trigger errors
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -429,3 +429,6 @@ core.min.css
|
|||||||
# Build script for nuget packages
|
# Build script for nuget packages
|
||||||
finalPackages/
|
finalPackages/
|
||||||
nupkgs/
|
nupkgs/
|
||||||
|
|
||||||
|
# Local daemon tests
|
||||||
|
**/data/volumes/**
|
||||||
@@ -1,20 +1,21 @@
|
|||||||
using MoonlightServers.DaemonShared.Enums;
|
using MoonlightServers.DaemonShared.Enums;
|
||||||
using MoonlightServers.Shared.Enums;
|
using MoonlightServers.Shared.Enums;
|
||||||
|
using ServerState = MoonlightServers.Shared.Enums.ServerState;
|
||||||
|
|
||||||
namespace MoonlightServers.ApiServer.Extensions;
|
namespace MoonlightServers.ApiServer.Extensions;
|
||||||
|
|
||||||
public static class ServerStateExtensions
|
public static class ServerStateExtensions
|
||||||
{
|
{
|
||||||
public static ServerPowerState ToServerPowerState(this ServerState state)
|
public static ServerState ToServerPowerState(this DaemonShared.Enums.ServerState state)
|
||||||
{
|
{
|
||||||
return state switch
|
return state switch
|
||||||
{
|
{
|
||||||
ServerState.Installing => ServerPowerState.Installing,
|
DaemonShared.Enums.ServerState.Installing => ServerState.Installing,
|
||||||
ServerState.Stopping => ServerPowerState.Stopping,
|
DaemonShared.Enums.ServerState.Stopping => ServerState.Stopping,
|
||||||
ServerState.Online => ServerPowerState.Online,
|
DaemonShared.Enums.ServerState.Online => ServerState.Online,
|
||||||
ServerState.Starting => ServerPowerState.Starting,
|
DaemonShared.Enums.ServerState.Starting => ServerState.Starting,
|
||||||
ServerState.Offline => ServerPowerState.Offline,
|
DaemonShared.Enums.ServerState.Offline => ServerState.Offline,
|
||||||
_ => ServerPowerState.Offline
|
_ => ServerState.Offline
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using MoonCore.Extended.PermFilter;
|
using MoonCore.Extended.PermFilter;
|
||||||
@@ -28,7 +29,7 @@ public class ServersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[RequirePermission("meta.authenticated")]
|
[Authorize]
|
||||||
public async Task<PagedData<ServerDetailResponse>> GetAll([FromQuery] int page, [FromQuery] int pageSize)
|
public async Task<PagedData<ServerDetailResponse>> GetAll([FromQuery] int page, [FromQuery] int pageSize)
|
||||||
{
|
{
|
||||||
var userIdClaim = User.Claims.First(x => x.Type == "userId");
|
var userIdClaim = User.Claims.First(x => x.Type == "userId");
|
||||||
@@ -69,7 +70,7 @@ public class ServersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{serverId:int}")]
|
[HttpGet("{serverId:int}")]
|
||||||
[RequirePermission("meta.authenticated")]
|
[Authorize]
|
||||||
public async Task<ServerDetailResponse> Get([FromRoute] int serverId)
|
public async Task<ServerDetailResponse> Get([FromRoute] int serverId)
|
||||||
{
|
{
|
||||||
var server = await GetServerWithPermCheck(
|
var server = await GetServerWithPermCheck(
|
||||||
@@ -96,7 +97,7 @@ public class ServersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{serverId:int}/status")]
|
[HttpGet("{serverId:int}/status")]
|
||||||
[RequirePermission("meta.authenticated")]
|
[Authorize]
|
||||||
public async Task<ServerStatusResponse> GetStatus([FromRoute] int serverId)
|
public async Task<ServerStatusResponse> GetStatus([FromRoute] int serverId)
|
||||||
{
|
{
|
||||||
var server = await GetServerWithPermCheck(serverId);
|
var server = await GetServerWithPermCheck(serverId);
|
||||||
@@ -111,7 +112,7 @@ public class ServersController : Controller
|
|||||||
|
|
||||||
return new ServerStatusResponse()
|
return new ServerStatusResponse()
|
||||||
{
|
{
|
||||||
PowerState = data.State.ToServerPowerState()
|
State = data.State.ToServerPowerState()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch (HttpRequestException e)
|
catch (HttpRequestException e)
|
||||||
@@ -120,9 +121,9 @@ public class ServersController : Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{serverId:int}/console")]
|
[HttpGet("{serverId:int}/ws")]
|
||||||
[RequirePermission("meta.authenticated")]
|
[Authorize]
|
||||||
public async Task<ServerConsoleResponse> GetConsole([FromRoute] int serverId)
|
public async Task<ServerWebSocketResponse> GetWebSocket([FromRoute] int serverId)
|
||||||
{
|
{
|
||||||
var server = await GetServerWithPermCheck(serverId);
|
var server = await GetServerWithPermCheck(serverId);
|
||||||
|
|
||||||
@@ -130,7 +131,7 @@ public class ServersController : Controller
|
|||||||
|
|
||||||
var accessToken = NodeService.CreateAccessToken(server.Node, parameters =>
|
var accessToken = NodeService.CreateAccessToken(server.Node, parameters =>
|
||||||
{
|
{
|
||||||
parameters.Add("type", "console");
|
parameters.Add("type", "websocket");
|
||||||
parameters.Add("serverId", server.Id);
|
parameters.Add("serverId", server.Id);
|
||||||
}, TimeSpan.FromMinutes(10));
|
}, TimeSpan.FromMinutes(10));
|
||||||
|
|
||||||
@@ -141,9 +142,9 @@ public class ServersController : Controller
|
|||||||
else
|
else
|
||||||
url += "http://";
|
url += "http://";
|
||||||
|
|
||||||
url += $"{server.Node.Fqdn}:{server.Node.HttpPort}/api/servers/console";
|
url += $"{server.Node.Fqdn}:{server.Node.HttpPort}/api/servers/ws";
|
||||||
|
|
||||||
return new ServerConsoleResponse()
|
return new ServerWebSocketResponse()
|
||||||
{
|
{
|
||||||
Target = url,
|
Target = url,
|
||||||
AccessToken = accessToken
|
AccessToken = accessToken
|
||||||
@@ -151,7 +152,7 @@ public class ServersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{serverId:int}/logs")]
|
[HttpGet("{serverId:int}/logs")]
|
||||||
[RequirePermission("meta.authenticated")]
|
[Authorize]
|
||||||
public async Task<ServerLogsResponse> GetLogs([FromRoute] int serverId)
|
public async Task<ServerLogsResponse> GetLogs([FromRoute] int serverId)
|
||||||
{
|
{
|
||||||
var server = await GetServerWithPermCheck(serverId);
|
var server = await GetServerWithPermCheck(serverId);
|
||||||
@@ -175,6 +176,42 @@ public class ServersController : Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("{serverId:int}/start")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task Start([FromRoute] int serverId)
|
||||||
|
{
|
||||||
|
var server = await GetServerWithPermCheck(serverId);
|
||||||
|
|
||||||
|
using var apiClient = await NodeService.CreateApiClient(server.Node);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await apiClient.Post($"api/servers/{server.Id}/start");
|
||||||
|
}
|
||||||
|
catch (HttpRequestException e)
|
||||||
|
{
|
||||||
|
throw new HttpApiException("Unable to access the node the server is running on", 502);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{serverId:int}/stop")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task Stop([FromRoute] int serverId)
|
||||||
|
{
|
||||||
|
var server = await GetServerWithPermCheck(serverId);
|
||||||
|
|
||||||
|
using var apiClient = await NodeService.CreateApiClient(server.Node);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await apiClient.Post($"api/servers/{server.Id}/stop");
|
||||||
|
}
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -372,11 +372,16 @@ public class StarImportExportService
|
|||||||
|
|
||||||
foreach (var pConfigFind in pConfig.Value.GetProperty("find").EnumerateObject())
|
foreach (var pConfigFind in pConfig.Value.GetProperty("find").EnumerateObject())
|
||||||
{
|
{
|
||||||
pc.Entries.Add(new ParseConfiguration.ParseConfigurationEntry()
|
var entry = new ParseConfiguration.ParseConfigurationEntry()
|
||||||
{
|
{
|
||||||
Key = pConfigFind.Name,
|
Key = pConfigFind.Name,
|
||||||
Value = pConfigFind.Value.GetString() ?? "Parse error"
|
Value = pConfigFind.Value.GetString() ?? "Parse error"
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Fix up special variables
|
||||||
|
entry.Value = entry.Value.Replace("server.allocations.default.port", "SERVER_PORT");
|
||||||
|
|
||||||
|
pc.Entries.Add(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
resultPcs.Add(pc);
|
resultPcs.Add(pc);
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Docker.DotNet;
|
using Docker.DotNet;
|
||||||
using Docker.DotNet.Models;
|
using Docker.DotNet.Models;
|
||||||
using MoonlightServers.Daemon.Models;
|
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Extensions.ServerExtensions;
|
namespace MoonlightServers.Daemon.Abstractions;
|
||||||
|
|
||||||
public static class ServerConsoleExtensions
|
public partial class Server
|
||||||
{
|
{
|
||||||
public static async Task Attach(this Server server)
|
private async Task AttachConsole(string containerId)
|
||||||
{
|
{
|
||||||
var dockerClient = server.ServiceProvider.GetRequiredService<DockerClient>();
|
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
|
||||||
|
|
||||||
var stream = await dockerClient.Containers.AttachContainerAsync(server.ContainerId, true,
|
var stream = await dockerClient.Containers.AttachContainerAsync(containerId, true,
|
||||||
new ContainerAttachParameters()
|
new ContainerAttachParameters()
|
||||||
{
|
{
|
||||||
Stderr = true,
|
Stderr = true,
|
||||||
@@ -19,12 +18,13 @@ public static class ServerConsoleExtensions
|
|||||||
Stdout = true,
|
Stdout = true,
|
||||||
Stream = true
|
Stream = true
|
||||||
},
|
},
|
||||||
server.Cancellation.Token
|
Cancellation.Token
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Reading
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
while (!server.Cancellation.Token.IsCancellationRequested)
|
while (!Cancellation.Token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -34,10 +34,10 @@ public static class ServerConsoleExtensions
|
|||||||
buffer,
|
buffer,
|
||||||
0,
|
0,
|
||||||
buffer.Length,
|
buffer.Length,
|
||||||
server.Cancellation.Token
|
Cancellation.Token
|
||||||
);
|
);
|
||||||
|
|
||||||
if(readResult.EOF)
|
if (readResult.EOF)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
var resizedBuffer = new byte[readResult.Count];
|
var resizedBuffer = new byte[readResult.Count];
|
||||||
@@ -45,7 +45,7 @@ public static class ServerConsoleExtensions
|
|||||||
buffer = new byte[buffer.Length];
|
buffer = new byte[buffer.Length];
|
||||||
|
|
||||||
var decodedText = Encoding.UTF8.GetString(resizedBuffer);
|
var decodedText = Encoding.UTF8.GetString(resizedBuffer);
|
||||||
await server.Console.WriteToOutput(decodedText);
|
await Console.WriteToOutput(decodedText);
|
||||||
}
|
}
|
||||||
catch (TaskCanceledException)
|
catch (TaskCanceledException)
|
||||||
{
|
{
|
||||||
@@ -57,9 +57,24 @@ public static class ServerConsoleExtensions
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
server.Logger.LogWarning("An unhandled error occured while reading from container stream: {e}", e);
|
Logger.LogWarning("An unhandled error occured while reading from container stream: {e}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Writing
|
||||||
|
Console.OnInput += async content =>
|
||||||
|
{
|
||||||
|
var contentBuffer = Encoding.UTF8.GetBytes(content);
|
||||||
|
await stream.WriteAsync(contentBuffer, 0, contentBuffer.Length, Cancellation.Token);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task LogToConsole(string message)
|
||||||
|
{
|
||||||
|
await Console.WriteToOutput($"\x1b[38;5;16;48;5;135m\x1b[39m\x1b[1m Moonlight \x1b[0m {message}\n\r");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string[]> GetConsoleMessages()
|
||||||
|
=> Task.FromResult(Console.Messages);
|
||||||
}
|
}
|
||||||
37
MoonlightServers.Daemon/Abstractions/Server.Create.cs
Normal file
37
MoonlightServers.Daemon/Abstractions/Server.Create.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using Docker.DotNet;
|
||||||
|
using MoonlightServers.Daemon.Extensions;
|
||||||
|
using MoonlightServers.Daemon.Services;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Daemon.Abstractions;
|
||||||
|
|
||||||
|
public partial class Server
|
||||||
|
{
|
||||||
|
private async Task Create()
|
||||||
|
{
|
||||||
|
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(Configuration.DockerImage, async message => { await LogToConsole(message); });
|
||||||
|
|
||||||
|
var hostPath = await EnsureRuntimeVolume();
|
||||||
|
|
||||||
|
await LogToConsole("Creating container");
|
||||||
|
|
||||||
|
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
|
||||||
|
|
||||||
|
var parameters = Configuration.ToRuntimeCreateParameters(
|
||||||
|
hostPath: hostPath,
|
||||||
|
containerName: RuntimeContainerName
|
||||||
|
);
|
||||||
|
|
||||||
|
var container = await dockerClient.Containers.CreateContainerAsync(parameters);
|
||||||
|
RuntimeContainerId = container.ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReCreate()
|
||||||
|
{
|
||||||
|
await Destroy();
|
||||||
|
await Create();
|
||||||
|
}
|
||||||
|
}
|
||||||
50
MoonlightServers.Daemon/Abstractions/Server.Destroy.cs
Normal file
50
MoonlightServers.Daemon/Abstractions/Server.Destroy.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
using Docker.DotNet;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Daemon.Abstractions;
|
||||||
|
|
||||||
|
public partial class Server
|
||||||
|
{
|
||||||
|
private async Task Destroy()
|
||||||
|
{
|
||||||
|
// Note: This only destroys the container, it doesn't delete any data
|
||||||
|
|
||||||
|
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var container = await dockerClient.Containers.InspectContainerAsync(
|
||||||
|
RuntimeContainerName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (container.State.Running)
|
||||||
|
{
|
||||||
|
// Stop container when running
|
||||||
|
|
||||||
|
await LogToConsole("Stopping container");
|
||||||
|
|
||||||
|
await dockerClient.Containers.StopContainerAsync(container.ID, new()
|
||||||
|
{
|
||||||
|
WaitBeforeKillSeconds = 30 // TODO: Config
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await LogToConsole("Removing container");
|
||||||
|
await dockerClient.Containers.RemoveContainerAsync(container.ID, new());
|
||||||
|
}
|
||||||
|
catch (DockerContainerNotFoundException){}
|
||||||
|
|
||||||
|
// Canceling server tasks & listeners
|
||||||
|
await CancelTasks();
|
||||||
|
|
||||||
|
// and recreating cancellation token
|
||||||
|
Cancellation = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CancelTasks()
|
||||||
|
{
|
||||||
|
// Note: This will keep the docker container running, it will just cancel the server cancellation token
|
||||||
|
|
||||||
|
if (!Cancellation.IsCancellationRequested)
|
||||||
|
await Cancellation.CancelAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
8
MoonlightServers.Daemon/Abstractions/Server.Notify.cs
Normal file
8
MoonlightServers.Daemon/Abstractions/Server.Notify.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using MoonlightServers.Daemon.Enums;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Daemon.Abstractions;
|
||||||
|
|
||||||
|
public partial class Server
|
||||||
|
{
|
||||||
|
public async Task NotifyContainerDied() => await StateMachine.FireAsync(ServerTrigger.NotifyContainerDied);
|
||||||
|
}
|
||||||
23
MoonlightServers.Daemon/Abstractions/Server.Start.cs
Normal file
23
MoonlightServers.Daemon/Abstractions/Server.Start.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Docker.DotNet;
|
||||||
|
using MoonlightServers.Daemon.Enums;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Daemon.Abstractions;
|
||||||
|
|
||||||
|
public partial class Server
|
||||||
|
{
|
||||||
|
public async Task Start() => await StateMachine.FireAsync(ServerTrigger.Start);
|
||||||
|
|
||||||
|
private async Task InternalStart()
|
||||||
|
{
|
||||||
|
await ReCreate();
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
13
MoonlightServers.Daemon/Abstractions/Server.Stop.cs
Normal file
13
MoonlightServers.Daemon/Abstractions/Server.Stop.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using MoonlightServers.Daemon.Enums;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Daemon.Abstractions;
|
||||||
|
|
||||||
|
public partial class Server
|
||||||
|
{
|
||||||
|
public async Task Stop() => await StateMachine.FireAsync(ServerTrigger.Stop);
|
||||||
|
|
||||||
|
private async Task InternalStop()
|
||||||
|
{
|
||||||
|
await Console.WriteToInput($"{Configuration.StopCommand}\n\r");
|
||||||
|
}
|
||||||
|
}
|
||||||
55
MoonlightServers.Daemon/Abstractions/Server.Storage.cs
Normal file
55
MoonlightServers.Daemon/Abstractions/Server.Storage.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using MoonCore.Helpers;
|
||||||
|
using MoonlightServers.Daemon.Configuration;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Daemon.Abstractions;
|
||||||
|
|
||||||
|
public partial class Server
|
||||||
|
{
|
||||||
|
private async Task<string> EnsureRuntimeVolume()
|
||||||
|
{
|
||||||
|
var appConfiguration = ServiceProvider.GetRequiredService<AppConfiguration>();
|
||||||
|
|
||||||
|
var hostPath = PathBuilder.Dir(
|
||||||
|
appConfiguration.Storage.Volumes,
|
||||||
|
Configuration.Id.ToString()
|
||||||
|
);
|
||||||
|
|
||||||
|
await LogToConsole("Creating storage");
|
||||||
|
|
||||||
|
// TODO: Virtual disk
|
||||||
|
|
||||||
|
// Create volume if missing
|
||||||
|
if (!Directory.Exists(hostPath))
|
||||||
|
Directory.CreateDirectory(hostPath);
|
||||||
|
|
||||||
|
if (hostPath.StartsWith("/"))
|
||||||
|
return hostPath;
|
||||||
|
else
|
||||||
|
return PathBuilder.JoinPaths(Directory.GetCurrentDirectory(), hostPath);
|
||||||
|
|
||||||
|
return hostPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> EnsureInstallationVolume()
|
||||||
|
{
|
||||||
|
var appConfiguration = ServiceProvider.GetRequiredService<AppConfiguration>();
|
||||||
|
|
||||||
|
var hostPath = PathBuilder.Dir(
|
||||||
|
appConfiguration.Storage.Volumes,
|
||||||
|
Configuration.Id.ToString()
|
||||||
|
);
|
||||||
|
|
||||||
|
await LogToConsole("Creating storage");
|
||||||
|
|
||||||
|
// Create volume if missing
|
||||||
|
if (!Directory.Exists(hostPath))
|
||||||
|
Directory.CreateDirectory(hostPath);
|
||||||
|
|
||||||
|
if (hostPath.StartsWith("/"))
|
||||||
|
return hostPath;
|
||||||
|
else
|
||||||
|
return PathBuilder.JoinPaths(Directory.GetCurrentDirectory(), hostPath);
|
||||||
|
|
||||||
|
return hostPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
52
MoonlightServers.Daemon/Abstractions/Server.cs
Normal file
52
MoonlightServers.Daemon/Abstractions/Server.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using Docker.DotNet.Models;
|
||||||
|
using MoonlightServers.Daemon.Enums;
|
||||||
|
using MoonlightServers.Daemon.Models;
|
||||||
|
using MoonlightServers.Daemon.Models.Cache;
|
||||||
|
using Stateless;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Daemon.Abstractions;
|
||||||
|
|
||||||
|
public partial class Server
|
||||||
|
{
|
||||||
|
// Exposed configuration/state values
|
||||||
|
public int Id => Configuration.Id;
|
||||||
|
public ServerState State => StateMachine.State;
|
||||||
|
|
||||||
|
// Exposed container names and ids
|
||||||
|
public string RuntimeContainerName { get; private set; }
|
||||||
|
public string? RuntimeContainerId { get; private set; }
|
||||||
|
|
||||||
|
public string InstallationContainerName { get; private set; }
|
||||||
|
public string? InstallationContainerId { get; private set; }
|
||||||
|
|
||||||
|
// Events
|
||||||
|
public event Func<ServerState, Task> OnStateChanged;
|
||||||
|
public event Func<string, Task> OnConsoleOutput;
|
||||||
|
|
||||||
|
// Private stuff
|
||||||
|
|
||||||
|
private readonly ILogger Logger;
|
||||||
|
private readonly IServiceProvider ServiceProvider;
|
||||||
|
private readonly ServerConsole Console;
|
||||||
|
|
||||||
|
private StateMachine<ServerState, ServerTrigger> StateMachine;
|
||||||
|
private ServerConfiguration Configuration;
|
||||||
|
private CancellationTokenSource Cancellation;
|
||||||
|
|
||||||
|
public Server(
|
||||||
|
ILogger logger,
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
ServerConfiguration configuration
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Logger = logger;
|
||||||
|
ServiceProvider = serviceProvider;
|
||||||
|
Configuration = configuration;
|
||||||
|
|
||||||
|
Console = new();
|
||||||
|
Cancellation = new();
|
||||||
|
|
||||||
|
RuntimeContainerName = $"moonlight-runtime-{Configuration.Id}";
|
||||||
|
InstallationContainerName = $"moonlight-install-{Configuration.Id}";
|
||||||
|
}
|
||||||
|
}
|
||||||
10
MoonlightServers.Daemon/Enums/ServerState.cs
Normal file
10
MoonlightServers.Daemon/Enums/ServerState.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace MoonlightServers.Daemon.Enums;
|
||||||
|
|
||||||
|
public enum ServerState
|
||||||
|
{
|
||||||
|
Offline = 0,
|
||||||
|
Starting = 1,
|
||||||
|
Online = 2,
|
||||||
|
Stopping = 3,
|
||||||
|
Installing = 4
|
||||||
|
}
|
||||||
13
MoonlightServers.Daemon/Enums/ServerTrigger.cs
Normal file
13
MoonlightServers.Daemon/Enums/ServerTrigger.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace MoonlightServers.Daemon.Enums;
|
||||||
|
|
||||||
|
public enum ServerTrigger
|
||||||
|
{
|
||||||
|
Start = 0,
|
||||||
|
Stop = 1,
|
||||||
|
Restart = 2,
|
||||||
|
Kill = 3,
|
||||||
|
Reinstall = 4,
|
||||||
|
NotifyOnline = 5,
|
||||||
|
NotifyContainerDied = 6,
|
||||||
|
NotifyInstallContainerDied = 7
|
||||||
|
}
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
using Docker.DotNet.Models;
|
using Docker.DotNet.Models;
|
||||||
using Mono.Unix.Native;
|
using Mono.Unix.Native;
|
||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
using MoonlightServers.Daemon.Models;
|
using MoonlightServers.Daemon.Models.Cache;
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Extensions.ServerExtensions;
|
namespace MoonlightServers.Daemon.Extensions;
|
||||||
|
|
||||||
public static class ServerConfigExtensions
|
public static class ServerConfigurationExtensions
|
||||||
{
|
{
|
||||||
public static CreateContainerParameters GetRuntimeContainerParameters(this Server server)
|
public static CreateContainerParameters ToRuntimeCreateParameters(this ServerConfiguration configuration, string hostPath, string containerName)
|
||||||
{
|
{
|
||||||
var parameters = server.GetSharedContainerParameters();
|
var parameters = configuration.ToSharedCreateParameters();
|
||||||
|
|
||||||
#region Security
|
#region Security
|
||||||
|
|
||||||
@@ -29,20 +29,20 @@ public static class ServerConfigExtensions
|
|||||||
|
|
||||||
#region Name
|
#region Name
|
||||||
|
|
||||||
parameters.Name = server.RuntimeContainerName;
|
parameters.Name = containerName;
|
||||||
parameters.Hostname = server.RuntimeContainerName;
|
parameters.Hostname = containerName;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Docker Image
|
#region Docker Image
|
||||||
|
|
||||||
parameters.Image = server.Configuration.DockerImage;
|
parameters.Image = configuration.DockerImage;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Environment
|
#region Environment
|
||||||
|
|
||||||
parameters.Env = server.ConstructEnv()
|
parameters.Env = configuration.ToEnvironmentVariables()
|
||||||
.Select(x => $"{x.Key}={x.Value}")
|
.Select(x => $"{x.Key}={x.Value}")
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ public static class ServerConfigExtensions
|
|||||||
|
|
||||||
#region User
|
#region User
|
||||||
|
|
||||||
var userId = Syscall.getuid();
|
var userId = Syscall.getuid(); // TODO: Extract to external service?
|
||||||
|
|
||||||
if (userId == 0)
|
if (userId == 0)
|
||||||
{
|
{
|
||||||
@@ -78,7 +78,7 @@ public static class ServerConfigExtensions
|
|||||||
|
|
||||||
parameters.HostConfig.Mounts.Add(new()
|
parameters.HostConfig.Mounts.Add(new()
|
||||||
{
|
{
|
||||||
Source = server.RuntimeVolumePath,
|
Source = hostPath,
|
||||||
Target = "/home/container",
|
Target = "/home/container",
|
||||||
ReadOnly = false,
|
ReadOnly = false,
|
||||||
Type = "bind"
|
Type = "bind"
|
||||||
@@ -93,7 +93,7 @@ public static class ServerConfigExtensions
|
|||||||
parameters.ExposedPorts = new Dictionary<string, EmptyStruct>();
|
parameters.ExposedPorts = new Dictionary<string, EmptyStruct>();
|
||||||
parameters.HostConfig.PortBindings = new Dictionary<string, IList<PortBinding>>();
|
parameters.HostConfig.PortBindings = new Dictionary<string, IList<PortBinding>>();
|
||||||
|
|
||||||
foreach (var allocation in server.Configuration.Allocations)
|
foreach (var allocation in configuration.Allocations)
|
||||||
{
|
{
|
||||||
parameters.ExposedPorts.Add($"{allocation.Port}/tcp", new());
|
parameters.ExposedPorts.Add($"{allocation.Port}/tcp", new());
|
||||||
parameters.ExposedPorts.Add($"{allocation.Port}/udp", new());
|
parameters.ExposedPorts.Add($"{allocation.Port}/udp", new());
|
||||||
@@ -123,7 +123,7 @@ public static class ServerConfigExtensions
|
|||||||
return parameters;
|
return parameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CreateContainerParameters GetSharedContainerParameters(this Server server)
|
private static CreateContainerParameters ToSharedCreateParameters(this ServerConfiguration configuration)
|
||||||
{
|
{
|
||||||
var parameters = new CreateContainerParameters()
|
var parameters = new CreateContainerParameters()
|
||||||
{
|
{
|
||||||
@@ -142,7 +142,7 @@ public static class ServerConfigExtensions
|
|||||||
|
|
||||||
#region CPU
|
#region CPU
|
||||||
|
|
||||||
parameters.HostConfig.CPUQuota = server.Configuration.Cpu * 1000;
|
parameters.HostConfig.CPUQuota = configuration.Cpu * 1000;
|
||||||
parameters.HostConfig.CPUPeriod = 100000;
|
parameters.HostConfig.CPUPeriod = 100000;
|
||||||
parameters.HostConfig.CPUShares = 1024;
|
parameters.HostConfig.CPUShares = 1024;
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ public static class ServerConfigExtensions
|
|||||||
|
|
||||||
#region Memory & Swap
|
#region Memory & Swap
|
||||||
|
|
||||||
var memoryLimit = server.Configuration.Memory;
|
var memoryLimit = configuration.Memory;
|
||||||
|
|
||||||
// The overhead multiplier gives the container a little bit more memory to prevent crashes
|
// The overhead multiplier gives the container a little bit more memory to prevent crashes
|
||||||
var memoryOverhead = memoryLimit + (memoryLimit * 0.05f); // TODO: Config
|
var memoryOverhead = memoryLimit + (memoryLimit * 0.05f); // TODO: Config
|
||||||
@@ -183,6 +183,8 @@ public static class ServerConfigExtensions
|
|||||||
|
|
||||||
#region DNS
|
#region DNS
|
||||||
|
|
||||||
|
// TODO: Read hosts dns settings?
|
||||||
|
|
||||||
parameters.HostConfig.DNS = /*config.Docker.DnsServers.Any() ? config.Docker.DnsServers :*/ new List<string>()
|
parameters.HostConfig.DNS = /*config.Docker.DnsServers.Any() ? config.Docker.DnsServers :*/ new List<string>()
|
||||||
{
|
{
|
||||||
"1.1.1.1",
|
"1.1.1.1",
|
||||||
@@ -215,27 +217,25 @@ public static class ServerConfigExtensions
|
|||||||
parameters.Labels = new Dictionary<string, string>();
|
parameters.Labels = new Dictionary<string, string>();
|
||||||
|
|
||||||
parameters.Labels.Add("Software", "Moonlight-Panel");
|
parameters.Labels.Add("Software", "Moonlight-Panel");
|
||||||
parameters.Labels.Add("ServerId", server.Configuration.Id.ToString());
|
parameters.Labels.Add("ServerId", configuration.Id.ToString());
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
return parameters;
|
return parameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Dictionary<string, string> ConstructEnv(this Server server)
|
public static Dictionary<string, string> ToEnvironmentVariables(this ServerConfiguration configuration)
|
||||||
{
|
{
|
||||||
var config = server.Configuration;
|
|
||||||
|
|
||||||
var result = new Dictionary<string, string>
|
var result = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
//TODO: Add timezone, add server ip
|
//TODO: Add timezone, add server ip
|
||||||
{ "STARTUP", config.StartupCommand },
|
{ "STARTUP", configuration.StartupCommand },
|
||||||
{ "SERVER_MEMORY", config.Memory.ToString() }
|
{ "SERVER_MEMORY", configuration.Memory.ToString() }
|
||||||
};
|
};
|
||||||
|
|
||||||
if (config.Allocations.Length > 0)
|
if (configuration.Allocations.Length > 0)
|
||||||
{
|
{
|
||||||
var mainAllocation = config.Allocations.First();
|
var mainAllocation = configuration.Allocations.First();
|
||||||
|
|
||||||
result.Add("SERVER_IP", mainAllocation.IpAddress);
|
result.Add("SERVER_IP", mainAllocation.IpAddress);
|
||||||
result.Add("SERVER_PORT", mainAllocation.Port.ToString());
|
result.Add("SERVER_PORT", mainAllocation.Port.ToString());
|
||||||
@@ -243,14 +243,14 @@ public static class ServerConfigExtensions
|
|||||||
|
|
||||||
// Handle allocation variables
|
// Handle allocation variables
|
||||||
var i = 1;
|
var i = 1;
|
||||||
foreach (var allocation in config.Allocations)
|
foreach (var allocation in configuration.Allocations)
|
||||||
{
|
{
|
||||||
result.Add($"ML_PORT_{i}", allocation.Port.ToString());
|
result.Add($"ML_PORT_{i}", allocation.Port.ToString());
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy variables as env vars
|
// Copy variables as env vars
|
||||||
foreach (var variable in config.Variables)
|
foreach (var variable in configuration.Variables)
|
||||||
result.Add(variable.Key, variable.Value);
|
result.Add(variable.Key, variable.Value);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
using Docker.DotNet;
|
|
||||||
using MoonlightServers.Daemon.Models;
|
|
||||||
using MoonlightServers.DaemonShared.Enums;
|
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Extensions.ServerExtensions;
|
|
||||||
|
|
||||||
public static class ServerCreateExtensions
|
|
||||||
{
|
|
||||||
public static async Task Create(this Server server)
|
|
||||||
{
|
|
||||||
var dockerClient = server.ServiceProvider.GetRequiredService<DockerClient>();
|
|
||||||
|
|
||||||
// Ensure image is pulled
|
|
||||||
await server.EnsureDockerImage();
|
|
||||||
|
|
||||||
// Ensure runtime storage is created
|
|
||||||
await server.EnsureRuntimeStorage();
|
|
||||||
|
|
||||||
// Creating container
|
|
||||||
await server.NotifyTask(ServerTask.CreatingContainer);
|
|
||||||
|
|
||||||
var parameters = server.GetRuntimeContainerParameters();
|
|
||||||
var container = await dockerClient.Containers.CreateContainerAsync(parameters);
|
|
||||||
|
|
||||||
server.ContainerId = container.ID;
|
|
||||||
|
|
||||||
// Attach console
|
|
||||||
await server.Attach();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task ReCreate(this Server server)
|
|
||||||
{
|
|
||||||
await server.Destroy();
|
|
||||||
await server.Create();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
using Docker.DotNet;
|
|
||||||
using MoonlightServers.Daemon.Models;
|
|
||||||
using MoonlightServers.DaemonShared.Enums;
|
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Extensions.ServerExtensions;
|
|
||||||
|
|
||||||
public static class ServerDestroyExtensions
|
|
||||||
{
|
|
||||||
public static async Task Destroy(this Server server)
|
|
||||||
{
|
|
||||||
// Note: This only destroys the container, it doesn't delete any data
|
|
||||||
|
|
||||||
var dockerClient = server.ServiceProvider.GetRequiredService<DockerClient>();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var container = await dockerClient.Containers.InspectContainerAsync(
|
|
||||||
server.RuntimeContainerName
|
|
||||||
);
|
|
||||||
|
|
||||||
if (container.State.Running)
|
|
||||||
{
|
|
||||||
// Stop container when running
|
|
||||||
|
|
||||||
await server.NotifyTask(ServerTask.StoppingContainer);
|
|
||||||
|
|
||||||
await dockerClient.Containers.StopContainerAsync(container.ID, new()
|
|
||||||
{
|
|
||||||
WaitBeforeKillSeconds = 30 // TODO: Config
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await server.NotifyTask(ServerTask.RemovingContainer);
|
|
||||||
await dockerClient.Containers.RemoveContainerAsync(container.ID, new());
|
|
||||||
}
|
|
||||||
catch (DockerContainerNotFoundException){}
|
|
||||||
|
|
||||||
// Canceling server sub-tasks and recreating cancellation token
|
|
||||||
if (!server.Cancellation.IsCancellationRequested)
|
|
||||||
await server.Cancellation.CancelAsync();
|
|
||||||
|
|
||||||
server.Cancellation = new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
using Docker.DotNet;
|
|
||||||
using Docker.DotNet.Models;
|
|
||||||
using MoonlightServers.Daemon.Models;
|
|
||||||
using MoonlightServers.DaemonShared.Enums;
|
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Extensions.ServerExtensions;
|
|
||||||
|
|
||||||
public static class ServerImageExtensions
|
|
||||||
{
|
|
||||||
public static async Task EnsureDockerImage(this Server server)
|
|
||||||
{
|
|
||||||
await server.NotifyTask(ServerTask.PullingDockerImage);
|
|
||||||
|
|
||||||
var dockerClient = server.ServiceProvider.GetRequiredService<DockerClient>();
|
|
||||||
|
|
||||||
await dockerClient.Images.CreateImageAsync(new()
|
|
||||||
{
|
|
||||||
FromImage = server.Configuration.DockerImage
|
|
||||||
},
|
|
||||||
new AuthConfig(),
|
|
||||||
new Progress<JSONMessage>(async message =>
|
|
||||||
{
|
|
||||||
if (message.Progress == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var percentage = message.Progress.Total > 0
|
|
||||||
? Math.Round((float)message.Progress.Current / message.Progress.Total * 100f, 2)
|
|
||||||
: 0d;
|
|
||||||
|
|
||||||
server.Logger.LogInformation(
|
|
||||||
"Docker Image: [{id}] {status} - {percent}",
|
|
||||||
message.ID,
|
|
||||||
message.Status,
|
|
||||||
percentage
|
|
||||||
);
|
|
||||||
|
|
||||||
//await UpdateProgress(server, serviceProvider, percentage);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
using MoonlightServers.Daemon.Models;
|
|
||||||
using MoonlightServers.DaemonShared.Enums;
|
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Extensions.ServerExtensions;
|
|
||||||
|
|
||||||
public static class ServerMetaExtensions
|
|
||||||
{
|
|
||||||
public static async Task NotifyTask(this Server server, ServerTask task)
|
|
||||||
{
|
|
||||||
server.Logger.LogInformation("Task: {task}", task);
|
|
||||||
await server.InvokeTaskAdded(task.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using Docker.DotNet;
|
|
||||||
using MoonlightServers.Daemon.Models;
|
|
||||||
using MoonlightServers.DaemonShared.Enums;
|
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Extensions.ServerExtensions;
|
|
||||||
|
|
||||||
public static class ServerStartExtensions
|
|
||||||
{
|
|
||||||
public static async Task StateMachineHandler_Start(this Server server)
|
|
||||||
{
|
|
||||||
await server.ReCreate();
|
|
||||||
|
|
||||||
await server.NotifyTask(ServerTask.StartingContainer);
|
|
||||||
var dockerClient = server.ServiceProvider.GetRequiredService<DockerClient>();
|
|
||||||
|
|
||||||
await dockerClient.Containers.StartContainerAsync(server.ContainerId, new());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using MoonlightServers.Daemon.Models;
|
|
||||||
using MoonlightServers.DaemonShared.Enums;
|
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Extensions.ServerExtensions;
|
|
||||||
|
|
||||||
public static class ServerStorageExtensions
|
|
||||||
{
|
|
||||||
public static async Task EnsureRuntimeStorage(this Server server)
|
|
||||||
{
|
|
||||||
// TODO: Add virtual disk
|
|
||||||
await server.NotifyTask(ServerTask.CreatingStorage);
|
|
||||||
|
|
||||||
// Create volume if missing
|
|
||||||
if (!Directory.Exists(server.RuntimeVolumePath))
|
|
||||||
Directory.CreateDirectory(server.RuntimeVolumePath);
|
|
||||||
|
|
||||||
// TODO: Chown
|
|
||||||
//Syscall.chown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
using Docker.DotNet;
|
|
||||||
using Docker.DotNet.Models;
|
|
||||||
using MoonCore.Helpers;
|
|
||||||
using MoonlightServers.Daemon.Configuration;
|
|
||||||
using MoonlightServers.Daemon.Models;
|
|
||||||
using MoonlightServers.DaemonShared.Enums;
|
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Helpers;
|
|
||||||
|
|
||||||
public class ServerActionHelper
|
|
||||||
{
|
|
||||||
public static async Task Start(Server server, IServiceProvider serviceProvider)
|
|
||||||
{
|
|
||||||
await EnsureStorage(server, serviceProvider);
|
|
||||||
await EnsureDockerImage(server, serviceProvider);
|
|
||||||
await CreateRuntimeContainer(server, serviceProvider);
|
|
||||||
await StartRuntimeContainer(server, serviceProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task EnsureStorage(Server server, IServiceProvider serviceProvider)
|
|
||||||
{
|
|
||||||
await NotifyTask(server, serviceProvider, ServerTask.CreatingStorage);
|
|
||||||
|
|
||||||
// Build paths
|
|
||||||
var configuration = serviceProvider.GetRequiredService<AppConfiguration>();
|
|
||||||
|
|
||||||
var volumePath = PathBuilder.Dir(
|
|
||||||
configuration.Storage.Volumes,
|
|
||||||
server.Configuration.Id.ToString()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create volume if missing
|
|
||||||
if (!Directory.Exists(volumePath))
|
|
||||||
Directory.CreateDirectory(volumePath);
|
|
||||||
|
|
||||||
// TODO: Virtual disk
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task EnsureDockerImage(Server server, IServiceProvider serviceProvider)
|
|
||||||
{
|
|
||||||
await NotifyTask(server, serviceProvider, ServerTask.PullingDockerImage);
|
|
||||||
|
|
||||||
var dockerClient = serviceProvider.GetRequiredService<DockerClient>();
|
|
||||||
|
|
||||||
await dockerClient.Images.CreateImageAsync(new()
|
|
||||||
{
|
|
||||||
FromImage = server.Configuration.DockerImage
|
|
||||||
},
|
|
||||||
new AuthConfig(),
|
|
||||||
new Progress<JSONMessage>(async message =>
|
|
||||||
{
|
|
||||||
//var percentage = (int)(message.Progress.Current / message.Progress.Total);
|
|
||||||
//await UpdateProgress(server, serviceProvider, percentage);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task CreateRuntimeContainer(Server server, IServiceProvider serviceProvider)
|
|
||||||
{
|
|
||||||
var dockerClient = serviceProvider.GetRequiredService<DockerClient>();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var existingContainer = await dockerClient.Containers.InspectContainerAsync(
|
|
||||||
$"moonlight-runtime-{server.Configuration.Id}"
|
|
||||||
);
|
|
||||||
|
|
||||||
await NotifyTask(server, serviceProvider, ServerTask.RemovingContainer);
|
|
||||||
|
|
||||||
if (existingContainer.State.Running) // Stop already running container
|
|
||||||
{
|
|
||||||
await dockerClient.Containers.StopContainerAsync(existingContainer.ID, new()
|
|
||||||
{
|
|
||||||
WaitBeforeKillSeconds = 30 // TODO: Config
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await dockerClient.Containers.RemoveContainerAsync(existingContainer.ID, new());
|
|
||||||
}
|
|
||||||
catch (DockerContainerNotFoundException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
await NotifyTask(server, serviceProvider, ServerTask.CreatingContainer);
|
|
||||||
|
|
||||||
// Create a new container
|
|
||||||
var parameters = new CreateContainerParameters();
|
|
||||||
|
|
||||||
ServerConfigurationHelper.ApplyRuntimeOptions(
|
|
||||||
parameters,
|
|
||||||
server.Configuration,
|
|
||||||
serviceProvider.GetRequiredService<AppConfiguration>()
|
|
||||||
);
|
|
||||||
|
|
||||||
var container = await dockerClient.Containers.CreateContainerAsync(parameters);
|
|
||||||
server.ContainerId = container.ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task StartRuntimeContainer(Server server, IServiceProvider serviceProvider)
|
|
||||||
{
|
|
||||||
await NotifyTask(server, serviceProvider, ServerTask.StartingContainer);
|
|
||||||
|
|
||||||
var dockerClient = serviceProvider.GetRequiredService<DockerClient>();
|
|
||||||
|
|
||||||
await dockerClient.Containers.StartContainerAsync(server.ContainerId, new());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task NotifyTask(Server server, IServiceProvider serviceProvider, ServerTask task)
|
|
||||||
{
|
|
||||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
|
||||||
var logger = loggerFactory.CreateLogger($"Server {server.Configuration.Id}");
|
|
||||||
|
|
||||||
logger.LogInformation("Task: {task}", task);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task UpdateProgress(Server server, IServiceProvider serviceProvider, int progress)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.SignalR;
|
|
||||||
using MoonlightServers.Daemon.Models;
|
|
||||||
using MoonlightServers.DaemonShared.Enums;
|
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Helpers;
|
|
||||||
|
|
||||||
public class ServerConsoleMonitor
|
|
||||||
{
|
|
||||||
private readonly Server Server;
|
|
||||||
private readonly IHubClients Clients;
|
|
||||||
|
|
||||||
public ServerConsoleMonitor(Server server, IHubClients clients)
|
|
||||||
{
|
|
||||||
Server = server;
|
|
||||||
Clients = clients;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Initialize()
|
|
||||||
{
|
|
||||||
Server.StateMachine.OnTransitioned += OnPowerStateChanged;
|
|
||||||
Server.OnTaskAdded += OnTaskNotify;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Destroy()
|
|
||||||
{
|
|
||||||
Server.StateMachine.OnTransitioned -= OnPowerStateChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnTaskNotify(string task)
|
|
||||||
{
|
|
||||||
await Clients.Group($"server-{Server.Configuration.Id}").SendAsync(
|
|
||||||
"TaskNotify",
|
|
||||||
task
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnPowerStateChanged(ServerState serverState)
|
|
||||||
{
|
|
||||||
await Clients.Group($"server-{Server.Configuration.Id}").SendAsync(
|
|
||||||
"PowerStateChanged",
|
|
||||||
serverState.ToString()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +1,29 @@
|
|||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using MoonlightServers.Daemon.Abstractions;
|
||||||
|
using MoonlightServers.Daemon.Enums;
|
||||||
using MoonlightServers.Daemon.Http.Hubs;
|
using MoonlightServers.Daemon.Http.Hubs;
|
||||||
using MoonlightServers.Daemon.Models;
|
using MoonlightServers.Daemon.Models;
|
||||||
using MoonlightServers.Daemon.Services;
|
using MoonlightServers.Daemon.Services;
|
||||||
using MoonlightServers.DaemonShared.Enums;
|
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Helpers;
|
namespace MoonlightServers.Daemon.Helpers;
|
||||||
|
|
||||||
public class ServerConsoleConnection
|
public class ServerWebSocketConnection
|
||||||
{
|
{
|
||||||
private readonly ServerService ServerService;
|
private readonly ServerService ServerService;
|
||||||
private readonly ILogger<ServerConsoleConnection> Logger;
|
private readonly ILogger<ServerWebSocketConnection> Logger;
|
||||||
private readonly AccessTokenHelper AccessTokenHelper;
|
private readonly AccessTokenHelper AccessTokenHelper;
|
||||||
private readonly IHubContext<ServerConsoleHub> HubContext;
|
private readonly IHubContext<ServerWebSocketHub> HubContext;
|
||||||
|
|
||||||
private int ServerId = -1;
|
private int ServerId = -1;
|
||||||
private Server Server;
|
private Server Server;
|
||||||
private bool IsInitialized = false;
|
private bool IsInitialized = false;
|
||||||
private string ConnectionId;
|
private string ConnectionId;
|
||||||
|
|
||||||
public ServerConsoleConnection(
|
public ServerWebSocketConnection(
|
||||||
ServerService serverService,
|
ServerService serverService,
|
||||||
ILogger<ServerConsoleConnection> logger,
|
ILogger<ServerWebSocketConnection> logger,
|
||||||
AccessTokenHelper accessTokenHelper,
|
AccessTokenHelper accessTokenHelper,
|
||||||
IHubContext<ServerConsoleHub> hubContext
|
IHubContext<ServerWebSocketHub> hubContext
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
ServerService = serverService;
|
ServerService = serverService;
|
||||||
@@ -64,7 +65,7 @@ public class ServerConsoleConnection
|
|||||||
// Validate access token type
|
// Validate access token type
|
||||||
var type = accessData["type"].GetString()!;
|
var type = accessData["type"].GetString()!;
|
||||||
|
|
||||||
if (type != "console")
|
if (type != "websocket")
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Received invalid access token: Invalid type '{type}'", type);
|
Logger.LogDebug("Received invalid access token: Invalid type '{type}'", type);
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ public class ServerConsoleConnection
|
|||||||
|
|
||||||
var serverId = accessData["serverId"].GetInt32();
|
var serverId = accessData["serverId"].GetInt32();
|
||||||
|
|
||||||
// Check that the access token isn't or another server
|
// Check that the access token isn't for another server
|
||||||
if (ServerId != -1 && ServerId == serverId)
|
if (ServerId != -1 && ServerId == serverId)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Received invalid access token: Server id not valid for this session. Current server id: {serverId}", ServerId);
|
Logger.LogDebug("Received invalid access token: Server id not valid for this session. Current server id: {serverId}", ServerId);
|
||||||
@@ -117,30 +118,26 @@ public class ServerConsoleConnection
|
|||||||
IsInitialized = true;
|
IsInitialized = true;
|
||||||
|
|
||||||
// Setup event handlers
|
// Setup event handlers
|
||||||
Server.StateMachine.OnTransitioned += HandlePowerStateChange;
|
Server.OnConsoleOutput += HandleConsoleOutput;
|
||||||
Server.OnTaskAdded += HandleTaskAdded;
|
Server.OnStateChanged += HandleStateChange;
|
||||||
Server.Console.OnOutput += HandleConsoleOutput;
|
|
||||||
|
|
||||||
Logger.LogTrace("Authenticated and initialized server console connection '{id}'", context.ConnectionId);
|
Logger.LogTrace("Authenticated and initialized server console connection '{id}'", context.ConnectionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Destroy(HubCallerContext context)
|
public Task Destroy(HubCallerContext context)
|
||||||
{
|
{
|
||||||
Server.StateMachine.OnTransitioned -= HandlePowerStateChange;
|
|
||||||
Server.OnTaskAdded -= HandleTaskAdded;
|
|
||||||
|
|
||||||
Logger.LogTrace("Destroyed server console connection '{id}'", context.ConnectionId);
|
Logger.LogTrace("Destroyed server console connection '{id}'", context.ConnectionId);
|
||||||
|
|
||||||
|
Server.OnConsoleOutput -= HandleConsoleOutput;
|
||||||
|
Server.OnStateChanged -= HandleStateChange;
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Event Handlers
|
#region Event Handlers
|
||||||
|
|
||||||
private async Task HandlePowerStateChange(ServerState serverState)
|
private async Task HandleStateChange(ServerState state)
|
||||||
=> await HubContext.Clients.Client(ConnectionId).SendAsync("PowerStateChanged", serverState.ToString());
|
=> await HubContext.Clients.Client(ConnectionId).SendAsync("StateChanged", state.ToString());
|
||||||
|
|
||||||
private async Task HandleTaskAdded(string task)
|
|
||||||
=> await HubContext.Clients.Client(ConnectionId).SendAsync("TaskNotify", task);
|
|
||||||
|
|
||||||
private async Task HandleConsoleOutput(string line)
|
private async Task HandleConsoleOutput(string line)
|
||||||
=> await HubContext.Clients.Client(ConnectionId).SendAsync("ConsoleOutput", line);
|
=> await HubContext.Clients.Client(ConnectionId).SendAsync("ConsoleOutput", line);
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
namespace MoonlightServers.Daemon.Helpers;
|
|
||||||
|
|
||||||
public class StateMachine<T> where T : struct, Enum
|
|
||||||
{
|
|
||||||
private readonly List<StateMachineTransition> Transitions = new();
|
|
||||||
private readonly object Lock = new();
|
|
||||||
|
|
||||||
public T CurrentState { get; private set; }
|
|
||||||
public event Func<T, Task> OnTransitioned;
|
|
||||||
public event Action<T, Exception> OnError;
|
|
||||||
|
|
||||||
public StateMachine(T initialState)
|
|
||||||
{
|
|
||||||
CurrentState = initialState;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddTransition(T from, T to, T? onError, Func<Task>? fun)
|
|
||||||
{
|
|
||||||
Transitions.Add(new()
|
|
||||||
{
|
|
||||||
From = from,
|
|
||||||
To = to,
|
|
||||||
OnError = onError,
|
|
||||||
OnTransitioning = fun
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddTransition(T from, T to, Func<Task> fun) => AddTransition(from, to, null, fun);
|
|
||||||
public void AddTransition(T from, T to) => AddTransition(from, to, null, null);
|
|
||||||
|
|
||||||
public async Task TransitionTo(T to)
|
|
||||||
{
|
|
||||||
lock (Lock)
|
|
||||||
{
|
|
||||||
var transition = Transitions.FirstOrDefault(x =>
|
|
||||||
x.From.Equals(CurrentState) &&
|
|
||||||
x.To.Equals(to)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (transition == null)
|
|
||||||
throw new InvalidOperationException("Unable to transition to the request state: No transition found");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if(transition.OnTransitioning != null)
|
|
||||||
transition.OnTransitioning.Invoke().Wait();
|
|
||||||
|
|
||||||
// Successfully executed => update state
|
|
||||||
CurrentState = transition.To;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
if(OnError != null)
|
|
||||||
OnError.Invoke(to, e);
|
|
||||||
|
|
||||||
if (transition.OnError.HasValue)
|
|
||||||
CurrentState = transition.OnError.Value;
|
|
||||||
else
|
|
||||||
throw new AggregateException("An error occured while transitioning to a state", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(OnTransitioned != null)
|
|
||||||
await OnTransitioned.Invoke(CurrentState);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class StateMachineTransition
|
|
||||||
{
|
|
||||||
public T From { get; set; }
|
|
||||||
public T To { get; set; }
|
|
||||||
public T? OnError { get; set; }
|
|
||||||
public Func<Task>? OnTransitioning { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using MoonCore.Exceptions;
|
||||||
|
using MoonlightServers.Daemon.Enums;
|
||||||
|
using MoonlightServers.Daemon.Services;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/servers")]
|
||||||
|
public class ServerPowerController : Controller
|
||||||
|
{
|
||||||
|
private readonly ServerService ServerService;
|
||||||
|
|
||||||
|
public ServerPowerController(ServerService serverService)
|
||||||
|
{
|
||||||
|
ServerService = serverService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{serverId:int}/start")]
|
||||||
|
public async Task Start(int serverId, [FromQuery] bool runAsync = true)
|
||||||
|
{
|
||||||
|
var server = ServerService.GetServer(serverId);
|
||||||
|
|
||||||
|
if (server == null)
|
||||||
|
throw new HttpApiException("No server with this id found", 404);
|
||||||
|
|
||||||
|
await server.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{serverId:int}/stop")]
|
||||||
|
public async Task Stop(int serverId, [FromQuery] bool runAsync = true)
|
||||||
|
{
|
||||||
|
var server = ServerService.GetServer(serverId);
|
||||||
|
|
||||||
|
if (server == null)
|
||||||
|
throw new HttpApiException("No server with this id found", 404);
|
||||||
|
|
||||||
|
await server.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using MoonCore.Exceptions;
|
using MoonCore.Exceptions;
|
||||||
using MoonlightServers.Daemon.Models;
|
|
||||||
using MoonlightServers.Daemon.Services;
|
using MoonlightServers.Daemon.Services;
|
||||||
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
|
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
|
||||||
using MoonlightServers.DaemonShared.Enums;
|
using MoonlightServers.DaemonShared.Enums;
|
||||||
@@ -28,7 +27,7 @@ public class ServersController : Controller
|
|||||||
|
|
||||||
return new ServerStatusResponse()
|
return new ServerStatusResponse()
|
||||||
{
|
{
|
||||||
State = server.State
|
State = (ServerState)server.State
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,18 +41,7 @@ public class ServersController : Controller
|
|||||||
|
|
||||||
return new ServerLogsResponse()
|
return new ServerLogsResponse()
|
||||||
{
|
{
|
||||||
Messages = server.Console.Messages
|
Messages = await server.GetConsoleMessages()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{serverId:int}/start")]
|
|
||||||
public async Task Start(int serverId)
|
|
||||||
{
|
|
||||||
var server = ServerService.GetServer(serverId);
|
|
||||||
|
|
||||||
if (server == null)
|
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
|
||||||
|
|
||||||
await server.StateMachine.TransitionTo(ServerState.Starting);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,29 +1,26 @@
|
|||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using MoonlightServers.Daemon.Helpers;
|
|
||||||
using MoonlightServers.Daemon.Models;
|
|
||||||
using MoonlightServers.Daemon.Services;
|
using MoonlightServers.Daemon.Services;
|
||||||
using MoonlightServers.DaemonShared.Enums;
|
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Http.Hubs;
|
namespace MoonlightServers.Daemon.Http.Hubs;
|
||||||
|
|
||||||
public class ServerConsoleHub : Hub
|
public class ServerWebSocketHub : Hub
|
||||||
{
|
{
|
||||||
private readonly ILogger<ServerConsoleHub> Logger;
|
private readonly ILogger<ServerWebSocketHub> Logger;
|
||||||
private readonly ServerConsoleService ConsoleService;
|
private readonly ServerWebSocketService WebSocketService;
|
||||||
|
|
||||||
public ServerConsoleHub(ILogger<ServerConsoleHub> logger, ServerConsoleService consoleService)
|
public ServerWebSocketHub(ILogger<ServerWebSocketHub> logger, ServerWebSocketService webSocketService)
|
||||||
{
|
{
|
||||||
Logger = logger;
|
Logger = logger;
|
||||||
ConsoleService = consoleService;
|
WebSocketService = webSocketService;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Connection Handlers
|
#region Connection Handlers
|
||||||
|
|
||||||
public override async Task OnConnectedAsync()
|
public override async Task OnConnectedAsync()
|
||||||
=> await ConsoleService.InitializeClient(Context);
|
=> await WebSocketService.InitializeClient(Context);
|
||||||
|
|
||||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||||
=> await ConsoleService.DestroyClient(Context);
|
=> await WebSocketService.DestroyClient(Context);
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -34,7 +31,7 @@ public class ServerConsoleHub : Hub
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await ConsoleService.AuthenticateClient(Context, accessToken);
|
await WebSocketService.AuthenticateClient(Context, accessToken);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
using MoonCore.Helpers;
|
|
||||||
using MoonlightServers.Daemon.Configuration;
|
|
||||||
using MoonlightServers.Daemon.Helpers;
|
|
||||||
using MoonlightServers.Daemon.Models.Cache;
|
|
||||||
using MoonlightServers.DaemonShared.Enums;
|
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Models;
|
|
||||||
|
|
||||||
public class Server
|
|
||||||
{
|
|
||||||
public ILogger Logger { get; set; }
|
|
||||||
public ServerConsole Console { get; set; }
|
|
||||||
public IServiceProvider ServiceProvider { get; set; }
|
|
||||||
public ServerState State => StateMachine.CurrentState;
|
|
||||||
public StateMachine<ServerState> StateMachine { get; set; }
|
|
||||||
public ServerConfiguration Configuration { get; set; }
|
|
||||||
public string? ContainerId { get; set; }
|
|
||||||
public event Func<string, Task> OnTaskAdded;
|
|
||||||
|
|
||||||
// This can be used to stop streaming when the server gets destroyed or something
|
|
||||||
public CancellationTokenSource Cancellation { get; set; }
|
|
||||||
|
|
||||||
#region Small helpers
|
|
||||||
|
|
||||||
public string RuntimeContainerName => $"moonlight-runtime-{Configuration.Id}";
|
|
||||||
public string InstallContainerName => $"moonlight-install-{Configuration.Id}";
|
|
||||||
|
|
||||||
public string RuntimeVolumePath
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var appConfig = ServiceProvider.GetRequiredService<AppConfiguration>();
|
|
||||||
|
|
||||||
var localPath = PathBuilder.Dir(
|
|
||||||
appConfig.Storage.Volumes,
|
|
||||||
Configuration.Id.ToString()
|
|
||||||
);
|
|
||||||
|
|
||||||
var absolutePath = Path.GetFullPath(localPath);
|
|
||||||
|
|
||||||
return absolutePath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string InstallVolumePath
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var appConfig = ServiceProvider.GetRequiredService<AppConfiguration>();
|
|
||||||
|
|
||||||
var localPath = PathBuilder.Dir(
|
|
||||||
appConfig.Storage.Install,
|
|
||||||
Configuration.Id.ToString()
|
|
||||||
);
|
|
||||||
|
|
||||||
var absolutePath = Path.GetFullPath(localPath);
|
|
||||||
|
|
||||||
return absolutePath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Event invokers
|
|
||||||
|
|
||||||
public async Task InvokeTaskAdded(string task)
|
|
||||||
{
|
|
||||||
if(OnTaskAdded == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await OnTaskAdded.Invoke(task).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
@@ -9,9 +9,10 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Docker.DotNet" Version="3.125.15" />
|
<PackageReference Include="Docker.DotNet" Version="3.125.15" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||||
<PackageReference Include="MoonCore" Version="1.8.1" />
|
<PackageReference Include="MoonCore" Version="1.8.2" />
|
||||||
<PackageReference Include="MoonCore.Extended" Version="1.2.4" />
|
<PackageReference Include="MoonCore.Extended" Version="1.2.7" />
|
||||||
<PackageReference Include="MoonCore.Unix" Version="1.0.0" />
|
<PackageReference Include="MoonCore.Unix" Version="1.0.0" />
|
||||||
|
<PackageReference Include="Stateless" Version="5.17.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2"/>
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@@ -24,13 +25,19 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<_ContentIncludedByDefault Remove="storage\volumes\11\banned-ips.json" />
|
<Compile Remove="data\**" />
|
||||||
<_ContentIncludedByDefault Remove="storage\volumes\11\banned-players.json" />
|
</ItemGroup>
|
||||||
<_ContentIncludedByDefault Remove="storage\volumes\11\ops.json" />
|
|
||||||
<_ContentIncludedByDefault Remove="storage\volumes\11\plugins\spark\config.json" />
|
<ItemGroup>
|
||||||
<_ContentIncludedByDefault Remove="storage\volumes\11\usercache.json" />
|
<EmbeddedResource Remove="data\**" />
|
||||||
<_ContentIncludedByDefault Remove="storage\volumes\11\version_history.json" />
|
</ItemGroup>
|
||||||
<_ContentIncludedByDefault Remove="storage\volumes\11\whitelist.json" />
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Remove="data\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="data\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
using MoonCore.Attributes;
|
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Services;
|
|
||||||
|
|
||||||
[Singleton]
|
|
||||||
public class ApplicationStateService : IHostedLifecycleService
|
|
||||||
{
|
|
||||||
private readonly ServerService ServerService;
|
|
||||||
private readonly ILogger<ApplicationStateService> Logger;
|
|
||||||
|
|
||||||
public ApplicationStateService(ServerService serverService, ILogger<ApplicationStateService> logger)
|
|
||||||
{
|
|
||||||
ServerService = serverService;
|
|
||||||
Logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
|
||||||
=> Task.CompletedTask;
|
|
||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
|
||||||
=> Task.CompletedTask;
|
|
||||||
|
|
||||||
public async Task StartedAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
Logger.LogInformation("Performing initialization");
|
|
||||||
|
|
||||||
await ServerService.Initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task StartingAsync(CancellationToken cancellationToken)
|
|
||||||
=> Task.CompletedTask;
|
|
||||||
|
|
||||||
public Task StoppedAsync(CancellationToken cancellationToken)
|
|
||||||
=> Task.CompletedTask;
|
|
||||||
|
|
||||||
public async Task StoppingAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
Logger.LogInformation("Stopping services");
|
|
||||||
|
|
||||||
await ServerService.Stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
40
MoonlightServers.Daemon/Services/DockerImageService.cs
Normal file
40
MoonlightServers.Daemon/Services/DockerImageService.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using Docker.DotNet;
|
||||||
|
using Docker.DotNet.Models;
|
||||||
|
using MoonCore.Attributes;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Daemon.Services;
|
||||||
|
|
||||||
|
[Singleton]
|
||||||
|
public class DockerImageService
|
||||||
|
{
|
||||||
|
private readonly DockerClient DockerClient;
|
||||||
|
private readonly ILogger<DockerImageService> Logger;
|
||||||
|
|
||||||
|
public DockerImageService(DockerClient dockerClient, ILogger<DockerImageService> logger)
|
||||||
|
{
|
||||||
|
DockerClient = dockerClient;
|
||||||
|
Logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Ensure(string name, Action<string>? onProgressUpdated)
|
||||||
|
{
|
||||||
|
await DockerClient.Images.CreateImageAsync(new()
|
||||||
|
{
|
||||||
|
FromImage = name
|
||||||
|
},
|
||||||
|
new AuthConfig(), // TODO: Config for custom registries
|
||||||
|
new Progress<JSONMessage>(async message =>
|
||||||
|
{
|
||||||
|
if (message.Progress == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var line = $"[{message.ID}] {message.ProgressMessage}";
|
||||||
|
|
||||||
|
Logger.LogInformation("{line}", line);
|
||||||
|
|
||||||
|
if(onProgressUpdated != null)
|
||||||
|
onProgressUpdated.Invoke(line);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,29 +2,30 @@ using Docker.DotNet;
|
|||||||
using Docker.DotNet.Models;
|
using Docker.DotNet.Models;
|
||||||
using MoonCore.Attributes;
|
using MoonCore.Attributes;
|
||||||
using MoonCore.Models;
|
using MoonCore.Models;
|
||||||
using MoonlightServers.Daemon.Extensions.ServerExtensions;
|
using MoonlightServers.Daemon.Abstractions;
|
||||||
using MoonlightServers.Daemon.Models;
|
|
||||||
using MoonlightServers.Daemon.Models.Cache;
|
using MoonlightServers.Daemon.Models.Cache;
|
||||||
using MoonlightServers.DaemonShared.Enums;
|
|
||||||
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
|
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Services;
|
namespace MoonlightServers.Daemon.Services;
|
||||||
|
|
||||||
[Singleton]
|
[Singleton]
|
||||||
public class ServerService
|
public class ServerService : IHostedLifecycleService
|
||||||
{
|
{
|
||||||
private readonly List<Server> Servers = new();
|
private readonly List<Server> Servers = new();
|
||||||
private readonly ILogger<ServerService> Logger;
|
private readonly ILogger<ServerService> Logger;
|
||||||
private readonly RemoteService RemoteService;
|
private readonly RemoteService RemoteService;
|
||||||
private readonly IServiceProvider ServiceProvider;
|
private readonly IServiceProvider ServiceProvider;
|
||||||
|
private readonly ILoggerFactory LoggerFactory;
|
||||||
private bool IsInitialized = false;
|
private bool IsInitialized = false;
|
||||||
private CancellationTokenSource Cancellation = new();
|
private CancellationTokenSource Cancellation = new();
|
||||||
|
|
||||||
public ServerService(RemoteService remoteService, ILogger<ServerService> logger, IServiceProvider serviceProvider)
|
public ServerService(RemoteService remoteService, ILogger<ServerService> logger, IServiceProvider serviceProvider,
|
||||||
|
ILoggerFactory loggerFactory)
|
||||||
{
|
{
|
||||||
RemoteService = remoteService;
|
RemoteService = remoteService;
|
||||||
Logger = logger;
|
Logger = logger;
|
||||||
ServiceProvider = serviceProvider;
|
ServiceProvider = serviceProvider;
|
||||||
|
LoggerFactory = loggerFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Initialize() //TODO: Add initialize call from panel
|
public async Task Initialize() //TODO: Add initialize call from panel
|
||||||
@@ -39,7 +40,7 @@ public class ServerService
|
|||||||
|
|
||||||
// Loading models and converting them
|
// Loading models and converting them
|
||||||
Logger.LogInformation("Fetching servers from panel");
|
Logger.LogInformation("Fetching servers from panel");
|
||||||
var apiClient = await RemoteService.CreateHttpClient();
|
using var apiClient = await RemoteService.CreateHttpClient();
|
||||||
|
|
||||||
var servers = await PagedData<ServerDataResponse>.All(async (page, pageSize) =>
|
var servers = await PagedData<ServerDataResponse>.All(async (page, pageSize) =>
|
||||||
await apiClient.GetJson<PagedData<ServerDataResponse>>(
|
await apiClient.GetJson<PagedData<ServerDataResponse>>(
|
||||||
@@ -69,8 +70,7 @@ public class ServerService
|
|||||||
|
|
||||||
Logger.LogInformation("Initializing {count} servers", servers.Length);
|
Logger.LogInformation("Initializing {count} servers", servers.Length);
|
||||||
|
|
||||||
foreach (var configuration in configurations)
|
await InitializeServerRange(configurations); // TODO: Initialize them multi threaded (maybe)
|
||||||
await InitializeServer(configuration);
|
|
||||||
|
|
||||||
// Attach to docker events
|
// Attach to docker events
|
||||||
await AttachToDockerEvents();
|
await AttachToDockerEvents();
|
||||||
@@ -83,11 +83,13 @@ public class ServerService
|
|||||||
lock (Servers)
|
lock (Servers)
|
||||||
servers = Servers.ToArray();
|
servers = Servers.ToArray();
|
||||||
|
|
||||||
Logger.LogTrace("Canceling server sub tasks");
|
//
|
||||||
|
Logger.LogTrace("Canceling server tasks");
|
||||||
|
|
||||||
foreach (var server in servers)
|
foreach (var server in servers)
|
||||||
await server.Cancellation.CancelAsync();
|
await server.CancelTasks();
|
||||||
|
|
||||||
|
//
|
||||||
Logger.LogTrace("Canceling own tasks");
|
Logger.LogTrace("Canceling own tasks");
|
||||||
await Cancellation.CancelAsync();
|
await Cancellation.CancelAsync();
|
||||||
}
|
}
|
||||||
@@ -108,23 +110,25 @@ public class ServerService
|
|||||||
await dockerClient.System.MonitorEventsAsync(new ContainerEventsParameters(),
|
await dockerClient.System.MonitorEventsAsync(new ContainerEventsParameters(),
|
||||||
new Progress<Message>(async message =>
|
new Progress<Message>(async message =>
|
||||||
{
|
{
|
||||||
if(message.Action != "die")
|
if (message.Action != "die")
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Server? server;
|
Server? server;
|
||||||
|
|
||||||
lock (Servers)
|
lock (Servers)
|
||||||
server = Servers.FirstOrDefault(x => x.ContainerId == message.ID);
|
server = Servers.FirstOrDefault(x => x.RuntimeContainerId == message.ID);
|
||||||
|
|
||||||
// TODO: Maybe implement a lookup for containers which id isn't set in the cache
|
// TODO: Maybe implement a lookup for containers which id isn't set in the cache
|
||||||
|
|
||||||
if(server == null)
|
if (server == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await server.StateMachine.TransitionTo(ServerState.Offline);
|
await server.NotifyContainerDied();
|
||||||
}), Cancellation.Token);
|
}), Cancellation.Token);
|
||||||
}
|
}
|
||||||
catch(TaskCanceledException){} // Can be ignored
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
} // Can be ignored
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Logger.LogError("An unhandled error occured while attaching to docker events: {e}", e);
|
Logger.LogError("An unhandled error occured while attaching to docker events: {e}", e);
|
||||||
@@ -133,44 +137,47 @@ public class ServerService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task InitializeServer(ServerConfiguration configuration)
|
private async Task InitializeServerRange(ServerConfiguration[] serverConfigurations)
|
||||||
{
|
{
|
||||||
Logger.LogTrace("Initializing server '{id}'", configuration.Id);
|
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
|
||||||
|
|
||||||
var loggerFactory = ServiceProvider.GetRequiredService<ILoggerFactory>();
|
var existingContainers = await dockerClient.Containers.ListContainersAsync(new()
|
||||||
|
|
||||||
var server = new Server()
|
|
||||||
{
|
{
|
||||||
Configuration = configuration,
|
All = true,
|
||||||
StateMachine = new(ServerState.Offline),
|
Limit = null,
|
||||||
ServiceProvider = ServiceProvider,
|
Filters = new Dictionary<string, IDictionary<string, bool>>()
|
||||||
Logger = loggerFactory.CreateLogger($"Server {configuration.Id}"),
|
{
|
||||||
Console = new(),
|
{
|
||||||
Cancellation = new()
|
"label",
|
||||||
};
|
new Dictionary<string, bool>()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
"Software=Moonlight-Panel",
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.StateMachine.OnError += (state, exception) =>
|
foreach (var configuration in serverConfigurations)
|
||||||
{
|
await InitializeServer(configuration, existingContainers);
|
||||||
server.Logger.LogError("Encountered an unhandled error while transitioning to {state}: {e}",
|
}
|
||||||
state,
|
|
||||||
exception
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
server.StateMachine.OnTransitioned += state =>
|
private async Task InitializeServer(
|
||||||
{
|
ServerConfiguration serverConfiguration,
|
||||||
server.Logger.LogInformation("State: {state}", state);
|
IList<ContainerListResponse> existingContainers
|
||||||
return Task.CompletedTask;
|
)
|
||||||
};
|
{
|
||||||
|
Logger.LogTrace("Initializing server '{id}'", serverConfiguration.Id);
|
||||||
|
|
||||||
server.StateMachine.AddTransition(ServerState.Offline, ServerState.Starting, ServerState.Offline, async () =>
|
var server = new Server(
|
||||||
await server.StateMachineHandler_Start()
|
LoggerFactory.CreateLogger($"Server {serverConfiguration.Id}"),
|
||||||
|
ServiceProvider,
|
||||||
|
serverConfiguration
|
||||||
);
|
);
|
||||||
|
|
||||||
server.StateMachine.AddTransition(ServerState.Starting, ServerState.Offline);
|
await server.Initialize(existingContainers);
|
||||||
server.StateMachine.AddTransition(ServerState.Online, ServerState.Offline);
|
|
||||||
server.StateMachine.AddTransition(ServerState.Stopping, ServerState.Offline);
|
|
||||||
server.StateMachine.AddTransition(ServerState.Installing, ServerState.Offline);
|
|
||||||
|
|
||||||
lock (Servers)
|
lock (Servers)
|
||||||
Servers.Add(server);
|
Servers.Add(server);
|
||||||
@@ -179,6 +186,46 @@ public class ServerService
|
|||||||
public Server? GetServer(int id)
|
public Server? GetServer(int id)
|
||||||
{
|
{
|
||||||
lock (Servers)
|
lock (Servers)
|
||||||
return Servers.FirstOrDefault(x => x.Configuration.Id == id);
|
return Servers.FirstOrDefault(x => x.Id == id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Lifecycle
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
=> Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
=> Task.CompletedTask;
|
||||||
|
|
||||||
|
public async Task StartedAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Initialize();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogCritical("Unable to initialize servers. Is the panel online? Error: {e}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartingAsync(CancellationToken cancellationToken)
|
||||||
|
=> Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task StoppedAsync(CancellationToken cancellationToken)
|
||||||
|
=> Task.CompletedTask;
|
||||||
|
|
||||||
|
public async Task StoppingAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Stop();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogCritical("Unable to stop server handling: {e}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
@@ -6,15 +6,15 @@ using MoonlightServers.Daemon.Http.Hubs;
|
|||||||
namespace MoonlightServers.Daemon.Services;
|
namespace MoonlightServers.Daemon.Services;
|
||||||
|
|
||||||
[Singleton]
|
[Singleton]
|
||||||
public class ServerConsoleService
|
public class ServerWebSocketService
|
||||||
{
|
{
|
||||||
private readonly ILogger<ServerConsoleService> Logger;
|
private readonly ILogger<ServerWebSocketService> Logger;
|
||||||
private readonly IServiceProvider ServiceProvider;
|
private readonly IServiceProvider ServiceProvider;
|
||||||
|
|
||||||
private readonly Dictionary<string, ServerConsoleConnection> Connections = new();
|
private readonly Dictionary<string, ServerWebSocketConnection> Connections = new();
|
||||||
|
|
||||||
public ServerConsoleService(
|
public ServerWebSocketService(
|
||||||
ILogger<ServerConsoleService> logger,
|
ILogger<ServerWebSocketService> logger,
|
||||||
IServiceProvider serviceProvider
|
IServiceProvider serviceProvider
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@@ -24,11 +24,11 @@ public class ServerConsoleService
|
|||||||
|
|
||||||
public async Task InitializeClient(HubCallerContext context)
|
public async Task InitializeClient(HubCallerContext context)
|
||||||
{
|
{
|
||||||
var connection = new ServerConsoleConnection(
|
var connection = new ServerWebSocketConnection(
|
||||||
ServiceProvider.GetRequiredService<ServerService>(),
|
ServiceProvider.GetRequiredService<ServerService>(),
|
||||||
ServiceProvider.GetRequiredService<ILogger<ServerConsoleConnection>>(),
|
ServiceProvider.GetRequiredService<ILogger<ServerWebSocketConnection>>(),
|
||||||
ServiceProvider.GetRequiredService<AccessTokenHelper>(),
|
ServiceProvider.GetRequiredService<AccessTokenHelper>(),
|
||||||
ServiceProvider.GetRequiredService<IHubContext<ServerConsoleHub>>()
|
ServiceProvider.GetRequiredService<IHubContext<ServerWebSocketHub>>()
|
||||||
);
|
);
|
||||||
|
|
||||||
lock (Connections)
|
lock (Connections)
|
||||||
@@ -39,7 +39,7 @@ public class ServerConsoleService
|
|||||||
|
|
||||||
public async Task AuthenticateClient(HubCallerContext context, string accessToken)
|
public async Task AuthenticateClient(HubCallerContext context, string accessToken)
|
||||||
{
|
{
|
||||||
ServerConsoleConnection? connection;
|
ServerWebSocketConnection? connection;
|
||||||
|
|
||||||
lock (Connections)
|
lock (Connections)
|
||||||
connection = Connections.GetValueOrDefault(context.ConnectionId);
|
connection = Connections.GetValueOrDefault(context.ConnectionId);
|
||||||
@@ -52,7 +52,7 @@ public class ServerConsoleService
|
|||||||
|
|
||||||
public async Task DestroyClient(HubCallerContext context)
|
public async Task DestroyClient(HubCallerContext context)
|
||||||
{
|
{
|
||||||
ServerConsoleConnection? connection;
|
ServerWebSocketConnection? connection;
|
||||||
|
|
||||||
lock (Connections)
|
lock (Connections)
|
||||||
connection = Connections.GetValueOrDefault(context.ConnectionId);
|
connection = Connections.GetValueOrDefault(context.ConnectionId);
|
||||||
@@ -249,8 +249,8 @@ public class Startup
|
|||||||
|
|
||||||
private Task RegisterServers()
|
private Task RegisterServers()
|
||||||
{
|
{
|
||||||
WebApplicationBuilder.Services.AddHostedService<ApplicationStateService>(
|
WebApplicationBuilder.Services.AddHostedService(
|
||||||
sp => sp.GetRequiredService<ApplicationStateService>()
|
sp => sp.GetRequiredService<ServerService>()
|
||||||
);
|
);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -263,7 +263,7 @@ public class Startup
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Maps
|
#region Hubs
|
||||||
|
|
||||||
private Task RegisterSignalR()
|
private Task RegisterSignalR()
|
||||||
{
|
{
|
||||||
@@ -273,7 +273,7 @@ public class Startup
|
|||||||
|
|
||||||
private Task MapHubs()
|
private Task MapHubs()
|
||||||
{
|
{
|
||||||
WebApplication.MapHub<ServerConsoleHub>("api/servers/console");
|
WebApplication.MapHub<ServerWebSocketHub>("api/servers/ws");
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
namespace MoonlightServers.DaemonShared.Enums;
|
|
||||||
|
|
||||||
public enum ServerTask
|
|
||||||
{
|
|
||||||
None = 0,
|
|
||||||
CreatingStorage = 1,
|
|
||||||
PullingDockerImage = 2,
|
|
||||||
RemovingContainer = 3,
|
|
||||||
CreatingContainer = 4,
|
|
||||||
StartingContainer = 5,
|
|
||||||
StoppingContainer = 6
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
|
"name": "Styles",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/forms": "^0.5.9"
|
"@tailwindcss/forms": "^0.5.9"
|
||||||
},
|
},
|
||||||
@@ -610,9 +611,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.7",
|
"version": "3.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
|
|||||||
@@ -41,7 +41,9 @@
|
|||||||
{
|
{
|
||||||
Form = new()
|
Form = new()
|
||||||
{
|
{
|
||||||
IpAddress = "0.0.0.0"
|
IpAddress = "0.0.0.0",
|
||||||
|
Start = 2000,
|
||||||
|
End = 3000
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,23 +11,23 @@
|
|||||||
|
|
||||||
if (IsLoaded && !IsFailed)
|
if (IsLoaded && !IsFailed)
|
||||||
{
|
{
|
||||||
gradient = Status.PowerState switch
|
gradient = Status.State switch
|
||||||
{
|
{
|
||||||
ServerPowerState.Installing => "from-primary-600/20",
|
ServerState.Installing => "from-primary-600/20",
|
||||||
ServerPowerState.Offline => "from-danger-600/20",
|
ServerState.Offline => "from-danger-600/20",
|
||||||
ServerPowerState.Starting => "from-warning-600/20",
|
ServerState.Starting => "from-warning-600/20",
|
||||||
ServerPowerState.Stopping => "from-warning-600/20",
|
ServerState.Stopping => "from-warning-600/20",
|
||||||
ServerPowerState.Online => "from-success-600/20",
|
ServerState.Online => "from-success-600/20",
|
||||||
_ => "from-gray-600/20"
|
_ => "from-gray-600/20"
|
||||||
};
|
};
|
||||||
|
|
||||||
border = Status.PowerState switch
|
border = Status.State switch
|
||||||
{
|
{
|
||||||
ServerPowerState.Installing => "border-primary-600",
|
ServerState.Installing => "border-primary-600",
|
||||||
ServerPowerState.Offline => "border-danger-600",
|
ServerState.Offline => "border-danger-600",
|
||||||
ServerPowerState.Starting => "border-warning-600",
|
ServerState.Starting => "border-warning-600",
|
||||||
ServerPowerState.Stopping => "border-warning-600",
|
ServerState.Stopping => "border-warning-600",
|
||||||
ServerPowerState.Online => "border-success-600",
|
ServerState.Online => "border-success-600",
|
||||||
_ => "border-gray-600"
|
_ => "border-gray-600"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
@if (
|
@if (
|
||||||
IsLoaded &&
|
IsLoaded &&
|
||||||
!IsFailed &&
|
!IsFailed &&
|
||||||
Status.PowerState is ServerPowerState.Starting or ServerPowerState.Stopping or ServerPowerState.Online
|
Status.State is ServerState.Starting or ServerState.Stopping or ServerState.Online
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
<div class="bg-gray-900 bg-opacity-45 py-1 px-2 rounded-lg flex flex-row">
|
<div class="bg-gray-900 bg-opacity-45 py-1 px-2 rounded-lg flex flex-row">
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
<div class="ms-3">Unreachable</div>
|
<div class="ms-3">Unreachable</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else if (IsLoaded && !IsFailed && Status.PowerState is ServerPowerState.Offline)
|
else if (IsLoaded && !IsFailed && Status.State is ServerState.Offline)
|
||||||
{
|
{
|
||||||
<div class="bg-gray-900 bg-opacity-45 py-1 px-2 rounded-lg flex flex-row text-danger-500">
|
<div class="bg-gray-900 bg-opacity-45 py-1 px-2 rounded-lg flex flex-row text-danger-500">
|
||||||
<div>
|
<div>
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
<div class="ms-3">Offline</div>
|
<div class="ms-3">Offline</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else if (IsLoaded && !IsFailed && Status.PowerState is ServerPowerState.Installing)
|
else if (IsLoaded && !IsFailed && Status.State is ServerState.Installing)
|
||||||
{
|
{
|
||||||
<div class="bg-gray-900 bg-opacity-45 py-1 px-2 rounded-lg flex flex-row text-primary-500">
|
<div class="bg-gray-900 bg-opacity-45 py-1 px-2 rounded-lg flex flex-row text-primary-500">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -15,9 +15,11 @@
|
|||||||
<LazyLoader Load="Load">
|
<LazyLoader Load="Load">
|
||||||
@if (NotFound)
|
@if (NotFound)
|
||||||
{
|
{
|
||||||
<div class="flex flex-col justify-center text-center"><img class="h-48 mt-5 mb-3" src="/svg/notfound.svg"
|
<div class="flex flex-col justify-center text-center">
|
||||||
alt="Not found illustration">
|
<img class="h-48 mt-5 mb-3" src="/svg/notfound.svg" alt="Not found illustration">
|
||||||
<h3 class="mt-2 font-semibold text-white text-lg">Server not found</h3>
|
<h3 class="mt-2 font-semibold text-white text-lg">
|
||||||
|
Server not found
|
||||||
|
</h3>
|
||||||
<p class="mt-1 text-gray-300">
|
<p class="mt-1 text-gray-300">
|
||||||
The server you requested does not exist
|
The server you requested does not exist
|
||||||
</p>
|
</p>
|
||||||
@@ -28,13 +30,13 @@
|
|||||||
<div class="card card-body justify-between py-2.5 px-5 flex-row">
|
<div class="card card-body justify-between py-2.5 px-5 flex-row">
|
||||||
<div class="flex flex-row items-center">
|
<div class="flex flex-row items-center">
|
||||||
@{
|
@{
|
||||||
var bgColor = PowerState switch
|
var bgColor = State switch
|
||||||
{
|
{
|
||||||
ServerPowerState.Installing => "bg-primary-500",
|
ServerState.Installing => "bg-primary-500",
|
||||||
ServerPowerState.Offline => "bg-danger-500",
|
ServerState.Offline => "bg-danger-500",
|
||||||
ServerPowerState.Starting => "bg-warning-500",
|
ServerState.Starting => "bg-warning-500",
|
||||||
ServerPowerState.Stopping => "bg-warning-500",
|
ServerState.Stopping => "bg-warning-500",
|
||||||
ServerPowerState.Online => "bg-success-500",
|
ServerState.Online => "bg-success-500",
|
||||||
_ => "bg-gray-500"
|
_ => "bg-gray-500"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -57,29 +59,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row items-center">
|
<div class="flex flex-row items-center">
|
||||||
@if (!string.IsNullOrEmpty(CurrentTask))
|
|
||||||
{
|
|
||||||
<div class="hidden md:flex me-8 items-center text-gray-600">
|
|
||||||
<i class="icon-loader me-1.5 animate-spin text-lg align-middle"></i>
|
|
||||||
<span class="text-base align-middle">
|
|
||||||
@CurrentTask
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="flex gap-x-1.5">
|
<div class="flex gap-x-1.5">
|
||||||
<button class="btn btn-success">
|
@if (State == ServerState.Offline)
|
||||||
<i class="icon-play me-1 align-middle"></i>
|
{
|
||||||
<span class="align-middle">Start</span>
|
<WButton CssClasses="btn btn-success" OnClick="_ => Start()">
|
||||||
</button>
|
<i class="icon-play me-1 align-middle"></i>
|
||||||
|
<span class="align-middle">Start</span>
|
||||||
|
</WButton>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<button type="button" class="btn btn-success" disabled="disabled">
|
||||||
|
<i class="icon-play me-1 align-middle"></i>
|
||||||
|
<span class="align-middle">Start</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
<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>
|
||||||
<button type="button" class="btn btn-danger">
|
@if (State == ServerState.Starting || State == ServerState.Online)
|
||||||
<i class="icon-squircle me-1 align-middle"></i>
|
{
|
||||||
<span class="align-middle">Stop</span>
|
<WButton CssClasses="btn btn-danger" OnClick="_ => Stop()">
|
||||||
</button>
|
<i class="icon-squircle me-1 align-middle"></i>
|
||||||
|
<span class="align-middle">Stop</span>
|
||||||
|
</WButton>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<button type="button" class="btn btn-danger" disabled="disabled">
|
||||||
|
<i class="icon-squircle me-1 align-middle"></i>
|
||||||
|
<span class="align-middle">Stop</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,31 +99,45 @@
|
|||||||
<div class="mt-5 mx-2 relative">
|
<div class="mt-5 mx-2 relative">
|
||||||
<ul class="relative text-sm font-medium flex flex-nowrap -mx-4 sm:-mx-6 lg:-mx-8 overflow-x-scroll no-scrollbar">
|
<ul class="relative text-sm font-medium flex flex-nowrap -mx-4 sm:-mx-6 lg:-mx-8 overflow-x-scroll no-scrollbar">
|
||||||
|
|
||||||
<li class="mr-6 last:mr-0 first:pl-4 sm:first:pl-6 lg:first:pl-8 last:pr-4 sm:last:pr-6 lg:last:pr-8"><a
|
<li class="mr-6 last:mr-0 first:pl-4 sm:first:pl-6 lg:first:pl-8 last:pr-4 sm:last:pr-6 lg:last:pr-8">
|
||||||
href="/admin/servers/all"
|
<a
|
||||||
class="block pb-3 text-white whitespace-nowrap border-b-2 border-primary-500">Console</a></li>
|
href="/admin/servers/all"
|
||||||
|
class="block pb-3 text-white whitespace-nowrap border-b-2 border-primary-500">Console</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="mr-6 last:mr-0 first:pl-4 sm:first:pl-6 lg:first:pl-8 last:pr-4 sm:last:pr-6 lg:last:pr-8"><a
|
<li class="mr-6 last:mr-0 first:pl-4 sm:first:pl-6 lg:first:pl-8 last:pr-4 sm:last:pr-6 lg:last:pr-8">
|
||||||
href="/admin/servers"
|
<a
|
||||||
class="block pb-3 text-gray-400 hover:text-white whitespace-nowrap hover:border-b-2 hover:border-primary-500">Files</a>
|
href="/admin/servers"
|
||||||
|
class="block pb-3 text-gray-400 hover:text-white whitespace-nowrap hover:border-b-2 hover:border-primary-500">
|
||||||
|
Files
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="mr-6 last:mr-0 first:pl-4 sm:first:pl-6 lg:first:pl-8 last:pr-4 sm:last:pr-6 lg:last:pr-8"><a
|
<li class="mr-6 last:mr-0 first:pl-4 sm:first:pl-6 lg:first:pl-8 last:pr-4 sm:last:pr-6 lg:last:pr-8">
|
||||||
href="/admin/servers/nodes"
|
<a
|
||||||
class="block pb-3 text-gray-400 hover:text-white whitespace-nowrap hover:border-b-2 hover:border-primary-500">Backups</a>
|
href="/admin/servers/nodes"
|
||||||
|
class="block pb-3 text-gray-400 hover:text-white whitespace-nowrap hover:border-b-2 hover:border-primary-500">
|
||||||
|
Backups
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="mr-6 last:mr-0 first:pl-4 sm:first:pl-6 lg:first:pl-8 last:pr-4 sm:last:pr-6 lg:last:pr-8"><a
|
<li class="mr-6 last:mr-0 first:pl-4 sm:first:pl-6 lg:first:pl-8 last:pr-4 sm:last:pr-6 lg:last:pr-8">
|
||||||
href="/admin/servers/stars"
|
<a
|
||||||
class="block pb-3 text-gray-400 hover:text-white whitespace-nowrap hover:border-b-2 hover:border-primary-500">Networking</a>
|
href="/admin/servers/stars"
|
||||||
|
class="block pb-3 text-gray-400 hover:text-white whitespace-nowrap hover:border-b-2 hover:border-primary-500">
|
||||||
|
Networking
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="mr-6 last:mr-0 first:pl-4 sm:first:pl-6 lg:first:pl-8 last:pr-4 sm:last:pr-6 lg:last:pr-8"><a
|
<li class="mr-6 last:mr-0 first:pl-4 sm:first:pl-6 lg:first:pl-8 last:pr-4 sm:last:pr-6 lg:last:pr-8">
|
||||||
href="/admin/servers/manager"
|
<a
|
||||||
class="block pb-3 text-gray-400 hover:text-white whitespace-nowrap hover:border-b-2 hover:border-primary-500">Variables</a>
|
href="/admin/servers/manager"
|
||||||
|
class="block pb-3 text-gray-400 hover:text-white whitespace-nowrap hover:border-b-2 hover:border-primary-500">
|
||||||
|
Variables
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3 h-44">
|
<div class="mt-3 h-44">
|
||||||
<XtermConsole @ref="XtermConsole" OnAfterInitialized="OnAfterConsoleInitialized" />
|
<XtermConsole @ref="XtermConsole" OnAfterInitialized="OnAfterConsoleInitialized"/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</LazyLoader>
|
</LazyLoader>
|
||||||
@@ -122,13 +148,12 @@
|
|||||||
|
|
||||||
private ServerDetailResponse Server;
|
private ServerDetailResponse Server;
|
||||||
private bool NotFound = false;
|
private bool NotFound = false;
|
||||||
private ServerPowerState PowerState;
|
private ServerState State;
|
||||||
private string InitialConsoleMessage; // TODO: When moving to a single component, fail safe when failed to load
|
private string InitialConsoleMessage; // TODO: When moving to a single component, fail safe when failed to load
|
||||||
|
|
||||||
private string CurrentTask = "";
|
|
||||||
private XtermConsole? XtermConsole;
|
private XtermConsole? XtermConsole;
|
||||||
|
|
||||||
private HubConnection ConsoleConnection;
|
private HubConnection WebSocketConnection;
|
||||||
|
|
||||||
private async Task Load(LazyLoader _)
|
private async Task Load(LazyLoader _)
|
||||||
{
|
{
|
||||||
@@ -144,7 +169,7 @@
|
|||||||
$"api/servers/{ServerId}/status"
|
$"api/servers/{ServerId}/status"
|
||||||
);
|
);
|
||||||
|
|
||||||
PowerState = status.PowerState;
|
State = status.State;
|
||||||
|
|
||||||
// Load initial messages
|
// Load initial messages
|
||||||
var initialLogs = await ApiClient.GetJson<ServerLogsResponse>(
|
var initialLogs = await ApiClient.GetJson<ServerLogsResponse>(
|
||||||
@@ -156,32 +181,27 @@
|
|||||||
foreach (var message in initialLogs.Messages)
|
foreach (var message in initialLogs.Messages)
|
||||||
InitialConsoleMessage += message;
|
InitialConsoleMessage += message;
|
||||||
|
|
||||||
// Load console meta
|
// Load websocket meta
|
||||||
var consoleDetails = await ApiClient.GetJson<ServerConsoleResponse>(
|
var websocketDetails = await ApiClient.GetJson<ServerWebSocketResponse>(
|
||||||
$"api/servers/{ServerId}/console"
|
$"api/servers/{ServerId}/ws"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Build signal r
|
// Build signal r
|
||||||
ConsoleConnection = new HubConnectionBuilder()
|
WebSocketConnection = new HubConnectionBuilder()
|
||||||
.WithUrl(consoleDetails.Target)
|
.WithUrl(websocketDetails.Target)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
// Define handlers
|
// Define handlers
|
||||||
ConsoleConnection.On<string>("PowerStateChanged", async powerStateStr =>
|
WebSocketConnection.On<string>("StateChanged", async stateStr =>
|
||||||
{
|
{
|
||||||
if(!Enum.TryParse(powerStateStr, out ServerPowerState receivedState))
|
if (!Enum.TryParse(stateStr, out ServerState receivedState))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
PowerState = receivedState;
|
State = receivedState;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
});
|
});
|
||||||
|
|
||||||
ConsoleConnection.On<string>("TaskNotify", async task =>
|
WebSocketConnection.On<string>("ConsoleOutput", async content =>
|
||||||
{
|
|
||||||
await AddTask(Formatter.ConvertCamelCaseToSpaces(task));
|
|
||||||
});
|
|
||||||
|
|
||||||
ConsoleConnection.On<string>("ConsoleOutput", async content =>
|
|
||||||
{
|
{
|
||||||
if (XtermConsole != null)
|
if (XtermConsole != null)
|
||||||
await XtermConsole.Write(content);
|
await XtermConsole.Write(content);
|
||||||
@@ -190,10 +210,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Connect
|
// Connect
|
||||||
await ConsoleConnection.StartAsync();
|
await WebSocketConnection.StartAsync();
|
||||||
|
|
||||||
// Authenticate
|
// Authenticate
|
||||||
await ConsoleConnection.SendAsync("Authenticate", consoleDetails.AccessToken);
|
await WebSocketConnection.SendAsync("Authenticate", websocketDetails.AccessToken);
|
||||||
}
|
}
|
||||||
catch (HttpApiException e)
|
catch (HttpApiException e)
|
||||||
{
|
{
|
||||||
@@ -209,28 +229,21 @@
|
|||||||
await XtermConsole!.Write(InitialConsoleMessage);
|
await XtermConsole!.Write(InitialConsoleMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task AddTask(string message)
|
private async Task Start()
|
||||||
{
|
{
|
||||||
CurrentTask = message;
|
await ApiClient.Post($"api/servers/{Server.Id}/start");
|
||||||
await InvokeAsync(StateHasChanged);
|
}
|
||||||
|
|
||||||
Task.Run(async () =>
|
private async Task Stop()
|
||||||
{
|
{
|
||||||
await Task.Delay(3000);
|
await ApiClient.Post($"api/servers/{Server.Id}/stop");
|
||||||
|
|
||||||
if (CurrentTask != message)
|
|
||||||
return;
|
|
||||||
|
|
||||||
CurrentTask = "";
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
if (ConsoleConnection.State == HubConnectionState.Connected)
|
if (WebSocketConnection.State == HubConnectionState.Connected)
|
||||||
await ConsoleConnection.StopAsync();
|
await WebSocketConnection.StopAsync();
|
||||||
|
|
||||||
await ConsoleConnection.DisposeAsync();
|
await WebSocketConnection.DisposeAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace MoonlightServers.Shared.Enums;
|
namespace MoonlightServers.Shared.Enums;
|
||||||
|
|
||||||
public enum ServerPowerState
|
public enum ServerState
|
||||||
{
|
{
|
||||||
Offline = 0,
|
Offline = 0,
|
||||||
Starting = 1,
|
Starting = 1,
|
||||||
@@ -4,5 +4,5 @@ namespace MoonlightServers.Shared.Http.Responses.Users.Servers;
|
|||||||
|
|
||||||
public class ServerStatusResponse
|
public class ServerStatusResponse
|
||||||
{
|
{
|
||||||
public ServerPowerState PowerState { get; set; }
|
public ServerState State { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace MoonlightServers.Shared.Http.Responses.Users.Servers;
|
namespace MoonlightServers.Shared.Http.Responses.Users.Servers;
|
||||||
|
|
||||||
public class ServerConsoleResponse
|
public class ServerWebSocketResponse
|
||||||
{
|
{
|
||||||
public string Target { get; set; }
|
public string Target { get; set; }
|
||||||
public string AccessToken { get; set; }
|
public string AccessToken { get; set; }
|
||||||
Reference in New Issue
Block a user