Implemented more life cycle handling. Added support for rootless environments
This commit is contained in:
@@ -56,7 +56,7 @@ public class RemoteServersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dockerImage == null)
|
if (dockerImage == null)
|
||||||
dockerImage = server.Star.DockerImages.FirstOrDefault();
|
dockerImage = server.Star.DockerImages.LastOrDefault();
|
||||||
|
|
||||||
if (dockerImage == null)
|
if (dockerImage == null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,258 @@
|
|||||||
|
using Docker.DotNet.Models;
|
||||||
|
using Mono.Unix.Native;
|
||||||
|
using MoonCore.Helpers;
|
||||||
|
using MoonlightServers.Daemon.Models;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Daemon.Extensions.ServerExtensions;
|
||||||
|
|
||||||
|
public static class ServerConfigExtensions
|
||||||
|
{
|
||||||
|
public static CreateContainerParameters GetRuntimeContainerParameters(this Server server)
|
||||||
|
{
|
||||||
|
var parameters = server.GetSharedContainerParameters();
|
||||||
|
|
||||||
|
#region Security
|
||||||
|
|
||||||
|
parameters.HostConfig.CapDrop = new List<string>()
|
||||||
|
{
|
||||||
|
"setpcap", "mknod", "audit_write", "net_raw", "dac_override",
|
||||||
|
"fowner", "fsetid", "net_bind_service", "sys_chroot", "setfcap"
|
||||||
|
};
|
||||||
|
|
||||||
|
parameters.HostConfig.ReadonlyRootfs = true;
|
||||||
|
parameters.HostConfig.SecurityOpt = new List<string>()
|
||||||
|
{
|
||||||
|
"no-new-privileges"
|
||||||
|
};
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Name
|
||||||
|
|
||||||
|
parameters.Name = server.RuntimeContainerName;
|
||||||
|
parameters.Hostname = server.RuntimeContainerName;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Docker Image
|
||||||
|
|
||||||
|
parameters.Image = server.Configuration.DockerImage;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Environment
|
||||||
|
|
||||||
|
parameters.Env = server.ConstructEnv()
|
||||||
|
.Select(x => $"{x.Key}={x.Value}")
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Working Dir
|
||||||
|
|
||||||
|
parameters.WorkingDir = "/home/container";
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region User
|
||||||
|
|
||||||
|
var userId = Syscall.getuid();
|
||||||
|
|
||||||
|
if (userId == 0)
|
||||||
|
{
|
||||||
|
// We are running as root, so we need to run the container as another user and chown the files when we make changes
|
||||||
|
parameters.User = $"998:998";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// We are not running as root, so we start the container as the same user,
|
||||||
|
// as we are not able to chown the container content to a different user
|
||||||
|
parameters.User = $"{userId}:{userId}";
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Mounts
|
||||||
|
|
||||||
|
parameters.HostConfig.Mounts = new List<Mount>();
|
||||||
|
|
||||||
|
parameters.HostConfig.Mounts.Add(new()
|
||||||
|
{
|
||||||
|
Source = server.RuntimeVolumePath,
|
||||||
|
Target = "/home/container",
|
||||||
|
ReadOnly = false,
|
||||||
|
Type = "bind"
|
||||||
|
});
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Port Bindings
|
||||||
|
|
||||||
|
if (true) // TODO: Add network toggle
|
||||||
|
{
|
||||||
|
parameters.ExposedPorts = new Dictionary<string, EmptyStruct>();
|
||||||
|
parameters.HostConfig.PortBindings = new Dictionary<string, IList<PortBinding>>();
|
||||||
|
|
||||||
|
foreach (var allocation in server.Configuration.Allocations)
|
||||||
|
{
|
||||||
|
parameters.ExposedPorts.Add($"{allocation.Port}/tcp", new());
|
||||||
|
parameters.ExposedPorts.Add($"{allocation.Port}/udp", new());
|
||||||
|
|
||||||
|
parameters.HostConfig.PortBindings.Add($"{allocation.Port}/tcp", new List<PortBinding>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
HostPort = allocation.Port.ToString(),
|
||||||
|
HostIP = allocation.IpAddress
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
parameters.HostConfig.PortBindings.Add($"{allocation.Port}/udp", new List<PortBinding>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
HostPort = allocation.Port.ToString(),
|
||||||
|
HostIP = allocation.IpAddress
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CreateContainerParameters GetSharedContainerParameters(this Server server)
|
||||||
|
{
|
||||||
|
var parameters = new CreateContainerParameters()
|
||||||
|
{
|
||||||
|
HostConfig = new()
|
||||||
|
};
|
||||||
|
|
||||||
|
#region Input, output & error streams and tty
|
||||||
|
|
||||||
|
parameters.Tty = true;
|
||||||
|
parameters.AttachStderr = true;
|
||||||
|
parameters.AttachStdin = true;
|
||||||
|
parameters.AttachStdout = true;
|
||||||
|
parameters.OpenStdin = true;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region CPU
|
||||||
|
|
||||||
|
parameters.HostConfig.CPUQuota = server.Configuration.Cpu * 1000;
|
||||||
|
parameters.HostConfig.CPUPeriod = 100000;
|
||||||
|
parameters.HostConfig.CPUShares = 1024;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Memory & Swap
|
||||||
|
|
||||||
|
var memoryLimit = server.Configuration.Memory;
|
||||||
|
|
||||||
|
// The overhead multiplier gives the container a little bit more memory to prevent crashes
|
||||||
|
var memoryOverhead = memoryLimit + (memoryLimit * 0.05f); // TODO: Config
|
||||||
|
|
||||||
|
long swapLimit = -1;
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
// If swap is enabled globally and not disabled on this server, set swap
|
||||||
|
if (!configuration.Limits.DisableSwap && config.Server.EnableSwap)
|
||||||
|
swapLimit = (long)(memoryOverhead + memoryOverhead * config.Server.SwapMultiplier);
|
||||||
|
co
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Finalize limits by converting and updating the host config
|
||||||
|
parameters.HostConfig.Memory = ByteConverter.FromMegaBytes((long)memoryOverhead, 1000).Bytes;
|
||||||
|
parameters.HostConfig.MemoryReservation = ByteConverter.FromMegaBytes(memoryLimit, 1000).Bytes;
|
||||||
|
parameters.HostConfig.MemorySwap = swapLimit == -1 ? swapLimit : ByteConverter.FromMegaBytes(swapLimit, 1000).Bytes;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Misc Limits
|
||||||
|
|
||||||
|
// -- Other limits
|
||||||
|
parameters.HostConfig.BlkioWeight = 100;
|
||||||
|
//container.HostConfig.PidsLimit = configuration.Limits.PidsLimit;
|
||||||
|
parameters.HostConfig.OomKillDisable = true; //!configuration.Limits.EnableOomKill;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region DNS
|
||||||
|
|
||||||
|
parameters.HostConfig.DNS = /*config.Docker.DnsServers.Any() ? config.Docker.DnsServers :*/ new List<string>()
|
||||||
|
{
|
||||||
|
"1.1.1.1",
|
||||||
|
"9.9.9.9"
|
||||||
|
};
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Tmpfs
|
||||||
|
|
||||||
|
parameters.HostConfig.Tmpfs = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ "/tmp", $"rw,exec,nosuid,size=100M" } // TODO: Config
|
||||||
|
};
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Logging
|
||||||
|
|
||||||
|
parameters.HostConfig.LogConfig = new()
|
||||||
|
{
|
||||||
|
Type = "json-file", // We need to use this provider, as the GetLogs endpoint needs it
|
||||||
|
Config = new Dictionary<string, string>()
|
||||||
|
};
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Labels
|
||||||
|
|
||||||
|
parameters.Labels = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
parameters.Labels.Add("Software", "Moonlight-Panel");
|
||||||
|
parameters.Labels.Add("ServerId", server.Configuration.Id.ToString());
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Dictionary<string, string> ConstructEnv(this Server server)
|
||||||
|
{
|
||||||
|
var config = server.Configuration;
|
||||||
|
|
||||||
|
var result = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
//TODO: Add timezone, add server ip
|
||||||
|
{ "STARTUP", config.StartupCommand },
|
||||||
|
{ "SERVER_MEMORY", config.Memory.ToString() }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.Allocations.Length > 0)
|
||||||
|
{
|
||||||
|
var mainAllocation = config.Allocations.First();
|
||||||
|
|
||||||
|
result.Add("SERVER_IP", mainAllocation.IpAddress);
|
||||||
|
result.Add("SERVER_PORT", mainAllocation.Port.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle allocation variables
|
||||||
|
var i = 1;
|
||||||
|
foreach (var allocation in config.Allocations)
|
||||||
|
{
|
||||||
|
result.Add($"ML_PORT_{i}", allocation.Port.ToString());
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy variables as env vars
|
||||||
|
foreach (var variable in config.Variables)
|
||||||
|
result.Add(variable.Key, variable.Value);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Docker.DotNet;
|
||||||
|
using Docker.DotNet.Models;
|
||||||
|
using MoonlightServers.Daemon.Models;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Daemon.Extensions.ServerExtensions;
|
||||||
|
|
||||||
|
public static class ServerConsoleExtensions
|
||||||
|
{
|
||||||
|
public static async Task Attach(this Server server)
|
||||||
|
{
|
||||||
|
var dockerClient = server.ServiceProvider.GetRequiredService<DockerClient>();
|
||||||
|
|
||||||
|
var stream = await dockerClient.Containers.AttachContainerAsync(server.ContainerId, true,
|
||||||
|
new ContainerAttachParameters()
|
||||||
|
{
|
||||||
|
Stderr = true,
|
||||||
|
Stdin = true,
|
||||||
|
Stdout = true,
|
||||||
|
Stream = true
|
||||||
|
},
|
||||||
|
server.Cancellation.Token
|
||||||
|
);
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
while (!server.Cancellation.Token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var buffer = new byte[1024];
|
||||||
|
|
||||||
|
var readResult = await stream.ReadOutputAsync(
|
||||||
|
buffer,
|
||||||
|
0,
|
||||||
|
buffer.Length,
|
||||||
|
server.Cancellation.Token
|
||||||
|
);
|
||||||
|
|
||||||
|
if(readResult.EOF)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var resizedBuffer = new byte[readResult.Count];
|
||||||
|
Array.Copy(buffer, resizedBuffer, readResult.Count);
|
||||||
|
buffer = new byte[buffer.Length];
|
||||||
|
|
||||||
|
var decodedText = Encoding.UTF8.GetString(resizedBuffer);
|
||||||
|
await server.Console.WriteToOutput(decodedText);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
// Ignored
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
server.Logger.LogWarning("An unhandled error occured while reading from container stream: {e}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ using Docker.DotNet.Models;
|
|||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
using MoonlightServers.Daemon.Configuration;
|
using MoonlightServers.Daemon.Configuration;
|
||||||
using MoonlightServers.Daemon.Models;
|
using MoonlightServers.Daemon.Models;
|
||||||
|
using MoonlightServers.DaemonShared.Enums;
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Helpers;
|
namespace MoonlightServers.Daemon.Helpers;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Docker.DotNet.Models;
|
using Docker.DotNet.Models;
|
||||||
|
using Mono.Unix.Native;
|
||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
using MoonlightServers.Daemon.Configuration;
|
using MoonlightServers.Daemon.Configuration;
|
||||||
using MoonlightServers.Daemon.Models.Cache;
|
using MoonlightServers.Daemon.Models.Cache;
|
||||||
@@ -42,8 +43,20 @@ public static class ServerConfigurationHelper
|
|||||||
parameters.WorkingDir = "/home/container";
|
parameters.WorkingDir = "/home/container";
|
||||||
|
|
||||||
// - User
|
// - User
|
||||||
//TODO: use config
|
var userId = Syscall.getuid();
|
||||||
parameters.User = $"998:998";
|
|
||||||
|
if (userId == 0)
|
||||||
|
{
|
||||||
|
// We are running as root, so we need to run the container as another user and chown the files when we make changes
|
||||||
|
parameters.User = $"998:998";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// We are not running as root, so we start the container as the same user,
|
||||||
|
// as we are not able to chown the container content to a different user
|
||||||
|
parameters.User = $"{userId}:{userId}";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// -- Mounts
|
// -- Mounts
|
||||||
parameters.HostConfig.Mounts = new List<Mount>();
|
parameters.HostConfig.Mounts = new List<Mount>();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public class StateMachine<T> where T : struct, Enum
|
|||||||
CurrentState = initialState;
|
CurrentState = initialState;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddTransition(T from, T to, T? onError, Func<Task> fun)
|
public void AddTransition(T from, T to, T? onError, Func<Task>? fun)
|
||||||
{
|
{
|
||||||
Transitions.Add(new()
|
Transitions.Add(new()
|
||||||
{
|
{
|
||||||
@@ -26,6 +26,7 @@ public class StateMachine<T> where T : struct, Enum
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void AddTransition(T from, T to, Func<Task> fun) => AddTransition(from, to, null, 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)
|
public async Task TransitionTo(T to)
|
||||||
{
|
{
|
||||||
@@ -41,7 +42,11 @@ public class StateMachine<T> where T : struct, Enum
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
transition.OnTransitioning.Invoke().Wait();
|
if(transition.OnTransitioning != null)
|
||||||
|
transition.OnTransitioning.Invoke().Wait();
|
||||||
|
|
||||||
|
// Successfully executed => update state
|
||||||
|
CurrentState = transition.To;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@@ -64,6 +69,6 @@ public class StateMachine<T> where T : struct, Enum
|
|||||||
public T From { get; set; }
|
public T From { get; set; }
|
||||||
public T To { get; set; }
|
public T To { get; set; }
|
||||||
public T? OnError { get; set; }
|
public T? OnError { get; set; }
|
||||||
public Func<Task> OnTransitioning { get; set; }
|
public Func<Task>? OnTransitioning { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,8 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using MoonCore.Exceptions;
|
using MoonCore.Exceptions;
|
||||||
using MoonlightServers.Daemon.Models;
|
using MoonlightServers.Daemon.Models;
|
||||||
using MoonlightServers.Daemon.Services;
|
using MoonlightServers.Daemon.Services;
|
||||||
|
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
|
||||||
|
using MoonlightServers.DaemonShared.Enums;
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
|
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
|
||||||
|
|
||||||
@@ -16,7 +18,21 @@ public class ServersController : Controller
|
|||||||
ServerService = serverService;
|
ServerService = serverService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{serverId}/start")]
|
[HttpGet("{serverId:int}/status")]
|
||||||
|
public async Task<ServerStatusResponse> GetStatus(int serverId)
|
||||||
|
{
|
||||||
|
var server = ServerService.GetServer(serverId);
|
||||||
|
|
||||||
|
if (server == null)
|
||||||
|
throw new HttpApiException("No server with this id found", 404);
|
||||||
|
|
||||||
|
return new ServerStatusResponse()
|
||||||
|
{
|
||||||
|
State = server.State
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{serverId:int}/start")]
|
||||||
public async Task Start(int serverId)
|
public async Task Start(int serverId)
|
||||||
{
|
{
|
||||||
var server = ServerService.GetServer(serverId);
|
var server = ServerService.GetServer(serverId);
|
||||||
|
|||||||
@@ -1,12 +1,62 @@
|
|||||||
|
using MoonCore.Helpers;
|
||||||
|
using MoonlightServers.Daemon.Configuration;
|
||||||
using MoonlightServers.Daemon.Helpers;
|
using MoonlightServers.Daemon.Helpers;
|
||||||
using MoonlightServers.Daemon.Models.Cache;
|
using MoonlightServers.Daemon.Models.Cache;
|
||||||
|
using MoonlightServers.DaemonShared.Enums;
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Models;
|
namespace MoonlightServers.Daemon.Models;
|
||||||
|
|
||||||
public class Server
|
public class Server
|
||||||
{
|
{
|
||||||
|
public ILogger Logger { get; set; }
|
||||||
|
public ServerConsole Console { get; set; }
|
||||||
|
public IServiceProvider ServiceProvider { get; set; }
|
||||||
public ServerState State => StateMachine.CurrentState;
|
public ServerState State => StateMachine.CurrentState;
|
||||||
public StateMachine<ServerState> StateMachine { get; set; }
|
public StateMachine<ServerState> StateMachine { get; set; }
|
||||||
public ServerConfiguration Configuration { get; set; }
|
public ServerConfiguration Configuration { get; set; }
|
||||||
public string? ContainerId { get; set; }
|
public string? ContainerId { get; set; }
|
||||||
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
41
MoonlightServers.Daemon/Models/ServerConsole.cs
Normal file
41
MoonlightServers.Daemon/Models/ServerConsole.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
namespace MoonlightServers.Daemon.Models;
|
||||||
|
|
||||||
|
public class ServerConsole
|
||||||
|
{
|
||||||
|
public event Func<string, Task> OnOutput;
|
||||||
|
public event Func<string, Task> OnInput;
|
||||||
|
|
||||||
|
public string[] Messages => GetMessages();
|
||||||
|
private readonly Queue<string> MessageCache = new();
|
||||||
|
private const int MaxMessagesInCache = 250; //TODO: Config
|
||||||
|
|
||||||
|
public async Task WriteToOutput(string content)
|
||||||
|
{
|
||||||
|
lock (MessageCache)
|
||||||
|
{
|
||||||
|
MessageCache.Enqueue(content);
|
||||||
|
|
||||||
|
if (MessageCache.Count > MaxMessagesInCache)
|
||||||
|
MessageCache.Dequeue();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OnOutput != null)
|
||||||
|
{
|
||||||
|
await OnOutput
|
||||||
|
.Invoke(content)
|
||||||
|
.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task WriteToInput(string content)
|
||||||
|
{
|
||||||
|
if (OnInput != null)
|
||||||
|
await OnInput.Invoke(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string[] GetMessages()
|
||||||
|
{
|
||||||
|
lock (MessageCache)
|
||||||
|
return MessageCache.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,11 +10,11 @@
|
|||||||
<PackageReference Include="Docker.DotNet" Version="3.125.15" />
|
<PackageReference Include="Docker.DotNet" Version="3.125.15" />
|
||||||
<PackageReference Include="MoonCore" Version="1.8.1" />
|
<PackageReference Include="MoonCore" Version="1.8.1" />
|
||||||
<PackageReference Include="MoonCore.Extended" Version="1.2.4" />
|
<PackageReference Include="MoonCore.Extended" Version="1.2.4" />
|
||||||
|
<PackageReference Include="MoonCore.Unix" Version="1.0.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2"/>
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Extensions\" />
|
|
||||||
<Folder Include="Http\Middleware\" />
|
<Folder Include="Http\Middleware\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@@ -22,4 +22,14 @@
|
|||||||
<ProjectReference Include="..\MoonlightServers.DaemonShared\MoonlightServers.DaemonShared.csproj" />
|
<ProjectReference Include="..\MoonlightServers.DaemonShared\MoonlightServers.DaemonShared.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<_ContentIncludedByDefault Remove="storage\volumes\11\banned-ips.json" />
|
||||||
|
<_ContentIncludedByDefault Remove="storage\volumes\11\banned-players.json" />
|
||||||
|
<_ContentIncludedByDefault Remove="storage\volumes\11\ops.json" />
|
||||||
|
<_ContentIncludedByDefault Remove="storage\volumes\11\plugins\spark\config.json" />
|
||||||
|
<_ContentIncludedByDefault Remove="storage\volumes\11\usercache.json" />
|
||||||
|
<_ContentIncludedByDefault Remove="storage\volumes\11\version_history.json" />
|
||||||
|
<_ContentIncludedByDefault Remove="storage\volumes\11\whitelist.json" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ public class ApplicationStateService : IHostedLifecycleService
|
|||||||
public Task StoppedAsync(CancellationToken cancellationToken)
|
public Task StoppedAsync(CancellationToken cancellationToken)
|
||||||
=> Task.CompletedTask;
|
=> Task.CompletedTask;
|
||||||
|
|
||||||
public Task StoppingAsync(CancellationToken cancellationToken)
|
public async Task StoppingAsync(CancellationToken cancellationToken)
|
||||||
=> Task.CompletedTask;
|
{
|
||||||
|
Logger.LogInformation("Stopping services");
|
||||||
|
|
||||||
|
await ServerService.Stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
using Docker.DotNet;
|
||||||
|
using Docker.DotNet.Models;
|
||||||
using MoonCore.Attributes;
|
using MoonCore.Attributes;
|
||||||
using MoonCore.Helpers;
|
|
||||||
using MoonCore.Models;
|
using MoonCore.Models;
|
||||||
using MoonlightServers.Daemon.Helpers;
|
using MoonlightServers.Daemon.Extensions.ServerExtensions;
|
||||||
using MoonlightServers.Daemon.Models;
|
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;
|
||||||
@@ -16,6 +18,7 @@ public class ServerService
|
|||||||
private readonly RemoteService RemoteService;
|
private readonly RemoteService RemoteService;
|
||||||
private readonly IServiceProvider ServiceProvider;
|
private readonly IServiceProvider ServiceProvider;
|
||||||
private bool IsInitialized = false;
|
private bool IsInitialized = false;
|
||||||
|
private CancellationTokenSource Cancellation = new();
|
||||||
|
|
||||||
public ServerService(RemoteService remoteService, ILogger<ServerService> logger, IServiceProvider serviceProvider)
|
public ServerService(RemoteService remoteService, ILogger<ServerService> logger, IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
@@ -68,30 +71,106 @@ public class ServerService
|
|||||||
|
|
||||||
foreach (var configuration in configurations)
|
foreach (var configuration in configurations)
|
||||||
await InitializeServer(configuration);
|
await InitializeServer(configuration);
|
||||||
|
|
||||||
|
// Attach to docker events
|
||||||
|
await AttachToDockerEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task Stop()
|
||||||
|
{
|
||||||
|
Server[] servers;
|
||||||
|
|
||||||
|
lock (Servers)
|
||||||
|
servers = Servers.ToArray();
|
||||||
|
|
||||||
|
Logger.LogTrace("Canceling server sub tasks");
|
||||||
|
|
||||||
|
foreach (var server in servers)
|
||||||
|
await server.Cancellation.CancelAsync();
|
||||||
|
|
||||||
|
Logger.LogTrace("Canceling own tasks");
|
||||||
|
await Cancellation.CancelAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AttachToDockerEvents()
|
||||||
|
{
|
||||||
|
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
// This lets the event monitor restart
|
||||||
|
while (!Cancellation.Token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Attached to docker events");
|
||||||
|
|
||||||
|
await dockerClient.System.MonitorEventsAsync(new ContainerEventsParameters(),
|
||||||
|
new Progress<Message>(async message =>
|
||||||
|
{
|
||||||
|
if(message.Action != "die")
|
||||||
|
return;
|
||||||
|
|
||||||
|
Server? server;
|
||||||
|
|
||||||
|
lock (Servers)
|
||||||
|
server = Servers.FirstOrDefault(x => x.ContainerId == message.ID);
|
||||||
|
|
||||||
|
// TODO: Maybe implement a lookup for containers which id isn't set in the cache
|
||||||
|
|
||||||
|
if(server == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await server.StateMachine.TransitionTo(ServerState.Offline);
|
||||||
|
}), Cancellation.Token);
|
||||||
|
}
|
||||||
|
catch(TaskCanceledException){} // Can be ignored
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError("An unhandled error occured while attaching to docker events: {e}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async Task InitializeServer(ServerConfiguration configuration)
|
private async Task InitializeServer(ServerConfiguration configuration)
|
||||||
{
|
{
|
||||||
Logger.LogInformation("Initializing server '{id}'", configuration.Id);
|
Logger.LogTrace("Initializing server '{id}'", configuration.Id);
|
||||||
|
|
||||||
|
var loggerFactory = ServiceProvider.GetRequiredService<ILoggerFactory>();
|
||||||
|
|
||||||
var server = new Server()
|
var server = new Server()
|
||||||
{
|
{
|
||||||
Configuration = configuration,
|
Configuration = configuration,
|
||||||
StateMachine = new(ServerState.Offline)
|
StateMachine = new(ServerState.Offline),
|
||||||
|
ServiceProvider = ServiceProvider,
|
||||||
|
Logger = loggerFactory.CreateLogger($"Server {configuration.Id}"),
|
||||||
|
Console = new(),
|
||||||
|
Cancellation = new()
|
||||||
};
|
};
|
||||||
|
|
||||||
server.StateMachine.OnError += (state, exception) =>
|
server.StateMachine.OnError += (state, exception) =>
|
||||||
{
|
{
|
||||||
Logger.LogError("Server {id} encountered an unhandled error while transitioning to {state}: {e}",
|
server.Logger.LogError("Encountered an unhandled error while transitioning to {state}: {e}",
|
||||||
server.Configuration.Id,
|
|
||||||
state,
|
state,
|
||||||
exception
|
exception
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
server.StateMachine.OnTransitioned += state =>
|
||||||
|
{
|
||||||
|
server.Logger.LogInformation("State: {state}", state);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
};
|
||||||
|
|
||||||
server.StateMachine.AddTransition(ServerState.Offline, ServerState.Starting, ServerState.Offline, async () =>
|
server.StateMachine.AddTransition(ServerState.Offline, ServerState.Starting, ServerState.Offline, async () =>
|
||||||
await ServerActionHelper.Start(server, ServiceProvider)
|
await server.StateMachineHandler_Start()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
server.StateMachine.AddTransition(ServerState.Starting, ServerState.Offline);
|
||||||
|
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);
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using MoonlightServers.DaemonShared.Enums;
|
||||||
|
|
||||||
|
namespace MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
|
||||||
|
|
||||||
|
public class ServerStatusResponse
|
||||||
|
{
|
||||||
|
public ServerState State { get; set; }
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace MoonlightServers.Daemon.Models;
|
namespace MoonlightServers.DaemonShared.Enums;
|
||||||
|
|
||||||
public enum ServerState
|
public enum ServerState
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace MoonlightServers.Daemon.Models;
|
namespace MoonlightServers.DaemonShared.Enums;
|
||||||
|
|
||||||
public enum ServerTask
|
public enum ServerTask
|
||||||
{
|
{
|
||||||
@@ -7,5 +7,6 @@ public enum ServerTask
|
|||||||
PullingDockerImage = 2,
|
PullingDockerImage = 2,
|
||||||
RemovingContainer = 3,
|
RemovingContainer = 3,
|
||||||
CreatingContainer = 4,
|
CreatingContainer = 4,
|
||||||
StartingContainer = 5
|
StartingContainer = 5,
|
||||||
|
StoppingContainer = 6
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user