Recreated plugin with new project template. Started implementing server system daemon

This commit is contained in:
2026-03-01 21:09:29 +01:00
parent f6b71f4de6
commit 52dbd13fb5
350 changed files with 2795 additions and 21553 deletions

View File

@@ -1,72 +0,0 @@
namespace MoonlightServers.Daemon.Configuration;
public class AppConfiguration
{
public DockerData Docker { get; set; } = new();
public StorageData Storage { get; set; } = new();
public SecurityData Security { get; set; } = new();
public RemoteData Remote { get; set; } = new();
public FilesData Files { get; set; } = new();
public ServerData Server { get; set; } = new();
public KestrelData Kestrel { get; set; } = new();
public class KestrelData
{
public int RequestBodySizeLimit { get; set; } = 25;
}
public class RemoteData
{
public string Url { get; set; }
}
public class DockerData
{
public string Uri { get; set; } = "unix:///var/run/docker.sock";
public DockerCredentialData[] Credentials { get; set; } = [];
public class DockerCredentialData
{
public string Domain { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string Email { get; set; }
}
}
public class SecurityData
{
public string Token { get; set; }
public string TokenId { get; set; }
}
public class StorageData
{
public string Volumes { get; set; } = Path.Combine("storage", "volumes");
public string VirtualDisks { get; set; } = Path.Combine("storage", "virtualDisks");
public string Backups { get; set; } = Path.Combine("storage", "backups");
public string Install { get; set; } =Path.Combine("storage", "install");
public VirtualDiskData VirtualDiskOptions { get; set; } = new();
}
public record VirtualDiskData
{
public string FileSystemType { get; set; } = "ext4";
public string E2FsckParameters { get; set; } = "-pf";
}
public class FilesData
{
public int UploadSizeLimit { get; set; } = 1024 * 2;
public int UploadChunkSize { get; set; } = 20;
}
public class ServerData
{
public int WaitBeforeKillSeconds { get; set; } = 30;
public int TmpFsSize { get; set; } = 100;
public float MemoryOverheadMultiplier { get; set; } = 0.05f;
public int ConsoleMessageCacheLimit { get; set; } = 250;
}
}

View File

@@ -1,10 +0,0 @@
namespace MoonlightServers.Daemon.Enums;
public enum ServerState
{
Offline = 0,
Starting = 1,
Online = 2,
Stopping = 3,
Installing = 4
}

View File

@@ -1,14 +0,0 @@
namespace MoonlightServers.Daemon.Enums;
public enum ServerTrigger
{
Start = 0,
Stop = 1,
Restart = 2,
Kill = 3,
Reinstall = 4,
NotifyOnline = 5,
NotifyRuntimeContainerDied = 6,
NotifyInstallationContainerDied = 7,
NotifyInternalError = 8
}

View File

@@ -1,359 +0,0 @@
using Docker.DotNet.Models;
using Mono.Unix.Native;
using MoonCore.Helpers;
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.Models.Cache;
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
namespace MoonlightServers.Daemon.Extensions;
public static class ServerConfigurationExtensions
{
public static ServerConfiguration ToServerConfiguration(this ServerDataResponse response)
{
return new ServerConfiguration()
{
Id = response.Id,
StartupCommand = response.StartupCommand,
Allocations = response.Allocations.Select(y => new ServerConfiguration.AllocationConfiguration()
{
IpAddress = y.IpAddress,
Port = y.Port
}).ToArray(),
Variables = response.Variables,
OnlineDetection = response.OnlineDetection,
DockerImage = response.DockerImage,
Cpu = response.Cpu,
Disk = response.Disk,
Memory = response.Memory,
StopCommand = response.StopCommand
};
}
public static CreateContainerParameters ToRuntimeCreateParameters(
this ServerConfiguration configuration,
AppConfiguration appConfiguration,
string hostPath,
string containerName
)
{
var parameters = configuration.ToSharedCreateParameters(appConfiguration);
#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 = containerName;
parameters.Hostname = containerName;
#endregion
#region Docker Image
parameters.Image = configuration.DockerImage;
#endregion
#region Environment
parameters.Env = configuration.ToEnvironmentVariables()
.Select(x => $"{x.Key}={x.Value}")
.ToList();
#endregion
#region Working Dir
parameters.WorkingDir = "/home/container";
#endregion
#region User
var userId = Syscall.getuid(); // TODO: Extract to external service?
if (userId == 0)
userId = 998;
parameters.User = $"{userId}:{userId}";
/*
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 = hostPath,
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 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 ToInstallationCreateParameters(
this ServerConfiguration configuration,
AppConfiguration appConfiguration,
string runtimeHostPath,
string installationHostPath,
string containerName,
string installDockerImage,
string installShell
)
{
var parameters = configuration.ToSharedCreateParameters(appConfiguration);
// - Name
parameters.Name = containerName;
parameters.Hostname = containerName;
// - Image
parameters.Image = installDockerImage;
// - Env
parameters.Env = configuration
.ToEnvironmentVariables()
.Select(x => $"{x.Key}={x.Value}")
.ToList();
// -- Working directory
parameters.WorkingDir = "/mnt/server";
// - User
// Note: Some images might not work if we set a user here
var userId = Syscall.getuid();
// If we are root, we are able to change owner permissions after the installation
// so we run the installation as root, otherwise we need to run it as our current user,
// so we are able to access the files created by the installer
if (userId == 0)
parameters.User = "0:0";
else
parameters.User = $"{userId}:{userId}";
// -- Mounts
parameters.HostConfig.Mounts = new List<Mount>();
parameters.HostConfig.Mounts.Add(new()
{
Source = runtimeHostPath,
Target = "/mnt/server",
ReadOnly = false,
Type = "bind"
});
parameters.HostConfig.Mounts.Add(new()
{
Source = installationHostPath,
Target = "/mnt/install",
ReadOnly = false,
Type = "bind"
});
parameters.Cmd = [installShell, "/mnt/install/install.sh"];
return parameters;
}
private static CreateContainerParameters ToSharedCreateParameters(this ServerConfiguration configuration,
AppConfiguration appConfiguration)
{
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 = configuration.Cpu * 1000;
parameters.HostConfig.CPUPeriod = 100000;
parameters.HostConfig.CPUShares = 1024;
#endregion
#region Memory & Swap
var memoryLimit = configuration.Memory;
// The overhead multiplier gives the container a little bit more memory to prevent crashes
var memoryOverhead = memoryLimit + (memoryLimit * appConfiguration.Server.MemoryOverheadMultiplier);
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
// TODO: Read hosts dns settings?
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={appConfiguration.Server.TmpFsSize}M" }
};
#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", configuration.Id.ToString());
#endregion
return parameters;
}
public static Dictionary<string, string> ToEnvironmentVariables(this ServerConfiguration configuration)
{
var result = new Dictionary<string, string>
{
//TODO: Add timezone, add server ip
{ "STARTUP", configuration.StartupCommand },
{ "SERVER_MEMORY", configuration.Memory.ToString() }
};
if (configuration.Allocations.Length > 0)
{
var mainAllocation = configuration.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 configuration.Allocations)
{
result.Add($"ML_PORT_{i}", allocation.Port.ToString());
i++;
}
// Copy variables as env vars
foreach (var variable in configuration.Variables)
result.Add(variable.Key, variable.Value);
return result;
}
}

View File

@@ -1,66 +0,0 @@
using Stateless;
namespace MoonlightServers.Daemon.Extensions;
public static class StateConfigurationExtensions
{
public static StateMachine<TState, TTrigger>.StateConfiguration OnExitFrom<TState, TTrigger>(
this StateMachine<TState, TTrigger>.StateConfiguration configuration, TTrigger trigger, Action entryAction
)
{
configuration.OnExit(transition =>
{
if(!transition.Trigger!.Equals(trigger))
return;
entryAction.Invoke();
});
return configuration;
}
public static StateMachine<TState, TTrigger>.StateConfiguration OnExitFrom<TState, TTrigger>(
this StateMachine<TState, TTrigger>.StateConfiguration configuration, TTrigger trigger, Action<StateMachine<TState, TTrigger>.Transition> entryAction
)
{
configuration.OnExit(transition =>
{
if(!transition.Trigger!.Equals(trigger))
return;
entryAction.Invoke(transition);
});
return configuration;
}
public static StateMachine<TState, TTrigger>.StateConfiguration OnExitFromAsync<TState, TTrigger>(
this StateMachine<TState, TTrigger>.StateConfiguration configuration, TTrigger trigger, Func<Task> entryAction
)
{
configuration.OnExitAsync(transition =>
{
if(!transition.Trigger!.Equals(trigger))
return Task.CompletedTask;
return entryAction.Invoke();
});
return configuration;
}
public static StateMachine<TState, TTrigger>.StateConfiguration OnExitFromAsync<TState, TTrigger>(
this StateMachine<TState, TTrigger>.StateConfiguration configuration, TTrigger trigger, Func<StateMachine<TState, TTrigger>.Transition, Task> entryAction
)
{
configuration.OnExitAsync(transition =>
{
if(!transition.Trigger!.Equals(trigger))
return Task.CompletedTask;
return entryAction.Invoke(transition);
});
return configuration;
}
}

View File

@@ -0,0 +1,76 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Console;
namespace MoonlightServers.Daemon.Helpers;
public class AppConsoleFormatter : ConsoleFormatter
{
private const string TimestampColor = "\e[38;2;148;148;148m";
private const string CategoryColor = "\e[38;2;198;198;198m";
private const string MessageColor = "\e[38;2;255;255;255m";
private const string Bold = "\e[1m";
// Pre-computed ANSI color codes for each log level
private const string CriticalColor = "\e[38;2;255;0;0m";
private const string ErrorColor = "\e[38;2;255;0;0m";
private const string WarningColor = "\e[38;2;215;215;0m";
private const string InfoColor = "\e[38;2;135;215;255m";
private const string DebugColor = "\e[38;2;198;198;198m";
private const string TraceColor = "\e[38;2;68;68;68m";
public AppConsoleFormatter() : base(nameof(AppConsoleFormatter))
{
}
public override void Write<TState>(
in LogEntry<TState> logEntry,
IExternalScopeProvider? scopeProvider,
TextWriter textWriter)
{
var message = logEntry.Formatter(logEntry.State, logEntry.Exception);
// Timestamp
textWriter.Write(TimestampColor);
textWriter.Write(DateTime.Now.ToString("dd.MM.yy HH:mm:ss"));
textWriter.Write(' ');
// Log level with color and bold
var (levelText, levelColor) = GetLevelInfo(logEntry.LogLevel);
textWriter.Write(levelColor);
textWriter.Write(Bold);
textWriter.Write(levelText);
textWriter.Write(' ');
// Category
textWriter.Write(CategoryColor);
textWriter.Write(logEntry.Category);
// Message
textWriter.Write(MessageColor);
textWriter.Write(": ");
textWriter.Write(message);
// Exception
if (logEntry.Exception != null)
{
textWriter.Write(MessageColor);
textWriter.WriteLine(logEntry.Exception.ToString());
}
else
textWriter.WriteLine();
}
private static (string text, string color) GetLevelInfo(LogLevel logLevel)
{
return logLevel switch
{
LogLevel.Critical => ("CRIT", CriticalColor),
LogLevel.Error => ("ERRO", ErrorColor),
LogLevel.Warning => ("WARN", WarningColor),
LogLevel.Information => ("INFO", InfoColor),
LogLevel.Debug => ("DEBG", DebugColor),
LogLevel.Trace => ("TRCE", TraceColor),
_ => ("NONE", "")
};
}
}

View File

@@ -1,31 +0,0 @@
namespace MoonlightServers.Daemon.Helpers;
public class CompositeServiceProvider : IServiceProvider
{
private readonly List<IServiceProvider> ServiceProviders;
public CompositeServiceProvider(params IServiceProvider[] serviceProviders)
{
ServiceProviders = new List<IServiceProvider>(serviceProviders);
}
public object? GetService(Type serviceType)
{
foreach (var provider in ServiceProviders)
{
try
{
var service = provider.GetService(serviceType);
if (service != null)
return service;
}
catch (InvalidOperationException)
{
// Ignored
}
}
return null;
}
}

View File

@@ -1,263 +0,0 @@
using System.Runtime.InteropServices;
using Mono.Unix.Native;
using MoonCore.Attributes;
using MoonCore.Helpers;
using MoonlightServers.Daemon.Models;
namespace MoonlightServers.Daemon.Helpers;
[Singleton]
public class HostSystemHelper
{
private readonly ILogger<HostSystemHelper> Logger;
public HostSystemHelper(ILogger<HostSystemHelper> logger)
{
Logger = logger;
}
public string GetOsName()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Windows platform detected
var osVersion = Environment.OSVersion.Version;
return $"Windows {osVersion.Major}.{osVersion.Minor}.{osVersion.Build}";
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
var releaseRaw = File
.ReadAllLines("/etc/os-release")
.FirstOrDefault(x => x.StartsWith("PRETTY_NAME="));
if (string.IsNullOrEmpty(releaseRaw))
return "Linux (unknown release)";
var release = releaseRaw
.Replace("PRETTY_NAME=", "")
.Replace("\"", "");
if (string.IsNullOrEmpty(release))
return "Linux (unknown release)";
return release;
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
// macOS platform detected
var osVersion = Environment.OSVersion.Version;
return $"Shitty macOS {osVersion.Major}.{osVersion.Minor}.{osVersion.Build}";
}
// Unknown platform
return "Unknown";
}
#region CPU Usage
public async Task<CpuUsageDetails> GetCpuUsageAsync()
{
var result = new CpuUsageDetails();
var perCoreUsages = new List<double>();
// Initial read
var (cpuLastStats, cpuLastSums) = await ReadAllCpuStatsAsync();
await Task.Delay(1000);
// Second read
var (cpuNowStats, cpuNowSums) = await ReadAllCpuStatsAsync();
for (var i = 0; i < cpuNowStats.Length; i++)
{
var cpuDelta = cpuNowSums[i] - cpuLastSums[i];
var cpuIdle = cpuNowStats[i][3] - cpuLastStats[i][3];
var cpuUsed = cpuDelta - cpuIdle;
var usage = 100.0 * cpuUsed / cpuDelta;
if (i == 0)
result.OverallUsage = usage;
else
perCoreUsages.Add(usage);
}
result.PerCoreUsage = perCoreUsages.ToArray();
// Get model name
var cpuInfoLines = await File.ReadAllLinesAsync("/proc/cpuinfo");
var modelLine = cpuInfoLines.FirstOrDefault(x => x.StartsWith("model name"));
result.Model = modelLine?.Split(":")[1].Trim() ?? "N/A";
return result;
}
private async Task<(long[][] cpuStatsList, long[] cpuSums)> ReadAllCpuStatsAsync()
{
var lines = await File.ReadAllLinesAsync("/proc/stat");
lines = lines.Where(line => line.StartsWith("cpu"))
.TakeWhile(line => line.StartsWith("cpu")) // Ensures only CPU lines are read
.ToArray();
var statsList = new List<long[]>();
var sumList = new List<long>();
foreach (var line in lines)
{
var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries)
.Skip(1) // Skip the "cpu" label
.ToArray();
var cpuTimes = parts
.Select(long.Parse)
.ToArray();
var sum = cpuTimes.Sum();
statsList.Add(cpuTimes);
sumList.Add(sum);
}
return (statsList.ToArray(), sumList.ToArray());
}
#endregion
#region Memory
public async Task ClearCachedMemoryAsync()
{
await File.WriteAllTextAsync("/proc/sys/vm/drop_caches", "3");
}
public async Task<MemoryUsageDetails> GetMemoryUsageAsync()
{
var details = new MemoryUsageDetails();
var lines = await File.ReadAllLinesAsync("/proc/meminfo");
foreach (var line in lines)
{
// We want to ignore all non kilobyte values
if (!line.Contains("kB"))
continue;
// Split the line up so we can extract the id and the value
// to map it to the model field
var parts = line.Split(":");
var id = parts[0];
var value = parts[1]
.Replace("kB", "")
.Trim();
if (!long.TryParse(value, out var longValue))
continue;
var bytes = ByteConverter.FromKiloBytes(longValue).Bytes;
switch (id)
{
case "MemTotal":
details.Total = bytes;
break;
case "MemFree":
details.Free = bytes;
break;
case "MemAvailable":
details.Available = bytes;
break;
case "Cached":
details.Cached = bytes;
break;
case "SwapTotal":
details.SwapTotal = bytes;
break;
case "SwapFree":
details.SwapFree = bytes;
break;
}
}
return details;
}
#endregion
#region Disks
public async Task<DiskUsageDetails[]> GetDiskUsagesAsync()
{
var details = new List<DiskUsageDetails>();
// First we need to check which mounts actually exist
var diskDevices = new Dictionary<string, string>();
string[] ignoredMounts = ["/boot/efi", "/boot"];
var mountLines = await File.ReadAllLinesAsync("/proc/mounts");
foreach (var mountLine in mountLines)
{
var parts = mountLine.Split(" ");
var device = parts[0];
var mountedAt = parts[1];
// We only want to handle mounted physical devices
if (!device.StartsWith("/dev/"))
continue;
// Ignore certain mounts which we dont want to show
if (ignoredMounts.Contains(mountedAt))
continue;
diskDevices.Add(device, mountedAt);
}
foreach (var diskMount in diskDevices)
{
var device = diskMount.Key;
var mount = diskMount.Value;
var statusCode = Syscall.statvfs(mount, out var statvfs);
if (statusCode != 0)
{
var error = Stdlib.GetLastError();
Logger.LogError(
"An error occured while checking disk stats for mount {mount}: {error}",
mount,
error
);
continue;
}
// Source: https://man7.org/linux/man-pages/man3/statvfs.3.html
var detail = new DiskUsageDetails()
{
Device = device,
MountPath = mount,
DiskTotal = statvfs.f_blocks * statvfs.f_frsize,
DiskFree = statvfs.f_bfree * statvfs.f_frsize,
InodesTotal = statvfs.f_files,
InodesFree = statvfs.f_ffree
};
details.Add(detail);
}
return details.ToArray();
}
#endregion
}

View File

@@ -1,354 +0,0 @@
using System.Text;
using ICSharpCode.SharpZipLib.GZip;
using ICSharpCode.SharpZipLib.Tar;
using ICSharpCode.SharpZipLib.Zip;
using Mono.Unix.Native;
using MoonCore.Unix.SecureFs;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
using MoonlightServers.DaemonShared.Enums;
namespace MoonlightServers.Daemon.Helpers;
public class ServerFileSystem
{
private readonly SecureFileSystem FileSystem;
public ServerFileSystem(SecureFileSystem fileSystem)
{
FileSystem = fileSystem;
}
public Task<ServerFileSystemResponse[]> ListAsync(string inputPath)
{
var path = Normalize(inputPath);
var entries = FileSystem.ReadDir(path);
IEnumerable<SecureFsEntry> entryQuery = entries;
// Filter all lost+found directories on the root of the file system
// to hide the folder shown by virtual disk volumes
if (string.IsNullOrEmpty(inputPath) || inputPath == "/")
entryQuery = entryQuery.Where(x => x.Name != "lost+found");
var result = entryQuery
.Select(x => new ServerFileSystemResponse()
{
Name = x.Name,
IsFolder = x.IsDirectory,
Size = x.Size,
UpdatedAt = x.LastChanged,
CreatedAt = x.CreatedAt
})
.ToArray();
return Task.FromResult(result);
}
public Task MoveAsync(string inputOldPath, string inputNewPath)
{
var oldPath = Normalize(inputOldPath);
var newPath = Normalize(inputNewPath);
FileSystem.Rename(oldPath, newPath);
return Task.CompletedTask;
}
public Task DeleteAsync(string inputPath)
{
var path = Normalize(inputPath);
FileSystem.RemoveAll(path);
return Task.CompletedTask;
}
public Task MkdirAsync(string inputPath)
{
var path = Normalize(inputPath);
FileSystem.MkdirAll(path, FilePermissions.ACCESSPERMS);
return Task.CompletedTask;
}
public Task TouchAsync(string inputPath)
{
var path = Normalize(inputPath);
var parentDirectory = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(parentDirectory) && parentDirectory != "/")
FileSystem.MkdirAll(parentDirectory, FilePermissions.ACCESSPERMS);
FileSystem.OpenFileWrite(
path,
_ => { },
OpenFlags.O_CREAT
); // We use these custom flags to ensure we aren't overwriting the file
return Task.CompletedTask;
}
public Task CreateChunkAsync(string inputPath, long totalSize, long positionToSkip, Stream chunkStream)
{
var path = Normalize(inputPath);
var parentDirectory = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(parentDirectory) && parentDirectory != "/")
FileSystem.MkdirAll(parentDirectory, FilePermissions.ACCESSPERMS);
FileSystem.OpenFileWrite(path, fileStream =>
{
if (fileStream.Length != totalSize)
fileStream.SetLength(totalSize);
fileStream.Position = positionToSkip;
chunkStream.CopyTo(fileStream);
fileStream.Flush();
}, OpenFlags.O_CREAT | OpenFlags.O_RDWR); // We use these custom flags to ensure we aren't overwriting the file
return Task.CompletedTask;
}
public Task CreateAsync(string inputPath, Stream dataStream)
{
var path = Normalize(inputPath);
var parentDirectory = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(parentDirectory) && parentDirectory != "/")
FileSystem.MkdirAll(parentDirectory, FilePermissions.ACCESSPERMS);
FileSystem.OpenFileWrite(path, stream =>
{
stream.Position = 0;
dataStream.CopyTo(stream);
stream.Flush();
});
return Task.CompletedTask;
}
public Task ReadAsync(string inputPath, Func<Stream, Task> onHandle)
{
var path = Normalize(inputPath);
FileSystem.OpenFileRead(path, stream =>
{
// No try catch here because the safe fs abstraction already handles every error occuring in the handle
onHandle.Invoke(stream).Wait();
});
return Task.CompletedTask;
}
#region Compression
public Task CompressAsync(string[] itemsInput, string destinationInput, CompressType type)
{
var destination = Normalize(destinationInput);
var items = itemsInput.Select(Normalize);
if (type == CompressType.Zip)
{
FileSystem.OpenFileWrite(destination, stream =>
{
using var zipStream = new ZipOutputStream(stream);
foreach (var item in items)
AddItemToZip(item, zipStream);
zipStream.Flush();
stream.Flush();
zipStream.Close();
});
}
else if (type == CompressType.TarGz)
{
FileSystem.OpenFileWrite(destination, stream =>
{
using var gzStream = new GZipOutputStream(stream);
using var tarStream = new TarOutputStream(gzStream, Encoding.UTF8);
foreach (var item in items)
AddItemToTar(item, tarStream);
tarStream.Flush();
gzStream.Flush();
stream.Flush();
tarStream.Close();
gzStream.Close();
});
}
return Task.CompletedTask;
}
public Task DecompressAsync(string pathInput, string destinationInput, CompressType type)
{
var path = Normalize(pathInput);
var destination = Normalize(destinationInput);
if (type == CompressType.Zip)
{
FileSystem.OpenFileRead(path, fileStream =>
{
var zipInputStream = new ZipInputStream(fileStream);
ExtractZip(zipInputStream, destination);
});
}
else if (type == CompressType.TarGz)
{
FileSystem.OpenFileRead(path, fileStream =>
{
var gzInputStream = new GZipInputStream(fileStream);
var zipInputStream = new TarInputStream(gzInputStream, Encoding.UTF8);
ExtractTar(zipInputStream, destination);
});
}
return Task.CompletedTask;
}
private void AddItemToZip(string path, ZipOutputStream outputStream)
{
var item = FileSystem.Stat(path);
if (item.IsDirectory)
{
var contents = FileSystem.ReadDir(path);
foreach (var content in contents)
{
AddItemToZip(
Path.Combine(path, content.Name),
outputStream
);
}
}
else
{
var entry = new ZipEntry(path)
{
Size = item.Size,
DateTime = item.LastChanged
};
outputStream.PutNextEntry(entry);
FileSystem.OpenFileRead(path, stream => { stream.CopyTo(outputStream); });
outputStream.CloseEntry();
}
}
private void AddItemToTar(string path, TarOutputStream outputStream)
{
var item = FileSystem.Stat(path);
if (item.IsDirectory)
{
var contents = FileSystem.ReadDir(path);
foreach (var content in contents)
{
AddItemToTar(
Path.Combine(path, content.Name),
outputStream
);
}
}
else
{
var entry = TarEntry.CreateTarEntry(path);
entry.Name = path;
entry.Size = item.Size;
entry.ModTime = item.LastChanged;
outputStream.PutNextEntry(entry);
FileSystem.OpenFileRead(path, stream => { stream.CopyTo(outputStream); });
outputStream.CloseEntry();
}
}
private void ExtractZip(ZipInputStream inputStream, string destination)
{
while (true)
{
var entry = inputStream.GetNextEntry();
if (entry == null)
break;
if (entry.IsDirectory)
continue;
var fileDestination = Path.Combine(destination, entry.Name);
var parentDirectory = Path.GetDirectoryName(fileDestination);
if (!string.IsNullOrEmpty(parentDirectory) && parentDirectory != "/")
FileSystem.MkdirAll(parentDirectory, FilePermissions.ACCESSPERMS);
FileSystem.OpenFileWrite(fileDestination, stream =>
{
stream.Position = 0;
inputStream.CopyTo(stream);
stream.Flush();
}); // This will override the file if it exists
}
}
private void ExtractTar(TarInputStream inputStream, string destination)
{
while (true)
{
var entry = inputStream.GetNextEntry();
if (entry == null)
break;
if (entry.IsDirectory)
continue;
var fileDestination = Path.Combine(destination, entry.Name);
var parentDirectory = Path.GetDirectoryName(fileDestination);
if (!string.IsNullOrEmpty(parentDirectory) && parentDirectory != "/")
FileSystem.MkdirAll(parentDirectory, FilePermissions.ACCESSPERMS);
FileSystem.OpenFileWrite(fileDestination, stream =>
{
stream.Position = 0;
inputStream.CopyTo(stream);
stream.Flush();
}); // This will override the file if it exists
}
}
#endregion
private string Normalize(string path)
{
return path
.Replace("//", "/")
.Replace("..", "")
.TrimStart('/');
}
}

View File

@@ -1,8 +0,0 @@
using Microsoft.AspNetCore.Authentication;
namespace MoonlightServers.Daemon.Helpers;
public class TokenAuthOptions : AuthenticationSchemeOptions
{
public string Token { get; set; }
}

View File

@@ -1,49 +0,0 @@
using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
namespace MoonlightServers.Daemon.Helpers;
public class TokenAuthScheme : AuthenticationHandler<TokenAuthOptions>
{
public TokenAuthScheme(IOptionsMonitor<TokenAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder,
ISystemClock clock) : base(options, logger, encoder, clock)
{
}
public TokenAuthScheme(IOptionsMonitor<TokenAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder) : base(
options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.ContainsKey("Authorization"))
return Task.FromResult(AuthenticateResult.NoResult());
var authHeaderValue = Request.Headers["Authorization"].FirstOrDefault();
if (string.IsNullOrEmpty(authHeaderValue))
return Task.FromResult(AuthenticateResult.NoResult());
if (!authHeaderValue.Contains("Bearer "))
return Task.FromResult(AuthenticateResult.NoResult());
var providedToken = authHeaderValue
.Replace("Bearer ", "")
.Trim();
if (providedToken != Options.Token)
return Task.FromResult(AuthenticateResult.NoResult());
return Task.FromResult(AuthenticateResult.Success(
new AuthenticationTicket(
new ClaimsPrincipal(
new ClaimsIdentity("token")
),
"token"
)
));
}
}

View File

@@ -1,46 +0,0 @@
using System.Net.Sockets;
using System.Text.Json;
using MoonCore.Attributes;
using MoonCore.Helpers;
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.Models.UnsafeDocker;
namespace MoonlightServers.Daemon.Helpers;
[Singleton]
public class UnsafeDockerClient
{
private readonly AppConfiguration Configuration;
public UnsafeDockerClient(AppConfiguration configuration)
{
Configuration = configuration;
}
public Task<HttpClient> CreateHttpClientAsync()
{
var client = new HttpClient(new SocketsHttpHandler()
{
ConnectCallback = async (context, token) =>
{
var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP);
var endpoint = new UnixDomainSocketEndPoint(
Formatter.ReplaceStart(Configuration.Docker.Uri, "unix://", "")
);
await socket.ConnectAsync(endpoint, token);
return new NetworkStream(socket, ownsSocket: true);
}
});
return Task.FromResult(client);
}
public async Task<DataUsageResponse> GetDataUsageAsync()
{
using var client = await CreateHttpClientAsync();
var responseJson = await client.GetStringAsync("http://some.random.domain/v1.47/system/df");
var response = JsonSerializer.Deserialize<DataUsageResponse>(responseJson)!;
return response;
}
}

View File

@@ -1,77 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using MoonlightServers.Daemon.ServerSystem.Enums;
using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
[ApiController]
[Route("api/servers/{id:int}")]
public class PowerController : Controller
{
private readonly ServerService ServerService;
public PowerController(ServerService serverService)
{
ServerService = serverService;
}
[HttpPost("start")]
public async Task<ActionResult> StartAsync([FromRoute] int id)
{
var server = ServerService.GetById(id);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
if (!server.StateMachine.CanFire(ServerTrigger.Start))
return Problem("Cannot fire start trigger in this state");
await server.StateMachine.FireAsync(ServerTrigger.Start);
return NoContent();
}
[HttpPost("stop")]
public async Task<ActionResult> StopAsync([FromRoute] int id)
{
var server = ServerService.GetById(id);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
if (!server.StateMachine.CanFire(ServerTrigger.Stop))
return Problem("Cannot fire stop trigger in this state");
await server.StateMachine.FireAsync(ServerTrigger.Stop);
return NoContent();
}
[HttpPost("kill")]
public async Task<ActionResult> KillAsync([FromRoute] int id)
{
var server = ServerService.GetById(id);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
if (!server.StateMachine.CanFire(ServerTrigger.Kill))
return Problem("Cannot fire kill trigger in this state");
await server.StateMachine.FireAsync(ServerTrigger.Kill);
return NoContent();
}
[HttpPost("install")]
public async Task<ActionResult> InstallAsync([FromRoute] int id)
{
var server = ServerService.GetById(id);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
if (!server.StateMachine.CanFire(ServerTrigger.Install))
return Problem("Cannot fire install trigger in this state");
await server.StateMachine.FireAsync(ServerTrigger.Install);
return NoContent();
}
}

View File

@@ -1,67 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using MoonlightServers.Daemon.Mappers;
using MoonlightServers.Daemon.Services;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
using MoonlightServers.DaemonShared.Enums;
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
[ApiController]
[Route("api/servers/{id:int}")]
public class ServersController : Controller
{
private readonly ServerService ServerService;
private readonly ServerConfigurationMapper ConfigurationMapper;
public ServersController(ServerService serverService, ServerConfigurationMapper configurationMapper)
{
ServerService = serverService;
ConfigurationMapper = configurationMapper;
}
[HttpPost("sync")]
public async Task<ActionResult> SyncAsync([FromRoute] int id)
{
await ServerService.InitializeByIdAsync(id);
return NoContent();
}
[HttpGet("status")]
public async Task<ActionResult<ServerStatusResponse>> StatusAsync([FromRoute] int id)
{
var server = ServerService.GetById(id);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
return new ServerStatusResponse()
{
State = (ServerState)server.StateMachine.State
};
}
[HttpGet("logs")]
public async Task<ActionResult<ServerLogsResponse>> LogsAsync([FromRoute] int id)
{
var server = ServerService.GetById(id);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
var messages = await server.Console.GetCacheAsync();
return new ServerLogsResponse()
{
Messages = messages.ToArray()
};
}
[HttpGet("stats")]
public async Task<ServerStatsResponse> GetStatsAsync([FromRoute] int id)
{
return new ServerStatsResponse()
{
};
}
}

View File

@@ -1,54 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonlightServers.Daemon.Helpers;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Statistics;
namespace MoonlightServers.Daemon.Http.Controllers.Statistics;
[Authorize]
[ApiController]
[Route("api/statistics")]
public class StatisticsController : Controller
{
private readonly HostSystemHelper HostSystemHelper;
public StatisticsController(HostSystemHelper hostSystemHelper)
{
HostSystemHelper = hostSystemHelper;
}
[HttpGet]
public async Task<StatisticsResponse> GetAsync()
{
var response = new StatisticsResponse();
var cpuUsage = await HostSystemHelper.GetCpuUsageAsync();
response.Cpu.Model = cpuUsage.Model;
response.Cpu.Usage = cpuUsage.OverallUsage;
response.Cpu.UsagePerCore = cpuUsage.PerCoreUsage;
var memoryUsage = await HostSystemHelper.GetMemoryUsageAsync();
response.Memory.Available = memoryUsage.Available;
response.Memory.Cached = memoryUsage.Cached;
response.Memory.Free = memoryUsage.Free;
response.Memory.Total = memoryUsage.Total;
response.Memory.SwapTotal = memoryUsage.SwapTotal;
response.Memory.SwapFree = memoryUsage.SwapFree;
var diskDetails = await HostSystemHelper.GetDiskUsagesAsync();
response.Disks = diskDetails.Select(x => new StatisticsResponse.DiskData()
{
Device = x.Device,
MountPath = x.MountPath,
DiskFree = x.DiskFree,
DiskTotal = x.DiskTotal,
InodesFree = x.InodesFree,
InodesTotal = x.InodesTotal
}).ToArray();
return response;
}
}

View File

@@ -1,38 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonlightServers.Daemon.Services;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Statistics;
namespace MoonlightServers.Daemon.Http.Controllers.Statistics;
// This controller hosts endpoints for the statistics for the docker environment
[Authorize]
[ApiController]
[Route("api/statistics/docker")]
public class StatisticsDockerController : Controller
{
private readonly DockerInfoService DockerInfoService;
public StatisticsDockerController(DockerInfoService dockerInfoService)
{
DockerInfoService = dockerInfoService;
}
[HttpGet]
public async Task<StatisticsDockerResponse> GetAsync()
{
var usage = await DockerInfoService.GetDataUsageAsync();
return new StatisticsDockerResponse
{
Version = await DockerInfoService.GetDockerVersionAsync(),
ContainersReclaimable = usage.Containers.Reclaimable,
ContainersUsed = usage.Containers.Used,
BuildCacheReclaimable = usage.BuildCache.Reclaimable,
BuildCacheUsed = usage.BuildCache.Used,
ImagesUsed = usage.Images.Used,
ImagesReclaimable = usage.Images.Reclaimable
};
}
}

View File

@@ -1,56 +0,0 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MoonlightServers.Daemon.Services;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Sys;
namespace MoonlightServers.Daemon.Http.Controllers.Sys;
[Authorize]
[ApiController]
[Route("api/system/status")]
public class SystemStatusController : Controller
{
private readonly RemoteService RemoteService;
public SystemStatusController(RemoteService remoteService)
{
RemoteService = remoteService;
}
public async Task<SystemStatusResponse> GetAsync()
{
SystemStatusResponse response;
var sw = new Stopwatch();
sw.Start();
try
{
await RemoteService.GetStatusAsync();
sw.Stop();
response = new()
{
TripSuccess = true,
TripTime = sw.Elapsed,
Version = "2.1.0" // TODO: Set global
};
}
catch (Exception e)
{
sw.Stop();
response = new()
{
TripError = e.Message,
TripTime = sw.Elapsed,
TripSuccess = false,
Version = "2.1.0" // TODO: Set global
};
}
return response;
}
}

View File

@@ -1,28 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
namespace MoonlightServers.Daemon.Http.Hubs;
[Authorize(AuthenticationSchemes = "accessToken", Policy = "serverWebsocket")]
public class ServerWebSocketHub : Hub
{
private readonly ILogger<ServerWebSocketHub> Logger;
public ServerWebSocketHub(ILogger<ServerWebSocketHub> logger)
{
Logger = logger;
}
public override async Task OnConnectedAsync()
{
// The policies validated already the type and the token so we can assume we are authenticated
// and just start adding ourselves into the desired group
var serverId = Context.User!.Claims.First(x => x.Type == "serverId").Value;
await Groups.AddToGroupAsync(
Context.ConnectionId,
serverId
);
}
}

View File

@@ -1,363 +0,0 @@
using Docker.DotNet.Models;
using Mono.Unix.Native;
using MoonCore.Helpers;
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.Models.Cache;
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
namespace MoonlightServers.Daemon.Mappers;
public class ServerConfigurationMapper
{
private readonly AppConfiguration AppConfiguration;
public ServerConfigurationMapper(AppConfiguration appConfiguration)
{
AppConfiguration = appConfiguration;
}
public ServerConfiguration FromServerDataResponse(ServerDataResponse response)
{
return new ServerConfiguration()
{
Id = response.Id,
StartupCommand = response.StartupCommand,
Allocations = response.Allocations.Select(y => new ServerConfiguration.AllocationConfiguration()
{
IpAddress = y.IpAddress,
Port = y.Port
}).ToArray(),
Variables = response.Variables,
OnlineDetection = response.OnlineDetection,
DockerImage = response.DockerImage,
Cpu = response.Cpu,
Disk = response.Disk,
Memory = response.Memory,
StopCommand = response.StopCommand,
};
}
public CreateContainerParameters ToRuntimeParameters(
ServerConfiguration serverConfiguration,
string hostPath,
string containerName
)
{
var parameters = ToSharedParameters(serverConfiguration);
#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 = containerName;
parameters.Hostname = containerName;
#endregion
#region Docker Image
parameters.Image = serverConfiguration.DockerImage;
#endregion
#region Working Dir
parameters.WorkingDir = "/home/container";
#endregion
#region User
// TODO: Extract this to an external service with config options and return a userspace user id and a install user id
// in order to know which permissions are required in order to run the container with the correct permissions
var userId = Syscall.getuid();
if (userId == 0)
userId = 998;
parameters.User = $"{userId}:{userId}";
/*
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 = hostPath,
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 serverConfiguration.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
// TODO: Implement a way to directly startup a server without using the entrypoint.sh and parsing the startup command here
// in the daemon instead of letting it the entrypoint do. iirc pelican wants to do that as well so we need to do that
// sooner or later in order to stay compatible to pelican
// Possible flag name: LegacyEntrypointMode
return parameters;
}
public CreateContainerParameters ToInstallParameters(
ServerConfiguration serverConfiguration,
ServerInstallDataResponse installData,
string runtimeHostPath,
string installationHostPath,
string containerName
)
{
var parameters = ToSharedParameters(serverConfiguration);
// - Name
parameters.Name = containerName;
parameters.Hostname = containerName;
// - Image
parameters.Image = installData.DockerImage;
// -- Working directory
parameters.WorkingDir = "/mnt/server";
// - User
// Note: Some images might not work if we set a user here
var userId = Syscall.getuid();
// If we are root, we are able to change owner permissions after the installation
// so we run the installation as root, otherwise we need to run it as our current user,
// so we are able to access the files created by the installer
if (userId == 0)
parameters.User = "0:0";
else
parameters.User = $"{userId}:{userId}";
// -- Mounts
parameters.HostConfig.Mounts = new List<Mount>();
parameters.HostConfig.Mounts.Add(new()
{
Source = runtimeHostPath,
Target = "/mnt/server",
ReadOnly = false,
Type = "bind"
});
parameters.HostConfig.Mounts.Add(new()
{
Source = installationHostPath,
Target = "/mnt/install",
ReadOnly = false,
Type = "bind"
});
parameters.Cmd = [installData.Shell, "/mnt/install/install.sh"];
return parameters;
}
public CreateContainerParameters ToSharedParameters(ServerConfiguration serverConfiguration)
{
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 = serverConfiguration.Cpu * 1000;
parameters.HostConfig.CPUPeriod = 100000;
parameters.HostConfig.CPUShares = 1024;
#endregion
#region Memory & Swap
var memoryLimit = serverConfiguration.Memory;
// The overhead multiplier gives the container a little bit more memory to prevent crashes
var memoryOverhead = memoryLimit + (memoryLimit * AppConfiguration.Server.MemoryOverheadMultiplier);
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
// TODO: Read hosts dns settings?
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={AppConfiguration.Server.TmpFsSize}M" }
};
#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", serverConfiguration.Id.ToString());
#endregion
#region Environment
parameters.Env = CreateEnvironmentVariables(serverConfiguration);
#endregion
return parameters;
}
private List<string> CreateEnvironmentVariables(ServerConfiguration serverConfiguration)
{
var result = new Dictionary<string, string>
{
//TODO: Add timezone, add server ip
{ "STARTUP", serverConfiguration.StartupCommand },
{ "SERVER_MEMORY", serverConfiguration.Memory.ToString() }
};
if (serverConfiguration.Allocations.Length > 0)
{
for (var i = 0; i < serverConfiguration.Allocations.Length; i++)
{
var allocation = serverConfiguration.Allocations[i];
result.Add($"ML_PORT_{i}", allocation.Port.ToString());
if (i == 0) // TODO: Implement a way to set the default/main allocation
{
result.Add("SERVER_IP", allocation.IpAddress);
result.Add("SERVER_PORT", allocation.Port.ToString());
}
}
}
// Copy variables as env vars
foreach (var variable in serverConfiguration.Variables)
result.Add(variable.Key, variable.Value);
// Convert to the format of the docker library
return result.Select(variable => $"{variable.Key}={variable.Value}").ToList();
}
}

View File

@@ -1,28 +0,0 @@
namespace MoonlightServers.Daemon.Models.Cache;
public class ServerConfiguration
{
public int Id { get; set; }
// Limits
public int Cpu { get; set; }
public int Memory { get; set; }
public int Disk { get; set; }
// Start, Stop & Status
public string StartupCommand { get; set; }
public string StopCommand { get; set; }
public string OnlineDetection { get; set; }
// Container
public string DockerImage { get; set; }
public AllocationConfiguration[] Allocations { get; set; }
public Dictionary<string, string> Variables { get; set; }
public struct AllocationConfiguration
{
public string IpAddress { get; set; }
public int Port { get; set; }
}
}

View File

@@ -1,8 +0,0 @@
namespace MoonlightServers.Daemon.Models;
public class CpuUsageDetails
{
public string Model { get; set; }
public double OverallUsage { get; set; }
public double[] PerCoreUsage { get; set; }
}

View File

@@ -1,11 +0,0 @@
namespace MoonlightServers.Daemon.Models;
public class DiskUsageDetails
{
public string Device { get; set; }
public string MountPath { get; set; }
public ulong DiskTotal { get; set; }
public ulong DiskFree { get; set; }
public ulong InodesTotal { get; set; }
public ulong InodesFree { get; set; }
}

View File

@@ -0,0 +1,3 @@
namespace MoonlightServers.Daemon.Models;
public record InstallConfiguration(string Shell, string DockerImage, string Script);

View File

@@ -1,11 +0,0 @@
namespace MoonlightServers.Daemon.Models;
public class MemoryUsageDetails
{
public long Total { get; set; }
public long Available { get; set; }
public long Free { get; set; }
public long Cached { get; set; }
public long SwapTotal { get; set; }
public long SwapFree { get; set; }
}

View File

@@ -0,0 +1,47 @@
namespace MoonlightServers.Daemon.Models;
public record RuntimeConfiguration(
RuntimeLimitsConfig Limits,
RuntimeStorageConfig Storage,
RuntimeTemplateConfig Template,
RuntimeNetworkConfig Network,
RuntimeEnvironmentConfig Environment
);
public record RuntimeLimitsConfig(
int? CpuPercent,
int? Threads,
int? MemoryMb,
int? SwapMb
);
public record RuntimeStorageConfig(
string Provider,
Dictionary<string, string> Options,
int LimitMb
);
public record RuntimeTemplateConfig(
string DockerImage,
string StartupCommand,
string StopCommand,
string[] OnlineTexts
);
public record RuntimeNetworkConfig(
string[] Networks,
string? FriendlyName,
string? OutgoingIpAddress,
RuntimePortConfig? MainPort,
RuntimePortConfig[] Ports
);
public record RuntimePortConfig(
string IpAddress,
int Port
);
public record RuntimeEnvironmentConfig(
Dictionary<string, string> Labels,
Dictionary<string, string> Variables
);

View File

@@ -1,46 +0,0 @@
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 int MaxMessagesInCache;
public ServerConsole(int maxMessagesInCache)
{
MaxMessagesInCache = maxMessagesInCache;
}
public async Task WriteToOutputAsync(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 WriteToInputAsync(string content)
{
if (OnInput != null)
await OnInput.Invoke(content);
}
private string[] GetMessages()
{
lock (MessageCache)
return MessageCache.ToArray();
}
}

View File

@@ -1,75 +0,0 @@
using System.Text.Json.Serialization;
namespace MoonlightServers.Daemon.Models.UnsafeDocker;
public class DataUsageResponse
{
[JsonPropertyName("BuildCache")]
public BuildCacheData[] BuildCache { get; set; }
[JsonPropertyName("LayersSize")]
public long LayersSize { get; set; }
[JsonPropertyName("Images")]
public ImageData[] Images { get; set; }
[JsonPropertyName("Containers")]
public ContainerData[] Containers { get; set; }
[JsonPropertyName("Volumes")]
public VolumeData[] Volumes { get; set; }
public class BuildCacheData
{
[JsonPropertyName("Size")]
public long Size { get; set; }
[JsonPropertyName("InUse")]
public bool InUse { get; set; }
}
public class ContainerData
{
[JsonPropertyName("Id")]
public string Id { get; set; }
[JsonPropertyName("SizeRw")]
public long SizeRw { get; set; }
[JsonPropertyName("SizeRootFs")]
public long SizeRootFs { get; set; }
}
public class ImageData
{
[JsonPropertyName("Containers")]
public long Containers { get; set; }
[JsonPropertyName("Id")]
public string Id { get; set; }
[JsonPropertyName("SharedSize")]
public long SharedSize { get; set; }
[JsonPropertyName("Size")]
public long Size { get; set; }
}
public class VolumeData
{
[JsonPropertyName("Name")]
public string Name { get; set; }
[JsonPropertyName("UsageData")]
public VolumeUsageData UsageData { get; set; }
}
public class VolumeUsageData
{
[JsonPropertyName("RefCount")]
public long RefCount { get; set; }
[JsonPropertyName("Size")]
public long Size { get; set; }
}
}

View File

@@ -1,7 +0,0 @@
namespace MoonlightServers.Daemon.Models.UnsafeDocker;
public class UsageData
{
public long Used { get; set; }
public long Reclaimable { get; set; }
}

View File

@@ -1,8 +0,0 @@
namespace MoonlightServers.Daemon.Models.UnsafeDocker;
public class UsageDataReport
{
public UsageData Containers { get; set; }
public UsageData Images { get; set; }
public UsageData BuildCache { get; set; }
}

View File

@@ -1,69 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Docker.DotNet" Version="3.125.15" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.9" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
<PackageReference Include="MoonCore" Version="2.0.1" />
<PackageReference Include="MoonCore.Extended" Version="1.4.0" />
<PackageReference Include="MoonCore.Unix" Version="1.0.8" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="Stateless" Version="5.19.0" />
<PackageReference Include="Docker.DotNet.Enhanced" Version="3.132.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="Http\Middleware\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MoonlightServers.DaemonShared\MoonlightServers.DaemonShared.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Remove="data\**" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Remove="data\**" />
</ItemGroup>
<ItemGroup>
<Content Remove="data\**" />
</ItemGroup>
<ItemGroup>
<None Remove="data\**" />
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="storage\volumes\2\banned-ips.json" />
<_ContentIncludedByDefault Remove="storage\volumes\2\banned-players.json" />
<_ContentIncludedByDefault Remove="storage\volumes\2\ops.json" />
<_ContentIncludedByDefault Remove="storage\volumes\2\plugins\spark\config.json" />
<_ContentIncludedByDefault Remove="storage\volumes\2\usercache.json" />
<_ContentIncludedByDefault Remove="storage\volumes\2\version_history.json" />
<_ContentIncludedByDefault Remove="storage\volumes\2\whitelist.json" />
<_ContentIncludedByDefault Remove="volumes\3\banned-ips.json" />
<_ContentIncludedByDefault Remove="volumes\3\banned-players.json" />
<_ContentIncludedByDefault Remove="volumes\3\ops.json" />
<_ContentIncludedByDefault Remove="volumes\3\plugins\spark\config.json" />
<_ContentIncludedByDefault Remove="volumes\3\usercache.json" />
<_ContentIncludedByDefault Remove="volumes\3\version_history.json" />
<_ContentIncludedByDefault Remove="volumes\3\whitelist.json" />
<_ContentIncludedByDefault Remove="storage\volumes\69\plugins\spark\config.json" />
<_ContentIncludedByDefault Remove="storage\volumes\6\banned-ips.json" />
<_ContentIncludedByDefault Remove="storage\volumes\6\banned-players.json" />
<_ContentIncludedByDefault Remove="storage\volumes\6\ops.json" />
<_ContentIncludedByDefault Remove="storage\volumes\6\plugins\spark\config.json" />
<_ContentIncludedByDefault Remove="storage\volumes\6\usercache.json" />
<_ContentIncludedByDefault Remove="storage\volumes\6\version_history.json" />
<_ContentIncludedByDefault Remove="storage\volumes\6\whitelist.json" />
<Compile Update="ServerSystem\Server.Power.cs">
<DependentUpon>Server.cs</DependentUpon>
</Compile>
<Compile Update="ServerSystem\Server.Install.cs">
<DependentUpon>Server.cs</DependentUpon>
</Compile>
<Compile Update="ServerSystem\Server.Restore.cs">
<DependentUpon>Server.cs</DependentUpon>
</Compile>
<Compile Update="ServerSystem\Server.Delete.cs">
<DependentUpon>Server.cs</DependentUpon>
</Compile>
</ItemGroup>
</Project>

View File

@@ -1,5 +1,59 @@
using MoonlightServers.Daemon;
using Microsoft.Extensions.Logging.Console;
using MoonlightServers.Daemon.Helpers;
using MoonlightServers.Daemon.ServerSystem;
using MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
using MoonlightServers.Daemon.ServerSystem.Implementations.Local;
using MoonlightServers.Daemon.Services;
var startup = new Startup();
var builder = WebApplication.CreateBuilder(args);
await startup.RunAsync(args);
// Configure logging
builder.Logging.ClearProviders();
builder.Logging.AddConsole(options => { options.FormatterName = nameof(AppConsoleFormatter); });
builder.Logging.AddConsoleFormatter<AppConsoleFormatter, ConsoleFormatterOptions>();
builder.Services.AddSingleton<ServerConfigurationService>();
builder.Services.AddSingleton<ServerFactory>();
builder.Services.AddDockerServices();
builder.Services.AddLocalServices();
builder.Services.AddControllers();
var app = builder.Build();
app.UseAuthorization();
app.MapControllers();
Task.Run(async () =>
{
Console.ReadLine();
try
{
var factory = app.Services.GetRequiredService<ServerFactory>();
var server = await factory.CreateAsync("a0e3ddb4-2c72-4f4c-bc49-35650a4bc5c0");
await server.InitializeAsync();
Console.WriteLine($"Server: {server.State}");
Console.ReadLine();
if (server.State == ServerState.Offline)
await server.StartAsync();
else
await server.StopAsync();
Console.ReadLine();
await server.DisposeAsync();
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
});
app.Run();

View File

@@ -1,16 +1,13 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5275",
"applicationUrl": "http://localhost:5086",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"HTTPS_PROXY": "",
"HTTP_PROXY": ""
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}

View File

@@ -0,0 +1,13 @@
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IInstallConsole
{
public event Func<string, Task>? OnOutput;
public Task AttachAsync();
public Task WriteInputAsync(string value);
public Task ClearCacheAsync();
public Task<string[]> GetCacheAsync();
}

View File

@@ -0,0 +1,14 @@
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IInstallEnvironment : IAsyncDisposable
{
public IInstallStatistics Statistics { get; }
public IInstallConsole Console { get; }
public event Func<Task>? OnExited;
public Task<bool> IsRunningAsync();
public Task StartAsync();
public Task KillAsync();
}

View File

@@ -0,0 +1,18 @@
using MoonlightServers.Daemon.Models;
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IInstallEnvironmentService
{
public Task<IInstallEnvironment?> FindAsync(string id);
public Task<IInstallEnvironment> CreateAsync(
string id,
RuntimeConfiguration runtimeConfiguration,
InstallConfiguration installConfiguration,
IInstallStorage installStorage,
IRuntimeStorage runtimeStorage
);
public Task DeleteAsync(IInstallEnvironment environment);
}

View File

@@ -0,0 +1,11 @@
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IInstallStatistics
{
public event Func<ServerStatistics, Task>? OnStatisticsReceived;
public Task AttachAsync();
public Task ClearCacheAsync();
public Task<ServerStatistics[]> GetCacheAsync();
}

View File

@@ -0,0 +1,6 @@
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IInstallStorage
{
public Task<string> GetHostPathAsync();
}

View File

@@ -0,0 +1,10 @@
using MoonlightServers.Daemon.Models;
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IInstallStorageService
{
public Task<IInstallStorage?> FindAsync(string id);
public Task<IInstallStorage> CreateAsync(string id, RuntimeConfiguration runtimeConfiguration, InstallConfiguration installConfiguration);
public Task DeleteAsync(IInstallStorage installStorage);
}

View File

@@ -0,0 +1,13 @@
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IRuntimeConsole
{
public event Func<string, Task>? OnOutput;
public Task AttachAsync();
public Task WriteInputAsync(string value);
public Task ClearCacheAsync();
public Task<string[]> GetCacheAsync();
}

View File

@@ -0,0 +1,14 @@
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IRuntimeEnvironment : IAsyncDisposable
{
public IRuntimeStatistics Statistics { get; }
public IRuntimeConsole Console { get; }
public event Func<Task>? OnExited;
public Task<bool> IsRunningAsync();
public Task StartAsync();
public Task KillAsync();
}

View File

@@ -0,0 +1,11 @@
using MoonlightServers.Daemon.Models;
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IRuntimeEnvironmentService
{
public Task<IRuntimeEnvironment?> FindAsync(string id);
public Task<IRuntimeEnvironment> CreateAsync(string id, RuntimeConfiguration configuration, IRuntimeStorage runtimeStorage);
public Task UpdateAsync(IRuntimeEnvironment environment, RuntimeConfiguration configuration);
public Task DeleteAsync(IRuntimeEnvironment environment);
}

View File

@@ -0,0 +1,11 @@
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IRuntimeStatistics
{
public event Func<ServerStatistics, Task>? OnStatisticsReceived;
public Task AttachAsync();
public Task ClearCacheAsync();
public Task<ServerStatistics[]> GetCacheAsync();
}

View File

@@ -0,0 +1,6 @@
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IRuntimeStorage
{
public Task<string> GetHostPathAsync();
}

View File

@@ -0,0 +1,11 @@
using MoonlightServers.Daemon.Models;
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IRuntimeStorageService
{
public Task<IRuntimeStorage?> FindAsync(string id);
public Task<IRuntimeStorage> CreateAsync(string id, RuntimeConfiguration configuration);
public Task UpdateAsync(IRuntimeStorage runtimeStorage, RuntimeConfiguration configuration);
public Task DeleteAsync(IRuntimeStorage runtimeStorage);
}

View File

@@ -1,221 +0,0 @@
using System.Text;
using Docker.DotNet;
using MoonCore.Events;
using MoonCore.Helpers;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.Docker;
public class DockerConsole : IConsole
{
private readonly EventSource<string> StdOutEventSource = new();
private readonly ConcurrentList<string> StdOutCache = new();
private readonly DockerClient DockerClient;
private readonly ServerContext Context;
private readonly ILogger Logger;
private MultiplexedStream? CurrentStream;
private CancellationTokenSource Cts = new();
public DockerConsole(DockerClient dockerClient, ServerContext context)
{
DockerClient = dockerClient;
Context = context;
Logger = Context.Logger;
}
public Task InitializeAsync()
=> Task.CompletedTask;
public async Task WriteStdInAsync(string content)
{
if (CurrentStream == null)
{
Logger.LogWarning("Unable to write to stdin as no stream is connected");
return;
}
var contextBuffer = Encoding.UTF8.GetBytes(content);
await CurrentStream.WriteAsync(contextBuffer, 0, contextBuffer.Length, Cts.Token);
}
public async Task WriteStdOutAsync(string content)
{
// Add output cache
if (StdOutCache.Count > 250) // TODO: Config
StdOutCache.RemoveRange(0, 100);
StdOutCache.Add(content);
// Fire event
await StdOutEventSource.InvokeAsync(content);
}
public async Task AttachRuntimeAsync()
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
await AttachToContainerAsync(containerName);
}
public async Task AttachInstallationAsync()
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, Context.Configuration.Id);
await AttachToContainerAsync(containerName);
}
private async Task AttachToContainerAsync(string containerName)
{
var cts = new CancellationTokenSource();
// Cancels previous active read task if it exists
if (!Cts.IsCancellationRequested)
await Cts.CancelAsync();
// Update the current cancellation token
Cts = cts;
// Start reading task
Task.Run(async () =>
{
// This loop is here to reconnect to the stream when connection is lost.
// This can occur when docker restarts for example
while (!cts.IsCancellationRequested)
{
MultiplexedStream? innerStream = null;
try
{
Logger.LogTrace("Attaching");
innerStream = await DockerClient.Containers.AttachContainerAsync(
containerName,
true,
new()
{
Stderr = true,
Stdin = true,
Stdout = true,
Stream = true
},
cts.Token
);
CurrentStream = innerStream;
var buffer = new byte[1024];
try
{
// Read while server tasks are not canceled
while (!cts.Token.IsCancellationRequested)
{
var readResult = await innerStream.ReadOutputAsync(
buffer,
0,
buffer.Length,
cts.Token
);
if (readResult.EOF)
await cts.CancelAsync();
var decodedText = Encoding.UTF8.GetString(buffer, 0, readResult.Count);
await WriteStdOutAsync(decodedText);
}
Logger.LogTrace("Read loop exited");
}
catch (TaskCanceledException)
{
// Ignored
}
catch (OperationCanceledException)
{
// Ignored
}
catch (Exception e)
{
Logger.LogWarning(e, "An unhandled error occured while reading from container stream");
}
}
catch (TaskCanceledException)
{
// ignored
}
catch (DockerContainerNotFoundException)
{
// Container got removed. Stop the reconnect loop
Logger.LogDebug("Container '{name}' got removed. Stopping reconnect stream for console", containerName);
await cts.CancelAsync();
}
catch (Exception e)
{
Logger.LogError(e, "An error occured while attaching to container");
}
innerStream?.Dispose();
}
Logger.LogDebug("Disconnected from container stream");
});
}
public async Task FetchRuntimeAsync()
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
await FetchFromContainerAsync(containerName);
}
public async Task FetchInstallationAsync()
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, Context.Configuration.Id);
await FetchFromContainerAsync(containerName);
}
private async Task FetchFromContainerAsync(string containerName)
{
var logStream = await DockerClient.Containers.GetContainerLogsAsync(containerName, true, new()
{
Follow = false,
ShowStderr = true,
ShowStdout = true
});
var combinedOutput = await logStream.ReadOutputToEndAsync(Cts.Token);
var contentToAdd = combinedOutput.stdout + combinedOutput.stderr;
await WriteStdOutAsync(contentToAdd);
}
public Task ClearCacheAsync()
{
StdOutCache.Clear();
return Task.CompletedTask;
}
public Task<IEnumerable<string>> GetCacheAsync()
{
return Task.FromResult<IEnumerable<string>>(StdOutCache);
}
public async Task<IAsyncDisposable> SubscribeStdOutAsync(Func<string, ValueTask> callback)
=> await StdOutEventSource.SubscribeAsync(callback);
public async ValueTask DisposeAsync()
{
if (!Cts.IsCancellationRequested)
await Cts.CancelAsync();
if (CurrentStream != null)
CurrentStream.Dispose();
}
}

View File

@@ -1,7 +0,0 @@
namespace MoonlightServers.Daemon.ServerSystem.Docker;
public static class DockerConstants
{
public const string RuntimeNameTemplate = "moonlight-runtime-{0}";
public const string InstallationNameTemplate = "moonlight-installation-{0}";
}

View File

@@ -1,184 +0,0 @@
using Docker.DotNet;
using Docker.DotNet.Models;
using MoonCore.Events;
using MoonlightServers.Daemon.Mappers;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using MoonlightServers.Daemon.Services;
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
namespace MoonlightServers.Daemon.ServerSystem.Docker;
public class DockerInstallation : IInstallation
{
private readonly DockerEventService DockerEventService;
private readonly ServerConfigurationMapper Mapper;
private readonly DockerImageService ImageService;
private readonly ServerContext ServerContext;
private readonly DockerClient DockerClient;
private IReporter Reporter => ServerContext.Server.Reporter;
private readonly EventSource<int> ExitEventSource = new();
private IAsyncDisposable ContainerEventSubscription;
private string ContainerId;
public DockerInstallation(
DockerClient dockerClient,
ServerContext serverContext,
ServerConfigurationMapper mapper,
DockerImageService imageService,
DockerEventService dockerEventService
)
{
DockerClient = dockerClient;
ServerContext = serverContext;
Mapper = mapper;
ImageService = imageService;
DockerEventService = dockerEventService;
}
public async Task InitializeAsync()
{
ContainerEventSubscription = await DockerEventService.SubscribeContainerAsync(OnContainerEvent);
}
private async ValueTask OnContainerEvent(Message message)
{
// Only handle events for our own container
if (message.ID != ContainerId)
return;
// Only handle die events
if (message.Action != "die")
return;
int exitCode;
if (message.Actor.Attributes.TryGetValue("exitCode", out var exitCodeStr))
{
if (!int.TryParse(exitCodeStr, out exitCode))
exitCode = 0;
}
else
exitCode = 0;
await ExitEventSource.InvokeAsync(exitCode);
}
public async Task<bool> CheckExistsAsync()
{
try
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
await DockerClient.Containers.InspectContainerAsync(
containerName
);
return true;
}
catch (DockerContainerNotFoundException)
{
return false;
}
}
public async Task CreateAsync(
string runtimePath,
string hostPath,
ServerInstallDataResponse data
)
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
var parameters = Mapper.ToInstallParameters(
ServerContext.Configuration,
data,
runtimePath,
hostPath,
containerName
);
// Docker image
await Reporter.StatusAsync("Downloading docker image");
await ImageService.DownloadAsync(data.DockerImage, async status => { await Reporter.StatusAsync(status); });
await Reporter.StatusAsync("Downloaded docker image");
// Write install script to install fs
await File.WriteAllTextAsync(
Path.Combine(hostPath, "install.sh"),
data.Script
);
//
var response = await DockerClient.Containers.CreateContainerAsync(parameters);
ContainerId = response.ID;
await Reporter.StatusAsync("Created container");
}
public async Task StartAsync()
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
await DockerClient.Containers.StartContainerAsync(containerName, new());
}
public async Task KillAsync()
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
await DockerClient.Containers.KillContainerAsync(containerName, new());
}
public async Task DestroyAsync()
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
try
{
var container = await DockerClient.Containers.InspectContainerAsync(containerName);
if (container.State.Running)
await DockerClient.Containers.KillContainerAsync(containerName, new());
await DockerClient.Containers.RemoveContainerAsync(containerName, new()
{
Force = true
});
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
}
public async Task<IAsyncDisposable> SubscribeExitedAsync(Func<int, ValueTask> callback)
=> await ExitEventSource.SubscribeAsync(callback);
public async Task RestoreAsync()
{
try
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
var container = await DockerClient.Containers.InspectContainerAsync(containerName);
ContainerId = container.ID;
}
catch (DockerContainerNotFoundException)
{
// Ignore
}
}
public async ValueTask DisposeAsync()
{
await ContainerEventSubscription.DisposeAsync();
}
}

View File

@@ -1,59 +0,0 @@
using Docker.DotNet;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.Docker;
public class DockerRestorer : IRestorer
{
private readonly DockerClient DockerClient;
private readonly ServerContext Context;
public DockerRestorer(DockerClient dockerClient, ServerContext context)
{
DockerClient = dockerClient;
Context = context;
}
public Task InitializeAsync()
=> Task.CompletedTask;
public async Task<bool> HandleRuntimeAsync()
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
try
{
var container = await DockerClient.Containers.InspectContainerAsync(
containerName
);
return container.State.Running;
}
catch (DockerContainerNotFoundException)
{
return false;
}
}
public async Task<bool> HandleInstallationAsync()
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, Context.Configuration.Id);
try
{
var container = await DockerClient.Containers.InspectContainerAsync(
containerName
);
return container.State.Running;
}
catch (DockerContainerNotFoundException)
{
return false;
}
}
public ValueTask DisposeAsync()
=> ValueTask.CompletedTask;
}

View File

@@ -1,177 +0,0 @@
using Docker.DotNet;
using Docker.DotNet.Models;
using MoonCore.Events;
using MoonlightServers.Daemon.Mappers;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon.ServerSystem.Docker;
public class DockerRuntime : IRuntime
{
private readonly DockerClient DockerClient;
private readonly ServerContext Context;
private readonly ServerConfigurationMapper Mapper;
private readonly DockerEventService DockerEventService;
private readonly DockerImageService ImageService;
private readonly EventSource<int> ExitEventSource = new();
private IReporter Reporter => Context.Server.Reporter;
private IAsyncDisposable ContainerEventSubscription;
private string ContainerId;
public DockerRuntime(
DockerClient dockerClient,
ServerContext context,
ServerConfigurationMapper mapper,
DockerEventService dockerEventService,
DockerImageService imageService
)
{
DockerClient = dockerClient;
Context = context;
Mapper = mapper;
DockerEventService = dockerEventService;
ImageService = imageService;
}
public async Task InitializeAsync()
{
ContainerEventSubscription = await DockerEventService.SubscribeContainerAsync(OnContainerEvent);
}
private async ValueTask OnContainerEvent(Message message)
{
// Only handle events for our own container
if (message.ID != ContainerId)
return;
// Only handle die events
if (message.Action != "die")
return;
int exitCode;
if (message.Actor.Attributes.TryGetValue("exitCode", out var exitCodeStr))
{
if (!int.TryParse(exitCodeStr, out exitCode))
exitCode = 0;
}
else
exitCode = 0;
await ExitEventSource.InvokeAsync(exitCode);
}
public async Task<bool> CheckExistsAsync()
{
try
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
await DockerClient.Containers.InspectContainerAsync(
containerName
);
return true;
}
catch (DockerContainerNotFoundException)
{
return false;
}
}
public async Task CreateAsync(string path)
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
var parameters = Mapper.ToRuntimeParameters(
Context.Configuration,
path,
containerName
);
// Docker image
await Reporter.StatusAsync("Downloading docker image");
await ImageService.DownloadAsync(
Context.Configuration.DockerImage,
async status => { await Reporter.StatusAsync(status); }
);
await Reporter.StatusAsync("Downloaded docker image");
//
var response = await DockerClient.Containers.CreateContainerAsync(parameters);
ContainerId = response.ID;
await Reporter.StatusAsync("Created container");
}
public async Task StartAsync()
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
await DockerClient.Containers.StartContainerAsync(containerName, new());
}
public Task UpdateAsync()
{
return Task.CompletedTask;
}
public async Task KillAsync()
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
await DockerClient.Containers.KillContainerAsync(containerName, new());
}
public async Task DestroyAsync()
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
try
{
var container = await DockerClient.Containers.InspectContainerAsync(containerName);
if (container.State.Running)
await DockerClient.Containers.KillContainerAsync(containerName, new());
await DockerClient.Containers.RemoveContainerAsync(containerName, new()
{
Force = true
});
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
}
public async Task<IAsyncDisposable> SubscribeExitedAsync(Func<int, ValueTask> callback)
=> await ExitEventSource.SubscribeAsync(callback);
public async Task RestoreAsync()
{
try
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
var container = await DockerClient.Containers.InspectContainerAsync(containerName);
ContainerId = container.ID;
}
catch (DockerContainerNotFoundException)
{
// Ignore
}
}
public async ValueTask DisposeAsync()
{
await ContainerEventSubscription.DisposeAsync();
}
}

View File

@@ -1,25 +0,0 @@
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.Docker;
public class DockerStatistics : IStatistics
{
public Task InitializeAsync()
=> Task.CompletedTask;
public Task AttachRuntimeAsync()
=> Task.CompletedTask;
public Task AttachInstallationAsync()
=> Task.CompletedTask;
public Task ClearCacheAsync()
=> Task.CompletedTask;
public Task<IEnumerable<StatisticsData>> GetCacheAsync()
=> Task.FromResult<IEnumerable<StatisticsData>>([]);
public ValueTask DisposeAsync()
=> ValueTask.CompletedTask;
}

View File

@@ -1,12 +0,0 @@
namespace MoonlightServers.Daemon.ServerSystem.Enums;
public enum ServerTrigger
{
Start = 0,
Stop = 1,
Kill = 2,
DetectOnline = 3,
Install = 4,
Fail = 5,
Exited = 6
}

View File

@@ -1,58 +0,0 @@
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.FileSystems;
public class RawInstallationFs : IFileSystem
{
private readonly string BaseDirectory;
public RawInstallationFs(ServerContext context)
{
BaseDirectory = Path.Combine(
Directory.GetCurrentDirectory(),
"storage",
"install",
context.Configuration.Id.ToString()
);
}
public Task InitializeAsync()
=> Task.CompletedTask;
public Task<string> GetPathAsync()
=> Task.FromResult(BaseDirectory);
public Task<bool> CheckExistsAsync()
{
var exists = Directory.Exists(BaseDirectory);
return Task.FromResult(exists);
}
public Task<bool> CheckMountedAsync()
=> Task.FromResult(true);
public Task CreateAsync()
{
Directory.CreateDirectory(BaseDirectory);
return Task.CompletedTask;
}
public Task PerformChecksAsync()
=> Task.CompletedTask;
public Task MountAsync()
=> Task.CompletedTask;
public Task UnmountAsync()
=> Task.CompletedTask;
public Task DestroyAsync()
{
Directory.Delete(BaseDirectory, true);
return Task.CompletedTask;
}
public ValueTask DisposeAsync()
=> ValueTask.CompletedTask;
}

View File

@@ -1,58 +0,0 @@
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.FileSystems;
public class RawRuntimeFs : IFileSystem
{
private readonly string BaseDirectory;
public RawRuntimeFs(ServerContext context)
{
BaseDirectory = Path.Combine(
Directory.GetCurrentDirectory(),
"storage",
"volumes",
context.Configuration.Id.ToString()
);
}
public Task InitializeAsync()
=> Task.CompletedTask;
public Task<string> GetPathAsync()
=> Task.FromResult(BaseDirectory);
public Task<bool> CheckExistsAsync()
{
var exists = Directory.Exists(BaseDirectory);
return Task.FromResult(exists);
}
public Task<bool> CheckMountedAsync()
=> Task.FromResult(true);
public Task CreateAsync()
{
Directory.CreateDirectory(BaseDirectory);
return Task.CompletedTask;
}
public Task PerformChecksAsync()
=> Task.CompletedTask;
public Task MountAsync()
=> Task.CompletedTask;
public Task UnmountAsync()
=> Task.CompletedTask;
public Task DestroyAsync()
{
Directory.Delete(BaseDirectory, true);
return Task.CompletedTask;
}
public ValueTask DisposeAsync()
=> ValueTask.CompletedTask;
}

View File

@@ -1,35 +0,0 @@
using MoonlightServers.Daemon.ServerSystem.Enums;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using Stateless;
namespace MoonlightServers.Daemon.ServerSystem.Handlers;
public class DebugHandler : IServerStateHandler
{
private readonly ServerContext Context;
private IAsyncDisposable? StdOutSubscription;
public DebugHandler(ServerContext context)
{
Context = context;
}
public async Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition)
{
if(StdOutSubscription != null)
return;
StdOutSubscription = await Context.Server.Console.SubscribeStdOutAsync(line =>
{
Console.WriteLine($"STD OUT: {line}");
return ValueTask.CompletedTask;
});
}
public async ValueTask DisposeAsync()
{
if (StdOutSubscription != null)
await StdOutSubscription.DisposeAsync();
}
}

View File

@@ -1,125 +0,0 @@
using MoonlightServers.Daemon.ServerSystem.Enums;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
using Stateless;
namespace MoonlightServers.Daemon.ServerSystem.Handlers;
public class InstallationHandler : IServerStateHandler
{
private readonly ServerContext Context;
private Server Server => Context.Server;
private IAsyncDisposable? ExitSubscription;
public InstallationHandler(ServerContext context)
{
Context = context;
}
public async Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition)
{
if (transition is
{ Source: ServerState.Offline, Destination: ServerState.Installing, Trigger: ServerTrigger.Install })
{
await StartAsync();
}
else if (transition is
{ Source: ServerState.Installing, Destination: ServerState.Offline, Trigger: ServerTrigger.Exited })
{
await CompleteAsync();
}
}
private async Task StartAsync()
{
// Plan:
// 1. Fetch latest configuration
// 2. Check if both file systems exists
// 3. Check if both file systems are mounted
// 4. Run file system checks
// 5. Create installation container
// 6. Attach console
// 7. Start installation container
// 1. Fetch latest configuration
var installData = new ServerInstallDataResponse()
{
Script = await File.ReadAllTextAsync(Path.Combine("storage", "install.sh")),
Shell = "/bin/ash",
DockerImage = "ghcr.io/parkervcp/installers:alpine"
};
// 2. Check if file system exists
if (!await Server.RuntimeFileSystem.CheckExistsAsync())
await Server.RuntimeFileSystem.CreateAsync();
if (!await Server.InstallationFileSystem.CheckExistsAsync())
await Server.InstallationFileSystem.CreateAsync();
// 3. Check if both file systems are mounted
if (!await Server.RuntimeFileSystem.CheckMountedAsync())
await Server.RuntimeFileSystem.MountAsync();
if (!await Server.InstallationFileSystem.CheckMountedAsync())
await Server.InstallationFileSystem.MountAsync();
// 4. Run file system checks
await Server.RuntimeFileSystem.PerformChecksAsync();
await Server.InstallationFileSystem.PerformChecksAsync();
// 5. Create installation
var runtimePath = await Server.RuntimeFileSystem.GetPathAsync();
var installationPath = await Server.InstallationFileSystem.GetPathAsync();
if (await Server.Installation.CheckExistsAsync())
await Server.Installation.DestroyAsync();
await Server.Installation.CreateAsync(runtimePath, installationPath, installData);
if (ExitSubscription == null)
ExitSubscription = await Server.Installation.SubscribeExitedAsync(OnInstallationExited);
// 6. Attach console
await Server.Console.AttachInstallationAsync();
// 7. Start installation container
await Server.Installation.StartAsync();
}
private async ValueTask OnInstallationExited(int exitCode)
{
// TODO: Notify the crash handler component of the exit code
await Server.StateMachine.FireAsync(ServerTrigger.Exited);
}
private async Task CompleteAsync()
{
// Plan:
// 1. Handle possible crash
// 2. Remove installation container
// 3. Remove installation file system
// 1. Handle possible crash
// TODO
// 2. Remove installation container
await Server.Installation.DestroyAsync();
// 3. Remove installation file system
await Server.InstallationFileSystem.UnmountAsync();
await Server.InstallationFileSystem.DestroyAsync();
Context.Logger.LogDebug("Completed installation");
}
public async ValueTask DisposeAsync()
{
if (ExitSubscription != null)
await ExitSubscription.DisposeAsync();
}
}

View File

@@ -1,85 +0,0 @@
using MoonlightServers.Daemon.ServerSystem.Enums;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using Stateless;
namespace MoonlightServers.Daemon.ServerSystem.Handlers;
public class OnlineDetectionHandler : IServerStateHandler
{
private readonly ServerContext Context;
private IOnlineDetector OnlineDetector => Context.Server.OnlineDetector;
private ILogger Logger => Context.Logger;
private IAsyncDisposable? ConsoleSubscription;
private bool IsActive = false;
public OnlineDetectionHandler(ServerContext context)
{
Context = context;
}
public async Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition)
{
if (
transition is
{ Source: ServerState.Offline, Destination: ServerState.Starting, Trigger: ServerTrigger.Start } && !IsActive
)
{
await StartAsync();
}
else if (transition is { Source: not ServerState.Installing, Destination: ServerState.Offline } && IsActive)
{
await StopAsync();
}
}
private async Task StartAsync()
{
IsActive = true;
await OnlineDetector.CreateAsync();
ConsoleSubscription = await Context.Server.Console.SubscribeStdOutAsync(OnHandleOutput);
Logger.LogTrace("Created online detector. Created console subscription");
}
private async ValueTask OnHandleOutput(string line)
{
if(!IsActive)
return;
if(!await OnlineDetector.HandleOutputAsync(line))
return;
if(!Context.Server.StateMachine.CanFire(ServerTrigger.DetectOnline))
return;
Logger.LogTrace("Detected server as online. Destroying online detector");
await Context.Server.StateMachine.FireAsync(ServerTrigger.DetectOnline);
await StopAsync();
}
private async Task StopAsync()
{
IsActive = false;
if (ConsoleSubscription != null)
{
await ConsoleSubscription.DisposeAsync();
ConsoleSubscription = null;
}
await OnlineDetector.DestroyAsync();
Logger.LogTrace("Destroyed online detector. Revoked console subscription");
}
public async ValueTask DisposeAsync()
{
if (ConsoleSubscription != null)
await ConsoleSubscription.DisposeAsync();
}
}

View File

@@ -1,42 +0,0 @@
using MoonlightServers.Daemon.ServerSystem.Enums;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using Stateless;
namespace MoonlightServers.Daemon.ServerSystem.Handlers;
public class ShutdownHandler : IServerStateHandler
{
private readonly ServerContext ServerContext;
public ShutdownHandler(ServerContext serverContext)
{
ServerContext = serverContext;
}
public async Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition)
{
// Filter (we only want to handle exists from the runtime, so we filter out the installing state)
if (transition is not
{
Destination: ServerState.Offline,
Source: not ServerState.Installing,
Trigger: ServerTrigger.Exited // We don't want to handle the fail event here
})
return;
// Plan:
// 1. Handle possible crash
// 2. Remove runtime
// 1. Handle possible crash
// TODO: Handle crash here
// 2. Remove runtime
await ServerContext.Server.Runtime.DestroyAsync();
}
public ValueTask DisposeAsync()
=> ValueTask.CompletedTask;
}

View File

@@ -1,84 +0,0 @@
using MoonlightServers.Daemon.ServerSystem.Enums;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using Stateless;
namespace MoonlightServers.Daemon.ServerSystem.Handlers;
public class StartupHandler : IServerStateHandler
{
private IAsyncDisposable? ExitSubscription;
private readonly ServerContext Context;
private Server Server => Context.Server;
public StartupHandler(ServerContext context)
{
Context = context;
}
public async Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition)
{
// Filter
if (transition is not {Source: ServerState.Offline, Destination: ServerState.Starting, Trigger: ServerTrigger.Start})
return;
// Plan:
// 1. Fetch latest configuration
// 2. Check if file system exists
// 3. Check if file system is mounted
// 4. Run file system checks
// 5. Create runtime
// 6. Attach console
// 7. Start runtime
// 1. Fetch latest configuration
// TODO
// Consider moving it out of the startup handler, as other handlers might need
// the updated config as well or add sorting into the handler registration to ensure they are executing in the correct order.
// Sort when building server, not when executing handlers
// 2. Check if file system exists
if (!await Server.RuntimeFileSystem.CheckExistsAsync())
await Server.RuntimeFileSystem.CreateAsync();
// 3. Check if file system is mounted
if (!await Server.RuntimeFileSystem.CheckMountedAsync())
await Server.RuntimeFileSystem.CheckMountedAsync();
// 4. Run file system checks
await Server.RuntimeFileSystem.PerformChecksAsync();
// 5. Create runtime
var hostPath = await Server.RuntimeFileSystem.GetPathAsync();
if (await Server.Runtime.CheckExistsAsync())
await Server.Runtime.DestroyAsync();
await Server.Runtime.CreateAsync(hostPath);
if (ExitSubscription == null)
ExitSubscription = await Server.Runtime.SubscribeExitedAsync(OnRuntimeExited);
// 6. Attach console
await Server.Console.AttachRuntimeAsync();
// 7. Start runtime
await Server.Runtime.StartAsync();
}
private async ValueTask OnRuntimeExited(int exitCode)
{
// TODO: Notify the crash handler component of the exit code
await Server.StateMachine.FireAsync(ServerTrigger.Exited);
}
public async ValueTask DisposeAsync()
{
if (ExitSubscription != null)
await ExitSubscription.DisposeAsync();
}
}

View File

@@ -1,39 +0,0 @@
using Microsoft.AspNetCore.SignalR;
using MoonlightServers.Daemon.Http.Hubs;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.Implementations;
public class ConsoleSignalRComponent : IServerComponent
{
private readonly IHubContext<ServerWebSocketHub> Hub;
private readonly ServerContext Context;
private IAsyncDisposable? StdOutSubscription;
private string HubGroup;
public ConsoleSignalRComponent(IHubContext<ServerWebSocketHub> hub, ServerContext context)
{
Hub = hub;
Context = context;
}
public async Task InitializeAsync()
{
HubGroup = Context.Configuration.Id.ToString();
StdOutSubscription = await Context.Server.Console.SubscribeStdOutAsync(OnStdOut);
}
private async ValueTask OnStdOut(string output)
{
await Hub.Clients.Group(HubGroup).SendAsync("ConsoleOutput", output);
}
public async ValueTask DisposeAsync()
{
if (StdOutSubscription != null)
await StdOutSubscription.DisposeAsync();
}
}

View File

@@ -0,0 +1,254 @@
using System.ComponentModel;
using Docker.DotNet.Models;
using MoonlightServers.Daemon.Models;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
public static class ConfigMapper
{
public static CreateContainerParameters GetRuntimeConfig(
string uuid,
string name,
RuntimeConfiguration configuration,
string runtimeStoragePath
)
{
var parameters = new CreateContainerParameters()
{
HostConfig = new()
};
ApplySharedOptions(parameters, configuration);
// Limits
if (configuration.Limits.CpuPercent.HasValue)
{
parameters.HostConfig.CPUQuota = configuration.Limits.CpuPercent.Value * 1000;
parameters.HostConfig.CPUPeriod = 100000;
parameters.HostConfig.CPUShares = 1024;
}
if (configuration.Limits.MemoryMb.HasValue)
{
var memoryLimit = configuration.Limits.MemoryMb.Value;
// The overhead multiplier gives the container a little bit more memory to prevent crashes
var memoryOverhead = memoryLimit + memoryLimit * 0.05f;
parameters.HostConfig.Memory = (long)memoryOverhead * 1024L * 1024L;
parameters.HostConfig.MemoryReservation = (long)memoryLimit * 1024L * 1024L;
if (configuration.Limits.SwapMb.HasValue)
{
var rawSwap = configuration.Limits.SwapMb.Value * 1024L * 1024L;
parameters.HostConfig.MemorySwap = rawSwap + (long)memoryOverhead;
}
}
parameters.HostConfig.BlkioWeight = 100;
parameters.HostConfig.OomKillDisable = true;
// Storage
parameters.HostConfig.Tmpfs = new Dictionary<string, string>()
{
{ "/tmp", "rw,exec,nosuid,size=100M" } // TODO: Config
};
parameters.WorkingDir = "/home/container";
parameters.HostConfig.Mounts = new List<Mount>();
parameters.HostConfig.Mounts.Add(new Mount()
{
Source = runtimeStoragePath,
Target = "/home/container",
Type = "bind",
ReadOnly = false
});
// Labels
parameters.Labels = new Dictionary<string, string>()
{
{ "dev.moonlightpanel", "true" },
{ "dev.moonlightpanel.id", uuid }
};
foreach (var label in configuration.Environment.Labels)
parameters.Labels.Add(label.Key, label.Value);
// 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"
};
// Name
parameters.Name = name;
// Docker Image
parameters.Image = configuration.Template.DockerImage;
// Networking
if (configuration.Network.Ports.Length > 0 && !string.IsNullOrWhiteSpace(configuration.Network.FriendlyName))
parameters.Hostname = configuration.Network.FriendlyName;
parameters.ExposedPorts = new Dictionary<string, EmptyStruct>();
parameters.HostConfig.PortBindings = new Dictionary<string, IList<PortBinding>>();
foreach (var port in configuration.Network.Ports)
{
parameters.ExposedPorts.Add($"{port.Port}/tcp", new());
parameters.ExposedPorts.Add($"{port.Port}/udp", new());
parameters.HostConfig.PortBindings.Add($"{port.Port}/tcp", new List<PortBinding>
{
new()
{
HostPort = port.Port.ToString(),
HostIP = port.IpAddress
}
});
parameters.HostConfig.PortBindings.Add($"{port.Port}/udp", new List<PortBinding>
{
new()
{
HostPort = port.Port.ToString(),
HostIP = port.IpAddress
}
});
}
// TODO: Force outgoing ip stuff
// User
parameters.User = "1000:1000";
return parameters;
}
public static CreateContainerParameters GetInstallConfig(
string uuid,
string name,
RuntimeConfiguration runtimeConfiguration,
InstallConfiguration installConfiguration,
string runtimeStoragePath,
string installStoragePath
)
{
var parameters = new CreateContainerParameters()
{
HostConfig = new()
};
ApplySharedOptions(parameters, runtimeConfiguration);
// Labels
parameters.Labels = new Dictionary<string, string>()
{
{ "dev.moonlightpanel", "true" },
{ "dev.moonlightpanel.id", uuid }
};
foreach (var label in runtimeConfiguration.Environment.Labels)
parameters.Labels.Add(label.Key, label.Value);
// Name
parameters.Name = name;
// Docker Image
parameters.Image = installConfiguration.DockerImage;
// User
parameters.User = "1000:1000";
// Storage
parameters.WorkingDir = "/mnt/server";
parameters.HostConfig.Mounts = new List<Mount>();
parameters.HostConfig.Mounts.Add(new Mount()
{
Source = runtimeStoragePath,
Target = "/mnt/server",
ReadOnly = false,
Type = "bind"
});
parameters.HostConfig.Mounts.Add(new Mount()
{
Source = installStoragePath,
Target = "/mnt/install",
ReadOnly = false,
Type = "bind"
});
// Command
parameters.Cmd = [installConfiguration.Shell, "/mnt/install/install.sh"];
return parameters;
}
private static void ApplySharedOptions(
CreateContainerParameters parameters,
RuntimeConfiguration configuration
)
{
// Input, output & error streams and TTY
parameters.Tty = true;
parameters.AttachStderr = true;
parameters.AttachStdin = true;
parameters.AttachStdout = true;
parameters.OpenStdin = true;
// 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>()
};
// Environment variables
parameters.Env = new List<string>()
{
$"STARTUP={configuration.Template.StartupCommand}",
//TODO: Add timezone, add server ip
};
if (configuration.Limits.MemoryMb.HasValue)
parameters.Env.Add($"SERVER_MEMORY={configuration.Limits.MemoryMb.Value}");
if (configuration.Network.MainPort != null)
{
parameters.Env.Add($"SERVER_IP={configuration.Network.MainPort.IpAddress}");
parameters.Env.Add($"SERVER_PORT={configuration.Network.MainPort.Port}");
}
// Handle port variables
var i = 1;
foreach (var port in configuration.Network.Ports)
{
parameters.Env.Add($"ML_PORT_{i}={port.Port}");
i++;
}
// Copy variables as env vars
foreach (var variable in configuration.Environment.Variables)
parameters.Env.Add($"{variable.Key}={variable.Value}");
}
}

View File

@@ -0,0 +1,195 @@
using System.Buffers;
using System.Text;
using Docker.DotNet;
using Docker.DotNet.Models;
using MoonlightServers.Daemon.ServerSystem.Abstractions;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
public class DockerConsole : IRuntimeConsole, IInstallConsole, IAsyncDisposable
{
public event Func<string, Task>? OnOutput;
private MultiplexedStream? Stream;
private readonly string ContainerId;
private readonly DockerClient DockerClient;
private readonly ILogger Logger;
private readonly List<string> Cache = new(302);
private readonly SemaphoreSlim CacheLock = new(1, 1);
private readonly CancellationTokenSource Cts = new();
public DockerConsole(
string containerId,
DockerClient dockerClient,
ILogger logger
)
{
ContainerId = containerId;
DockerClient = dockerClient;
Logger = logger;
}
public async Task AttachAsync()
{
// Fetch initial logs
Logger.LogTrace("Fetching pre-existing logs from container");
var logResponse = await DockerClient.Containers.GetContainerLogsAsync(
ContainerId,
new()
{
Follow = false,
ShowStderr = true,
ShowStdout = true
}
);
// Append to cache
var logs = await logResponse.ReadOutputToEndAsync(Cts.Token);
await CacheLock.WaitAsync(Cts.Token);
try
{
Cache.Add(logs.stdout);
Cache.Add(logs.stderr);
}
finally
{
CacheLock.Release();
}
// Stream new logs
Logger.LogTrace("Starting log streaming");
Task.Run(async () =>
{
var capturedCt = Cts.Token;
Logger.LogTrace("Starting attach loop");
while (!capturedCt.IsCancellationRequested)
{
try
{
using var stream = await DockerClient.Containers.AttachContainerAsync(
ContainerId,
new ContainerAttachParameters()
{
Stderr = true,
Stdin = true,
Stdout = true,
Stream = true
},
capturedCt
);
// Make stream accessible from the outside
Stream = stream;
const int bufferSize = 1024;
var buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
while (!capturedCt.IsCancellationRequested)
{
try
{
var readResult = await stream.ReadOutputAsync(buffer, 0, bufferSize, capturedCt);
if (readResult.Count > 0)
{
var decodedBuffer = Encoding.UTF8.GetString(buffer, 0, readResult.Count);
await CacheLock.WaitAsync(capturedCt);
try
{
if (Cache.Count > 300)
Cache.RemoveRange(0, 50);
Cache.Add(decodedBuffer);
}
finally
{
CacheLock.Release();
}
if (OnOutput != null)
await OnOutput.Invoke(decodedBuffer);
}
if (readResult.EOF)
break;
}
catch (OperationCanceledException)
{
// Ignored
}
catch (Exception e)
{
Logger.LogError(e, "An unhandled error occured while processing container stream");
}
}
ArrayPool<byte>.Shared.Return(buffer);
}
catch (OperationCanceledException)
{
// Ignored
}
catch (Exception e)
{
Logger.LogError(e, "An unhandled error occured while handling container attaching");
}
}
Logger.LogTrace("Attach loop exited");
});
}
public async Task WriteInputAsync(string value)
{
if (Stream == null)
throw new AggregateException("Stream is not available. Container might not be attached");
var buffer = Encoding.UTF8.GetBytes(value);
await Stream.WriteAsync(buffer, 0, buffer.Length, Cts.Token);
}
public async Task ClearCacheAsync()
{
await CacheLock.WaitAsync(Cts.Token);
try
{
Cache.Clear();
}
finally
{
CacheLock.Release();
}
}
public async Task<string[]> GetCacheAsync()
{
await CacheLock.WaitAsync();
try
{
return Cache.ToArray();
}
finally
{
CacheLock.Release();
}
}
public async ValueTask DisposeAsync()
{
await Cts.CancelAsync();
Stream?.Dispose();
CacheLock.Dispose();
}
}

View File

@@ -0,0 +1,93 @@
using Docker.DotNet;
using Docker.DotNet.Models;
using MoonlightServers.Daemon.ServerSystem.Implementations.Docker.Events;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
public class DockerEventService : BackgroundService
{
public event Func<ContainerDieEvent, Task>? OnContainerDied;
private readonly ILogger<DockerEventService> Logger;
private readonly DockerClient DockerClient;
public DockerEventService(
ILogger<DockerEventService> logger,
DockerClient dockerClient
)
{
Logger = logger;
DockerClient = dockerClient;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Logger.LogTrace("Starting up docker event monitor");
while (!stoppingToken.IsCancellationRequested)
{
try
{
Logger.LogTrace("Monitoring events");
await DockerClient.System.MonitorEventsAsync(
new ContainerEventsParameters(),
new Progress<Message>(OnEventAsync),
stoppingToken
);
}
catch (OperationCanceledException)
{
// ignored
}
catch (Exception e)
{
Logger.LogError(e, "An error occured while processing container event monitoring");
}
}
Logger.LogTrace("Closed docker event monitor");
}
private async void OnEventAsync(Message message)
{
try
{
switch (message.Type)
{
case "container":
var containerId = message.Actor.ID;
switch (message.Action)
{
case "die":
if (
!message.Actor.Attributes.TryGetValue("exitCode", out var exitCodeStr) ||
!int.TryParse(exitCodeStr, out var exitCode)
)
{
return;
}
if (OnContainerDied != null)
await OnContainerDied.Invoke(new ContainerDieEvent(containerId, exitCode));
return;
}
break;
}
}
catch (Exception e)
{
Logger.LogError(
e,
"An error occured while handling event {type} for {action}",
message.Type,
message.Action
);
}
}
}

View File

@@ -0,0 +1,63 @@
using Docker.DotNet;
using MoonlightServers.Daemon.ServerSystem.Abstractions;
using MoonlightServers.Daemon.ServerSystem.Implementations.Docker.Events;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
public class DockerInstallEnv : IInstallEnvironment
{
public IInstallStatistics Statistics => InnerStatistics;
public IInstallConsole Console => InnerConsole;
public event Func<Task>? OnExited;
public string ContainerId { get; }
private readonly DockerClient DockerClient;
private readonly ILogger Logger;
private readonly DockerEventService EventService;
private readonly DockerStatistics InnerStatistics;
private readonly DockerConsole InnerConsole;
public DockerInstallEnv(string containerId, DockerClient dockerClient, ILogger logger, DockerEventService eventService)
{
ContainerId = containerId;
DockerClient = dockerClient;
Logger = logger;
EventService = eventService;
InnerStatistics = new DockerStatistics();
InnerConsole = new DockerConsole(containerId, dockerClient, logger);
EventService.OnContainerDied += HandleDieEventAsync;
}
public async Task<bool> IsRunningAsync()
{
var container = await DockerClient.Containers.InspectContainerAsync(ContainerId);
return container.State.Running;
}
public async Task StartAsync()
=> await DockerClient.Containers.StartContainerAsync(ContainerId, new());
public async Task KillAsync()
=> await DockerClient.Containers.KillContainerAsync(ContainerId, new());
private async Task HandleDieEventAsync(ContainerDieEvent dieEvent)
{
if(dieEvent.ContainerId != ContainerId)
return;
if(OnExited != null)
await OnExited.Invoke();
}
public async ValueTask DisposeAsync()
{
EventService.OnContainerDied -= HandleDieEventAsync;
await InnerConsole.DisposeAsync();
}
}

View File

@@ -0,0 +1,109 @@
using Docker.DotNet;
using MoonlightServers.Daemon.Models;
using MoonlightServers.Daemon.ServerSystem.Abstractions;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
public class DockerInstallEnvService : IInstallEnvironmentService
{
private readonly DockerClient DockerClient;
private readonly ILoggerFactory LoggerFactory;
private readonly DockerEventService DockerEventService;
private const string NameTemplate = "ml-install-{0}";
public DockerInstallEnvService(DockerClient dockerClient, ILoggerFactory loggerFactory,
DockerEventService dockerEventService)
{
DockerClient = dockerClient;
LoggerFactory = loggerFactory;
DockerEventService = dockerEventService;
}
public async Task<IInstallEnvironment?> FindAsync(string id)
{
try
{
var dockerInspect = await DockerClient.Containers.InspectContainerAsync(
string.Format(NameTemplate, id)
);
var logger =
LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})");
return new DockerInstallEnv(dockerInspect.ID, DockerClient, logger, DockerEventService);
}
catch (DockerContainerNotFoundException)
{
return null;
}
}
public async Task<IInstallEnvironment> CreateAsync(
string id,
RuntimeConfiguration runtimeConfiguration,
InstallConfiguration installConfiguration,
IInstallStorage installStorage,
IRuntimeStorage runtimeStorage
)
{
try
{
var dockerInspect = await DockerClient.Containers.InspectContainerAsync(
string.Format(NameTemplate, id)
);
if (dockerInspect.State.Running)
await DockerClient.Containers.KillContainerAsync(dockerInspect.ID, new());
await DockerClient.Containers.RemoveContainerAsync(dockerInspect.ID, new());
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
var runtimeStoragePath = await runtimeStorage.GetHostPathAsync();
var installStoragePath = await installStorage.GetHostPathAsync();
var parameters = ConfigMapper.GetInstallConfig(
id,
string.Format(NameTemplate, id),
runtimeConfiguration,
installConfiguration,
runtimeStoragePath,
installStoragePath
);
var container = await DockerClient.Containers.CreateContainerAsync(parameters);
var logger = LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})");
return new DockerInstallEnv(container.ID, DockerClient, logger, DockerEventService);
}
public async Task DeleteAsync(IInstallEnvironment environment)
{
if (environment is not DockerInstallEnv dockerInstallEnv)
throw new ArgumentException(
$"You cannot delete runtime environments which haven't been created by {nameof(DockerInstallEnv)}");
await dockerInstallEnv.DisposeAsync();
try
{
var dockerInspect = await DockerClient.Containers.InspectContainerAsync(
dockerInstallEnv.ContainerId
);
if (dockerInspect.State.Running)
await DockerClient.Containers.KillContainerAsync(dockerInspect.ID, new());
await DockerClient.Containers.RemoveContainerAsync(dockerInspect.ID, new());
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
}
}

View File

@@ -0,0 +1,61 @@
using Docker.DotNet;
using MoonlightServers.Daemon.ServerSystem.Abstractions;
using MoonlightServers.Daemon.ServerSystem.Implementations.Docker.Events;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
public class DockerRuntimeEnv : IRuntimeEnvironment
{
public IRuntimeStatistics Statistics => InnerStatistics;
public IRuntimeConsole Console => InnerConsole;
public string ContainerId { get; }
public event Func<Task>? OnExited;
private readonly DockerClient DockerClient;
private readonly DockerEventService EventService;
private readonly DockerConsole InnerConsole;
private readonly DockerStatistics InnerStatistics;
public DockerRuntimeEnv(string containerId, DockerClient dockerClient, ILogger logger, DockerEventService eventService)
{
ContainerId = containerId;
DockerClient = dockerClient;
EventService = eventService;
InnerStatistics = new DockerStatistics();
InnerConsole = new DockerConsole(containerId, dockerClient, logger);
EventService.OnContainerDied += HandleDieEventAsync;
}
public async Task<bool> IsRunningAsync()
{
var container = await DockerClient.Containers.InspectContainerAsync(ContainerId);
return container.State.Running;
}
public async Task StartAsync()
=> await DockerClient.Containers.StartContainerAsync(ContainerId, new());
public async Task KillAsync()
=> await DockerClient.Containers.KillContainerAsync(ContainerId, new());
private async Task HandleDieEventAsync(ContainerDieEvent dieEvent)
{
if(dieEvent.ContainerId != ContainerId)
return;
if(OnExited != null)
await OnExited.Invoke();
}
public async ValueTask DisposeAsync()
{
EventService.OnContainerDied -= HandleDieEventAsync;
await InnerConsole.DisposeAsync();
}
}

View File

@@ -0,0 +1,112 @@
using Docker.DotNet;
using MoonlightServers.Daemon.Models;
using MoonlightServers.Daemon.ServerSystem.Abstractions;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
public class DockerRuntimeEnvService : IRuntimeEnvironmentService
{
private readonly DockerClient DockerClient;
private readonly ILoggerFactory LoggerFactory;
private readonly DockerEventService DockerEventService;
private const string NameTemplate = "ml-runtime-{0}";
public DockerRuntimeEnvService(DockerClient dockerClient, ILoggerFactory loggerFactory,
DockerEventService dockerEventService)
{
DockerClient = dockerClient;
LoggerFactory = loggerFactory;
DockerEventService = dockerEventService;
}
public async Task<IRuntimeEnvironment?> FindAsync(string id)
{
try
{
var dockerInspect = await DockerClient.Containers.InspectContainerAsync(
string.Format(NameTemplate, id)
);
var logger =
LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})");
return new DockerRuntimeEnv(dockerInspect.ID, DockerClient, logger, DockerEventService);
}
catch (DockerContainerNotFoundException)
{
return null;
}
}
public async Task<IRuntimeEnvironment> CreateAsync(
string id,
RuntimeConfiguration configuration,
IRuntimeStorage storage
)
{
try
{
var dockerInspect = await DockerClient.Containers.InspectContainerAsync(
string.Format(NameTemplate, id)
);
if (dockerInspect.State.Running)
await DockerClient.Containers.KillContainerAsync(dockerInspect.ID, new());
await DockerClient.Containers.RemoveContainerAsync(dockerInspect.ID, new());
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
var storagePath = await storage.GetHostPathAsync();
var parameters = ConfigMapper.GetRuntimeConfig(
id,
string.Format(NameTemplate, id),
configuration,
storagePath
);
var container = await DockerClient.Containers.CreateContainerAsync(parameters);
var logger = LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})");
return new DockerRuntimeEnv(container.ID, DockerClient, logger, DockerEventService);
}
public Task UpdateAsync(IRuntimeEnvironment environment, RuntimeConfiguration configuration)
{
throw new NotImplementedException();
}
public async Task DeleteAsync(IRuntimeEnvironment environment)
{
if (environment is not DockerRuntimeEnv dockerRuntimeEnv)
{
throw new ArgumentException(
$"You cannot delete runtime environments which haven't been created by {nameof(DockerRuntimeEnvService)}"
);
}
await dockerRuntimeEnv.DisposeAsync();
try
{
var dockerInspect = await DockerClient.Containers.InspectContainerAsync(
dockerRuntimeEnv.ContainerId
);
if (dockerInspect.State.Running)
await DockerClient.Containers.KillContainerAsync(dockerInspect.ID, new());
await DockerClient.Containers.RemoveContainerAsync(dockerInspect.ID, new());
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
}
}

View File

@@ -0,0 +1,14 @@
using MoonlightServers.Daemon.ServerSystem.Abstractions;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
public class DockerStatistics : IRuntimeStatistics, IInstallStatistics
{
public event Func<ServerStatistics, Task>? OnStatisticsReceived;
public Task AttachAsync() => Task.CompletedTask;
public Task ClearCacheAsync() => Task.CompletedTask;
public Task<ServerStatistics[]> GetCacheAsync() => Task.FromResult<ServerStatistics[]>([]);
}

View File

@@ -0,0 +1,3 @@
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker.Events;
public record ContainerDieEvent(string ContainerId, int ExitCode);

View File

@@ -0,0 +1,22 @@
using Docker.DotNet;
using MoonlightServers.Daemon.ServerSystem.Abstractions;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
public static class Extensions
{
public static void AddDockerServices(this IServiceCollection collection)
{
var client = new DockerClientBuilder()
.WithEndpoint(new Uri("unix:///var/run/docker.sock"))
.Build();
collection.AddSingleton(client);
collection.AddSingleton<DockerEventService>();
collection.AddHostedService(sp => sp.GetRequiredService<DockerEventService>());
collection.AddSingleton<IRuntimeEnvironmentService, DockerRuntimeEnvService>();
collection.AddSingleton<IInstallEnvironmentService, DockerInstallEnvService>();
}
}

View File

@@ -0,0 +1,12 @@
using MoonlightServers.Daemon.ServerSystem.Abstractions;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local;
public static class Extensions
{
public static void AddLocalServices(this IServiceCollection services)
{
services.AddSingleton<IRuntimeStorageService, LocalRuntimeStorageService>();
services.AddSingleton<IInstallStorageService, LocalInstallStorageService>();
}
}

View File

@@ -0,0 +1,15 @@
using MoonlightServers.Daemon.ServerSystem.Abstractions;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local;
public class LocalInstallStorage : IInstallStorage
{
public string HostPath { get; }
public LocalInstallStorage(string hostPath)
{
HostPath = hostPath;
}
public Task<string> GetHostPathAsync() => Task.FromResult(HostPath);
}

View File

@@ -0,0 +1,43 @@
using MoonlightServers.Daemon.Models;
using MoonlightServers.Daemon.ServerSystem.Abstractions;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local;
public class LocalInstallStorageService : IInstallStorageService
{
private const string HostPathTemplate = "./mldaemon/install/{0}";
public Task<IInstallStorage?> FindAsync(string id)
{
var path = string.Format(HostPathTemplate, id);
if (!Directory.Exists(path))
return Task.FromResult<IInstallStorage?>(null);
return Task.FromResult<IInstallStorage?>(new LocalInstallStorage(path));
}
public Task<IInstallStorage> CreateAsync(string id, RuntimeConfiguration runtimeConfiguration, InstallConfiguration installConfiguration)
{
var path = string.Format(HostPathTemplate, id);
Directory.CreateDirectory(path);
return Task.FromResult<IInstallStorage>(new LocalInstallStorage(path));
}
public Task DeleteAsync(IInstallStorage installStorage)
{
if (installStorage is not LocalInstallStorage localInstallStorage)
{
throw new ArgumentException(
$"You cannot delete install storages which haven't been created by {nameof(LocalInstallStorageService)}"
);
}
if(Directory.Exists(localInstallStorage.HostPath))
Directory.Delete(localInstallStorage.HostPath, true);
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,15 @@
using MoonlightServers.Daemon.ServerSystem.Abstractions;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local;
public class LocalRuntimeStorage : IRuntimeStorage
{
public string HostPath { get; }
public LocalRuntimeStorage(string hostPath)
{
HostPath = hostPath;
}
public Task<string> GetHostPathAsync() => Task.FromResult(HostPath);
}

View File

@@ -0,0 +1,46 @@
using MoonlightServers.Daemon.Models;
using MoonlightServers.Daemon.ServerSystem.Abstractions;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local;
public class LocalRuntimeStorageService : IRuntimeStorageService
{
private const string HostPathTemplate = "./mldaemon/runtime/{0}";
public Task<IRuntimeStorage?> FindAsync(string id)
{
var path = string.Format(HostPathTemplate, id);
if (!Directory.Exists(path))
return Task.FromResult<IRuntimeStorage?>(null);
return Task.FromResult<IRuntimeStorage?>(new LocalRuntimeStorage(path));
}
public Task<IRuntimeStorage> CreateAsync(string id, RuntimeConfiguration configuration)
{
var path = string.Format(HostPathTemplate, id);
Directory.CreateDirectory(path);
return Task.FromResult<IRuntimeStorage>(new LocalRuntimeStorage(path));
}
public Task UpdateAsync(IRuntimeStorage runtimeStorage, RuntimeConfiguration configuration)
=> Task.CompletedTask;
public Task DeleteAsync(IRuntimeStorage runtimeStorage)
{
if (runtimeStorage is not LocalRuntimeStorage localRuntimeStorage)
{
throw new ArgumentException(
$"You cannot delete runtime storages which haven't been created by {nameof(LocalRuntimeStorageService)}"
);
}
if(Directory.Exists(localRuntimeStorage.HostPath))
Directory.Delete(localRuntimeStorage.HostPath, true);
return Task.CompletedTask;
}
}

View File

@@ -1,52 +0,0 @@
using System.Text.RegularExpressions;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.Implementations;
public class RegexOnlineDetector : IOnlineDetector
{
private readonly ServerContext Context;
private Regex? Expression;
public RegexOnlineDetector(ServerContext context)
{
Context = context;
}
public Task InitializeAsync()
=> Task.CompletedTask;
public Task CreateAsync()
{
if(string.IsNullOrEmpty(Context.Configuration.OnlineDetection))
return Task.CompletedTask;
Expression = new Regex(Context.Configuration.OnlineDetection, RegexOptions.Compiled);
return Task.CompletedTask;
}
public Task<bool> HandleOutputAsync(string line)
{
if (Expression == null)
return Task.FromResult(false);
var result = Expression.Matches(line).Count > 0;
return Task.FromResult(result);
}
public Task DestroyAsync()
{
Expression = null;
return Task.CompletedTask;
}
public ValueTask DisposeAsync()
{
Expression = null;
return ValueTask.CompletedTask;
}
}

View File

@@ -1,44 +0,0 @@
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.Implementations;
public class ServerReporter : IReporter
{
private readonly ServerContext Context;
private const string StatusTemplate =
"\x1b[1;38;2;200;90;200mM\x1b[1;38;2;204;110;230mo\x1b[1;38;2;170;130;245mo\x1b[1;38;2;140;150;255mn\x1b[1;38;2;110;180;255ml\x1b[1;38;2;100;200;255mi\x1b[1;38;2;100;220;255mg\x1b[1;38;2;120;235;255mh\x1b[1;38;2;140;250;255mt\x1b[0m \x1b[3;38;2;200;200;200m{0}\x1b[0m\n\r";
private const string ErrorTemplate =
"\x1b[1;38;2;200;90;200mM\x1b[1;38;2;204;110;230mo\x1b[1;38;2;170;130;245mo\x1b[1;38;2;140;150;255mn\x1b[1;38;2;110;180;255ml\x1b[1;38;2;100;200;255mi\x1b[1;38;2;100;220;255mg\x1b[1;38;2;120;235;255mh\x1b[1;38;2;140;250;255mt\x1b[0m \x1b[1;38;2;255;0;0m{0}\x1b[0m\n\r";
public ServerReporter(ServerContext context)
{
Context = context;
}
public Task InitializeAsync()
=> Task.CompletedTask;
public async Task StatusAsync(string message)
{
Context.Logger.LogInformation("Status: {message}", message);
await Context.Server.Console.WriteStdOutAsync(
string.Format(StatusTemplate, message)
);
}
public async Task ErrorAsync(string message)
{
Context.Logger.LogError("Error: {message}", message);
await Context.Server.Console.WriteStdOutAsync(
string.Format(ErrorTemplate, message)
);
}
public ValueTask DisposeAsync()
=> ValueTask.CompletedTask;
}

View File

@@ -1,64 +0,0 @@
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
public interface IConsole : IServerComponent
{
/// <summary>
/// Writes to the standard input of the console. If attached to the runtime when using docker for example this
/// would write into the containers standard input.
/// <remarks>This method does not add a newline separator at the end of the content. The caller needs to add this themselves if required</remarks>
/// </summary>
/// <param name="content">Content to write</param>
/// <returns></returns>
public Task WriteStdInAsync(string content);
/// <summary>
/// Writes to the standard output of the console. If attached to the runtime when using docker for example this
/// would write into the containers standard output.
/// <remarks>This method does not add a newline separator at the end of the content. The caller needs to add this themselves if required</remarks>
/// </summary>
/// <param name="content">Content to write</param>
/// <returns></returns>
public Task WriteStdOutAsync(string content);
/// <summary>
/// Attaches the console to the runtime environment
/// </summary>
/// <returns></returns>
public Task AttachRuntimeAsync();
/// <summary>
/// Attaches the console to the installation environment
/// </summary>
/// <returns></returns>
public Task AttachInstallationAsync();
/// <summary>
/// Fetches all output from the runtime environment and write them into the cache without triggering any events
/// </summary>
/// <returns></returns>
public Task FetchRuntimeAsync();
/// <summary>
/// Fetches all output from the installation environment and write them into the cache without triggering any events
/// </summary>
/// <returns></returns>
public Task FetchInstallationAsync();
/// <summary>
/// Clears the cache of the standard output received by the environments
/// </summary>
/// <returns></returns>
public Task ClearCacheAsync();
/// <summary>
/// Gets the content from the standard output cache
/// </summary>
/// <returns>Content from the cache</returns>
public Task<IEnumerable<string>> GetCacheAsync();
/// <summary>
/// Subscribes to standard output receive events
/// </summary>
/// <param name="callback">Callback which will be invoked whenever a new line is received</param>
/// <returns>Subscription disposable to unsubscribe from the event</returns>
public Task<IAsyncDisposable> SubscribeStdOutAsync(Func<string, ValueTask> callback);
}

View File

@@ -1,54 +0,0 @@
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
public interface IFileSystem : IServerComponent
{
/// <summary>
/// Gets the path of the file system on the host operating system to be reused by other components
/// </summary>
/// <returns>Path to the file systems storage location</returns>
public Task<string> GetPathAsync();
/// <summary>
/// Checks if the file system exists
/// </summary>
/// <returns>True if it does exist. False if it doesn't exist</returns>
public Task<bool> CheckExistsAsync();
/// <summary>
/// Checks if the file system is mounted
/// </summary>
/// <returns>True if its mounted, False if it is not mounted</returns>
public Task<bool> CheckMountedAsync();
/// <summary>
/// Creates the file system. E.g. Creating a virtual disk, formatting it
/// </summary>
/// <returns></returns>
public Task CreateAsync();
/// <summary>
/// Performs checks and optimisations on the file system.
/// E.g. checking for corrupted files, resizing a virtual disk or adjusting file permissions
/// <remarks>Requires <see cref="MountAsync"/> to be called before or the file system to be in a mounted state</remarks>
/// </summary>
/// <returns></returns>
public Task PerformChecksAsync();
/// <summary>
/// Mounts the file system
/// </summary>
/// <returns></returns>
public Task MountAsync();
/// <summary>
/// Unmounts the file system
/// </summary>
/// <returns></returns>
public Task UnmountAsync();
/// <summary>
/// Destroys the file system and its contents
/// </summary>
/// <returns></returns>
public Task DestroyAsync();
}

View File

@@ -1,53 +0,0 @@
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
public interface IInstallation : IServerComponent
{
/// <summary>
/// Checks if the installation environment exists. It doesn't matter if it is currently running or not
/// </summary>
/// <returns>True if it exists, False if it doesn't</returns>
public Task<bool> CheckExistsAsync();
/// <summary>
/// Creates the installation environment
/// </summary>
/// <param name="runtimePath">Host path of the runtime storage location</param>
/// <param name="hostPath">Host path of the installation file system</param>
/// <param name="data">Installation data for the server</param>
/// <returns></returns>
public Task CreateAsync(string runtimePath, string hostPath, ServerInstallDataResponse data);
/// <summary>
/// Starts the installation
/// </summary>
/// <returns></returns>
public Task StartAsync();
/// <summary>
/// Kills the current installation immediately
/// </summary>
/// <returns></returns>
public Task KillAsync();
/// <summary>
/// Removes the installation. E.g. removes the docker container
/// </summary>
/// <returns></returns>
public Task DestroyAsync();
/// <summary>
/// Subscribes to the event when the installation exists
/// </summary>
/// <param name="callback">Callback to invoke whenever the installation exists</param>
/// <returns>Subscription disposable to unsubscribe from the event</returns>
public Task<IAsyncDisposable> SubscribeExitedAsync(Func<int, ValueTask> callback);
/// <summary>
/// Connects an existing installation to this abstraction in order to restore it.
/// E.g. fetching the container id and using it for exit events
/// </summary>
/// <returns></returns>
public Task RestoreAsync();
}

View File

@@ -1,23 +0,0 @@
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
public interface IOnlineDetector : IServerComponent
{
/// <summary>
/// Creates the detection engine for the online state
/// </summary>
/// <returns></returns>
public Task CreateAsync();
/// <summary>
/// Handles the detection of the online state based on the received output
/// </summary>
/// <param name="line">Excerpt of the output</param>
/// <returns>True if the detection showed that the server is online. False if the detection didnt find anything</returns>
public Task<bool> HandleOutputAsync(string line);
/// <summary>
/// Destroys the detection engine for the online state
/// </summary>
/// <returns></returns>
public Task DestroyAsync();
}

View File

@@ -1,18 +0,0 @@
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
public interface IReporter : IServerComponent
{
/// <summary>
/// Writes both in the server logs as well in the server console the provided message as a status update
/// </summary>
/// <param name="message">Message to write</param>
/// <returns></returns>
public Task StatusAsync(string message);
/// <summary>
/// Writes both in the server logs as well in the server console the provided message as an error
/// </summary>
/// <param name="message">Message to write</param>
/// <returns></returns>
public Task ErrorAsync(string message);
}

View File

@@ -1,16 +0,0 @@
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
public interface IRestorer : IServerComponent
{
/// <summary>
/// Checks for any running runtime environment from which the state can be restored from
/// </summary>
/// <returns></returns>
public Task<bool> HandleRuntimeAsync();
/// <summary>
/// Checks for any running installation environment from which the state can be restored from
/// </summary>
/// <returns></returns>
public Task<bool> HandleInstallationAsync();
}

View File

@@ -1,55 +0,0 @@
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
public interface IRuntime : IServerComponent
{
/// <summary>
/// Checks if the runtime does exist. This includes already running instances
/// </summary>
/// <returns>True if it exists, False if it doesn't</returns>
public Task<bool> CheckExistsAsync();
/// <summary>
/// Creates the runtime with the specified path as the storage path where the server files should be stored in
/// </summary>
/// <param name="path">Path where the server files are located</param>
/// <returns></returns>
public Task CreateAsync(string path);
/// <summary>
/// Starts the runtime. This requires <see cref="CreateAsync"/> to be called before this function
/// </summary>
/// <returns></returns>
public Task StartAsync();
/// <summary>
/// Performs a live update on the runtime. When this method is called the current server configuration has already been updated
/// </summary>
/// <returns></returns>
public Task UpdateAsync();
/// <summary>
/// Kills the current runtime immediately
/// </summary>
/// <returns></returns>
public Task KillAsync();
/// <summary>
/// Destroys the runtime. When implemented using docker this would remove the container used for hosting the runtime
/// </summary>
/// <returns></returns>
public Task DestroyAsync();
/// <summary>
/// This subscribes to the exited event of the runtime
/// </summary>
/// <param name="callback">Callback gets invoked whenever the runtime exites</param>
/// <returns>Subscription disposable to unsubscribe from the event</returns>
public Task<IAsyncDisposable> SubscribeExitedAsync(Func<int, ValueTask> callback);
/// <summary>
/// Connects an existing runtime to this abstraction in order to restore it.
/// E.g. fetching the container id and using it for exit events
/// </summary>
/// <returns></returns>
public Task RestoreAsync();
}

View File

@@ -1,10 +0,0 @@
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
public interface IServerComponent : IAsyncDisposable
{
/// <summary>
/// Initializes the server component
/// </summary>
/// <returns></returns>
public Task InitializeAsync();
}

View File

@@ -1,9 +0,0 @@
using MoonlightServers.Daemon.ServerSystem.Enums;
using Stateless;
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
public interface IServerStateHandler : IAsyncDisposable
{
public Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition);
}

View File

@@ -1,30 +0,0 @@
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
public interface IStatistics : IServerComponent
{
/// <summary>
/// Attaches the statistics collector to the currently running runtime
/// </summary>
/// <returns></returns>
public Task AttachRuntimeAsync();
/// <summary>
/// Attaches the statistics collector to the currently running installation
/// </summary>
/// <returns></returns>
public Task AttachInstallationAsync();
/// <summary>
/// Clears the statistics cache
/// </summary>
/// <returns></returns>
public Task ClearCacheAsync();
/// <summary>
/// Gets the statistics data from the cache
/// </summary>
/// <returns>All data from the cache</returns>
public Task<IEnumerable<StatisticsData>> GetCacheAsync();
}

View File

@@ -1,12 +0,0 @@
using MoonlightServers.Daemon.Models.Cache;
namespace MoonlightServers.Daemon.ServerSystem.Models;
public class ServerContext
{
public ServerConfiguration Configuration { get; set; }
public int Identifier { get; set; }
public AsyncServiceScope ServiceScope { get; set; }
public Server Server { get; set; }
public ILogger Logger { get; set; }
}

View File

@@ -1,6 +0,0 @@
namespace MoonlightServers.Daemon.ServerSystem.Models;
public class StatisticsData
{
}

View File

@@ -0,0 +1,37 @@
namespace MoonlightServers.Daemon.ServerSystem;
public partial class Server
{
public async Task DeleteAsync()
{
await Lock.WaitAsync();
try
{
if(State != ServerState.Offline)
throw new InvalidOperationException("Server is not offline");
Logger.LogTrace("Deleting");
InstallStorage ??= await InstallStorageService.FindAsync(Uuid);
if (InstallStorage != null)
{
Logger.LogTrace("Deleting install storage");
await InstallStorageService.DeleteAsync(InstallStorage);
}
RuntimeStorage ??= await RuntimeStorageService.FindAsync(Uuid);
if (RuntimeStorage != null)
{
Logger.LogTrace("Deleting runtime storage");
await RuntimeStorageService.DeleteAsync(RuntimeStorage);
}
}
finally
{
Lock.Release();
}
}
}

View File

@@ -0,0 +1,157 @@
namespace MoonlightServers.Daemon.ServerSystem;
public partial class Server
{
public async Task InstallAsync()
{
await Lock.WaitAsync();
try
{
if (State != ServerState.Offline)
throw new InvalidOperationException("Server is not offline");
// Check if any pre-existing install env exists, if we don't have a reference to it already
InstallEnvironment ??= await InstallEnvironmentService.FindAsync(Uuid);
// Check if storages exist
InstallStorage ??= await InstallStorageService.FindAsync(Uuid);
RuntimeStorage ??= await RuntimeStorageService.FindAsync(Uuid);
// Remove any pre-existing installation env
if (InstallEnvironment != null)
{
Logger.LogTrace("Destroying pre-existing install environment");
if (await InstallEnvironment.IsRunningAsync())
{
Logger.LogTrace("Pre-existing install environment is still running, killing it");
await InstallEnvironment.KillAsync();
}
// Remove any event handlers if existing
InstallEnvironment.Console.OnOutput -= OnConsoleMessageAsync;
InstallEnvironment.Statistics.OnStatisticsReceived -= OnStatisticsReceivedAsync;
InstallEnvironment.OnExited -= OnInstallExitedAsync;
// Now remove it
// Finally remove it
await InstallEnvironmentService.DeleteAsync(InstallEnvironment);
InstallEnvironment = null;
Logger.LogTrace("Pre-existing install environment destroyed");
}
// Remove pre-existing installation storage
if (InstallStorage != null)
{
Logger.LogTrace("Destroying pre-existing installation storage");
await InstallStorageService.DeleteAsync(InstallStorage);
InstallStorage = null;
}
// Fetch the latest configuration
Logger.LogTrace("Fetching latest configuration");
RuntimeConfiguration = await ConfigurationService.GetRuntimeConfigurationAsync(Uuid);
InstallConfiguration = await ConfigurationService.GetInstallConfigurationAsync(Uuid);
// Ensure runtime storage
if (RuntimeStorage == null)
{
Logger.LogTrace("Creating runtime storage");
RuntimeStorage = await RuntimeStorageService.CreateAsync(Uuid, RuntimeConfiguration);
}
else
{
Logger.LogTrace("Updating runtime storage");
await RuntimeStorageService.UpdateAsync(RuntimeStorage, RuntimeConfiguration);
}
// Create installation storage
Logger.LogTrace("Creating installation storage");
InstallStorage = await InstallStorageService.CreateAsync(Uuid, RuntimeConfiguration, InstallConfiguration);
// Write install script
var installStoragePath = await InstallStorage.GetHostPathAsync();
await File.WriteAllTextAsync(
Path.Combine(installStoragePath, "install.sh"),
InstallConfiguration.Script
);
// Create env
Logger.LogTrace("Creating install environment");
InstallEnvironment = await InstallEnvironmentService.CreateAsync(
Uuid,
RuntimeConfiguration,
InstallConfiguration,
InstallStorage,
RuntimeStorage
);
// Add event handlers
Logger.LogTrace("Attaching to install environment");
InstallEnvironment.Console.OnOutput += OnConsoleMessageAsync;
InstallEnvironment.Statistics.OnStatisticsReceived += OnStatisticsReceivedAsync;
InstallEnvironment.OnExited += OnInstallExitedAsync;
// Attach console and statistics
await InstallEnvironment.Console.AttachAsync();
await InstallEnvironment.Statistics.AttachAsync();
// Finally start the env
Logger.LogTrace("Starting install environment");
await InstallEnvironment.StartAsync();
await ChangeStateAsync(ServerState.Installing);
}
finally
{
Lock.Release();
}
}
private async Task OnInstallExitedAsync()
{
Logger.LogTrace("Install environment exited, checking result and cleaning up");
await Lock.WaitAsync();
try
{
// TODO: Handle crash
if (InstallEnvironment == null)
throw new InvalidOperationException("Install environment is not set");
// Make sure no event handler is there
InstallEnvironment.Console.OnOutput -= OnConsoleMessageAsync;
InstallEnvironment.Statistics.OnStatisticsReceived -= OnStatisticsReceivedAsync;
InstallEnvironment.OnExited -= OnInstallExitedAsync;
// Remove env
await InstallEnvironmentService.DeleteAsync(InstallEnvironment);
InstallEnvironment = null;
Logger.LogTrace("Install environment cleaned up");
if(InstallStorage == null)
throw new InvalidOperationException("Install storage is not set");
Logger.LogTrace("Cleaned up install storage");
await InstallStorageService.DeleteAsync(InstallStorage);
InstallStorage = null;
}
finally
{
Lock.Release();
}
await ChangeStateAsync(ServerState.Offline);
}
}

View File

@@ -0,0 +1,163 @@
namespace MoonlightServers.Daemon.ServerSystem;
public partial class Server
{
public async Task StartAsync()
{
await Lock.WaitAsync();
try
{
if (State != ServerState.Offline)
throw new InvalidOperationException("Server is not offline");
// Check for any pre-existing runtime environment, if we don't have a reference already
RuntimeEnvironment ??= await RuntimeEnvironmentService.FindAsync(Uuid);
RuntimeStorage ??= await RuntimeStorageService.FindAsync(Uuid);
// Remove any pre-existing environment
if (RuntimeEnvironment != null)
{
Logger.LogTrace("Destroying pre-existing runtime environment");
if (await RuntimeEnvironment.IsRunningAsync())
{
Logger.LogTrace("Pre-existing runtime environment is still running, killing it");
await RuntimeEnvironment.KillAsync();
}
// Make sure no event handler is there anymore
RuntimeEnvironment.Console.OnOutput -= OnConsoleMessageAsync;
RuntimeEnvironment.Statistics.OnStatisticsReceived -= OnStatisticsReceivedAsync;
RuntimeEnvironment.OnExited -= OnRuntimeExitedAsync;
// Finally remove it
await RuntimeEnvironmentService.DeleteAsync(RuntimeEnvironment);
RuntimeEnvironment = null;
Logger.LogTrace("Pre-existing runtime environment destroyed");
}
// Fetch the latest config
Logger.LogTrace("Fetching latest configuration");
RuntimeConfiguration = await ConfigurationService.GetRuntimeConfigurationAsync(Uuid);
// Ensure runtime storage
if (RuntimeStorage == null)
{
Logger.LogTrace("Creating runtime storage");
RuntimeStorage = await RuntimeStorageService.CreateAsync(Uuid, RuntimeConfiguration);
}
else
{
Logger.LogTrace("Updating runtime storage");
await RuntimeStorageService.UpdateAsync(RuntimeStorage, RuntimeConfiguration);
}
// Create the environment
Logger.LogTrace("Creating runtime environment");
RuntimeEnvironment = await RuntimeEnvironmentService.CreateAsync(Uuid, RuntimeConfiguration, RuntimeStorage);
// Set event handlers
Logger.LogTrace("Attaching to runtime environment");
RuntimeEnvironment.Console.OnOutput += OnConsoleMessageAsync;
RuntimeEnvironment.Statistics.OnStatisticsReceived += OnStatisticsReceivedAsync;
RuntimeEnvironment.OnExited += OnRuntimeExitedAsync;
// Attach console & statistics
await RuntimeEnvironment.Console.AttachAsync();
await RuntimeEnvironment.Statistics.AttachAsync();
// Start up
Logger.LogTrace("Starting runtime environment");
await RuntimeEnvironment.StartAsync();
await ChangeStateAsync(ServerState.Starting);
}
finally
{
Lock.Release();
}
}
public async Task StopAsync()
{
await Lock.WaitAsync();
try
{
if (State is not (ServerState.Starting or ServerState.Online))
throw new InvalidOperationException("Server is not starting or online");
if (RuntimeEnvironment == null)
throw new InvalidOperationException("Runtime environment is not set");
Logger.LogTrace("Sending stop command to runtime environment");
await RuntimeEnvironment.Console.WriteInputAsync("stop\n\r");
await ChangeStateAsync(ServerState.Stopping);
}
finally
{
Lock.Release();
}
}
public async Task KillAsync()
{
await Lock.WaitAsync();
try
{
if (State is not (ServerState.Starting or ServerState.Online or ServerState.Stopping))
throw new InvalidOperationException("Server is not starting, stopping or online");
if (RuntimeEnvironment == null)
throw new InvalidOperationException("Runtime environment is not set");
Logger.LogTrace("Killing runtime environment");
await RuntimeEnvironment.KillAsync();
await ChangeStateAsync(ServerState.Stopping);
}
finally
{
Lock.Release();
}
}
private async Task OnRuntimeExitedAsync()
{
Logger.LogTrace("Runtime environment exited, checking result and cleaning up");
await Lock.WaitAsync();
try
{
// TODO: Handle crash
if (RuntimeEnvironment == null)
throw new InvalidOperationException("Runtime environment is not set");
// Make sure no event handler is there anymore
RuntimeEnvironment.Console.OnOutput -= OnConsoleMessageAsync;
RuntimeEnvironment.Statistics.OnStatisticsReceived -= OnStatisticsReceivedAsync;
RuntimeEnvironment.OnExited -= OnRuntimeExitedAsync;
// Finally remove it
await RuntimeEnvironmentService.DeleteAsync(RuntimeEnvironment);
RuntimeEnvironment = null;
Logger.LogTrace("Runtime environment cleaned up");
}
finally
{
Lock.Release();
}
await ChangeStateAsync(ServerState.Offline);
}
}

View File

@@ -0,0 +1,69 @@
namespace MoonlightServers.Daemon.ServerSystem;
public partial class Server
{
// Attempts to reattach to any running install or runtime environment that survived a daemon restart.
// Returns the appropriate state based on what was found, or Offline if nothing is running.
private async Task<ServerState> RestoreAsync()
{
// Install
Logger.LogTrace("Checking for existing install environment");
InstallEnvironment = await InstallEnvironmentService.FindAsync(Uuid);
InstallStorage = await InstallStorageService.FindAsync(Uuid);
if (InstallEnvironment != null)
{
var isRunning = await InstallEnvironment.IsRunningAsync();
if (isRunning)
{
Logger.LogTrace("Found running install environment, reattaching");
InstallEnvironment.Console.OnOutput += OnConsoleMessageAsync;
InstallEnvironment.Statistics.OnStatisticsReceived += OnStatisticsReceivedAsync;
InstallEnvironment.OnExited += OnInstallExitedAsync;
await InstallEnvironment.Console.AttachAsync();
await InstallEnvironment.Statistics.AttachAsync();
return ServerState.Installing;
}
Logger.LogTrace("Install environment exists but is not running, ignoring");
}
// Runtime
Logger.LogTrace("Checking for existing runtime environment");
RuntimeEnvironment = await RuntimeEnvironmentService.FindAsync(Uuid);
RuntimeStorage = await RuntimeStorageService.FindAsync(Uuid);
if (RuntimeEnvironment != null)
{
var isRunning = await RuntimeEnvironment.IsRunningAsync();
if (isRunning)
{
Logger.LogTrace("Found running runtime environment, reattaching");
RuntimeEnvironment.Console.OnOutput += OnConsoleMessageAsync;
RuntimeEnvironment.Statistics.OnStatisticsReceived += OnStatisticsReceivedAsync;
RuntimeEnvironment.OnExited += OnRuntimeExitedAsync;
await RuntimeEnvironment.Console.AttachAsync();
await RuntimeEnvironment.Statistics.AttachAsync();
// TODO: Use string online check here
return ServerState.Online;
}
Logger.LogTrace("Runtime environment exists but is not running, ignoring");
}
Logger.LogTrace("No running environments found");
return ServerState.Offline;
}
}

View File

@@ -1,161 +1,111 @@
using MoonlightServers.Daemon.ServerSystem.Enums;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using Stateless;
using MoonlightServers.Daemon.Models;
using MoonlightServers.Daemon.ServerSystem.Abstractions;
using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon.ServerSystem;
public partial class Server : IAsyncDisposable
{
public int Identifier => InnerContext.Identifier;
public ServerContext Context => InnerContext;
public ServerState State { get; private set; }
public IConsole Console { get; }
public IFileSystem RuntimeFileSystem { get; }
public IFileSystem InstallationFileSystem { get; }
public IInstallation Installation { get; }
public IOnlineDetector OnlineDetector { get; }
public IReporter Reporter { get; }
public IRestorer Restorer { get; }
public IRuntime Runtime { get; }
public IStatistics Statistics { get; }
public StateMachine<ServerState, ServerTrigger> StateMachine { get; private set; }
private IRuntimeEnvironment? RuntimeEnvironment;
private RuntimeConfiguration RuntimeConfiguration;
private IRuntimeStorage? RuntimeStorage;
private readonly IServerStateHandler[] Handlers;
private IInstallEnvironment? InstallEnvironment;
private InstallConfiguration InstallConfiguration;
private IInstallStorage? InstallStorage;
private readonly IServerComponent[] AllComponents;
private readonly ServerContext InnerContext;
private readonly IRuntimeEnvironmentService RuntimeEnvironmentService;
private readonly IInstallEnvironmentService InstallEnvironmentService;
private readonly IRuntimeStorageService RuntimeStorageService;
private readonly IInstallStorageService InstallStorageService;
private readonly ServerConfigurationService ConfigurationService;
private readonly string Uuid;
private readonly ILogger Logger;
private readonly SemaphoreSlim Lock = new(1, 1);
public Server(
ILogger logger,
ServerContext context,
IConsole console,
IFileSystem runtimeFileSystem,
IFileSystem installationFileSystem,
IInstallation installation,
IOnlineDetector onlineDetector,
IReporter reporter,
IRestorer restorer,
IRuntime runtime,
IStatistics statistics,
IEnumerable<IServerStateHandler> handlers,
IEnumerable<IServerComponent> additionalComponents
string uuid,
IRuntimeEnvironmentService runtimeEnvironmentService,
IInstallEnvironmentService installEnvironmentService,
IRuntimeStorageService runtimeStorageService,
IInstallStorageService installStorageService,
ServerConfigurationService configurationService,
ILogger logger
)
{
Uuid = uuid;
RuntimeEnvironmentService = runtimeEnvironmentService;
InstallEnvironmentService = installEnvironmentService;
RuntimeStorageService = runtimeStorageService;
InstallStorageService = installStorageService;
ConfigurationService = configurationService;
Logger = logger;
InnerContext = context;
Console = console;
RuntimeFileSystem = runtimeFileSystem;
InstallationFileSystem = installationFileSystem;
Installation = installation;
OnlineDetector = onlineDetector;
Reporter = reporter;
Restorer = restorer;
Runtime = runtime;
Statistics = statistics;
IEnumerable<IServerComponent> defaultComponents =
[
Console, RuntimeFileSystem, InstallationFileSystem, Installation, OnlineDetector, Reporter, Restorer,
Runtime, Statistics
];
AllComponents = defaultComponents.Concat(additionalComponents).ToArray();
Handlers = handlers.ToArray();
}
private void ConfigureStateMachine(ServerState initialState)
{
StateMachine = new StateMachine<ServerState, ServerTrigger>(
initialState, FiringMode.Queued
);
StateMachine.Configure(ServerState.Offline)
.Permit(ServerTrigger.Start, ServerState.Starting)
.Permit(ServerTrigger.Install, ServerState.Installing)
.PermitReentry(ServerTrigger.Fail);
StateMachine.Configure(ServerState.Starting)
.Permit(ServerTrigger.DetectOnline, ServerState.Online)
.Permit(ServerTrigger.Fail, ServerState.Offline)
.Permit(ServerTrigger.Exited, ServerState.Offline)
.Permit(ServerTrigger.Stop, ServerState.Stopping)
.Permit(ServerTrigger.Kill, ServerState.Stopping);
StateMachine.Configure(ServerState.Online)
.Permit(ServerTrigger.Stop, ServerState.Stopping)
.Permit(ServerTrigger.Kill, ServerState.Stopping)
.Permit(ServerTrigger.Exited, ServerState.Offline);
StateMachine.Configure(ServerState.Stopping)
.PermitReentry(ServerTrigger.Fail)
.PermitReentry(ServerTrigger.Kill)
.Permit(ServerTrigger.Exited, ServerState.Offline);
StateMachine.Configure(ServerState.Installing)
.Permit(ServerTrigger.Fail, ServerState.Offline) // TODO: Add kill
.Permit(ServerTrigger.Exited, ServerState.Offline);
}
private void ConfigureStateMachineEvents()
{
// Configure the calling of the handlers
StateMachine.OnTransitionedAsync(async transition =>
{
var hasFailed = false;
foreach (var handler in Handlers)
{
try
{
await handler.ExecuteAsync(transition);
}
catch (Exception e)
{
Logger.LogError(
e,
"Handler {name} has thrown an unexpected exception",
handler.GetType().FullName
);
hasFailed = true;
break;
}
}
if(!hasFailed)
return; // Everything went fine, we can exit now
// Something has failed, lets check if we can handle the error
// via a fail trigger
if(!StateMachine.CanFire(ServerTrigger.Fail))
return;
// Trigger the fail so the server gets a chance to handle the error softly
await StateMachine.FireAsync(ServerTrigger.Fail);
});
}
public async Task InitializeAsync()
{
foreach (var component in AllComponents)
await component.InitializeAsync();
Logger.LogTrace("Initializing");
var restoredState = ServerState.Offline;
await Lock.WaitAsync();
ConfigureStateMachine(restoredState);
ConfigureStateMachineEvents();
try
{
// Restore state
State = await RestoreAsync();
Logger.LogTrace("Initialization complete, restored to state {State}", State);
}
finally
{
Lock.Release();
}
}
private async Task OnConsoleMessageAsync(string message)
{
Console.WriteLine($"Console: {message}");
}
private async Task OnStatisticsReceivedAsync(ServerStatistics statistics)
{
}
private Task ChangeStateAsync(ServerState newState)
{
Logger.LogTrace("State changed from {OldState} to {NewState}", State, newState);
State = newState;
return Task.CompletedTask;
}
public async ValueTask DisposeAsync()
{
foreach (var handler in Handlers)
await handler.DisposeAsync();
foreach (var component in AllComponents)
await component.DisposeAsync();
Logger.LogTrace("Disposing");
if (RuntimeEnvironment != null)
{
Logger.LogTrace("Detaching and disposing runtime environment");
RuntimeEnvironment.Console.OnOutput -= OnConsoleMessageAsync;
RuntimeEnvironment.Statistics.OnStatisticsReceived -= OnStatisticsReceivedAsync;
RuntimeEnvironment.OnExited -= OnRuntimeExitedAsync;
await RuntimeEnvironment.DisposeAsync();
}
if (InstallEnvironment != null)
{
Logger.LogTrace("Detaching and disposing install environment");
InstallEnvironment.Console.OnOutput -= OnConsoleMessageAsync;
InstallEnvironment.Statistics.OnStatisticsReceived -= OnStatisticsReceivedAsync;
InstallEnvironment.OnExited -= OnInstallExitedAsync;
await InstallEnvironment.DisposeAsync();
}
}
}

View File

@@ -1,97 +1,46 @@
using MoonlightServers.Daemon.Models.Cache;
using MoonlightServers.Daemon.ServerSystem.Docker;
using MoonlightServers.Daemon.ServerSystem.FileSystems;
using MoonlightServers.Daemon.ServerSystem.Handlers;
using MoonlightServers.Daemon.ServerSystem.Implementations;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using MoonlightServers.Daemon.ServerSystem.Abstractions;
using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon.ServerSystem;
public class ServerFactory
{
private readonly IServiceProvider ServiceProvider;
private readonly IRuntimeEnvironmentService RuntimeEnvironmentService;
private readonly IInstallEnvironmentService InstallEnvironmentService;
private readonly IRuntimeStorageService RuntimeStorageService;
private readonly IInstallStorageService InstallStorageService;
private readonly ServerConfigurationService ConfigurationService;
private readonly ILoggerFactory LoggerFactory;
public ServerFactory(IServiceProvider serviceProvider)
public ServerFactory(
IRuntimeEnvironmentService runtimeEnvironmentService,
IInstallEnvironmentService installEnvironmentService,
IRuntimeStorageService runtimeStorageService,
IInstallStorageService installStorageService,
ServerConfigurationService configurationService,
ILoggerFactory loggerFactory
)
{
ServiceProvider = serviceProvider;
RuntimeEnvironmentService = runtimeEnvironmentService;
InstallEnvironmentService = installEnvironmentService;
RuntimeStorageService = runtimeStorageService;
InstallStorageService = installStorageService;
ConfigurationService = configurationService;
LoggerFactory = loggerFactory;
}
public async Task<Server> CreateAsync(ServerConfiguration configuration)
public async Task<Server> CreateAsync(string uuid)
{
var scope = ServiceProvider.CreateAsyncScope();
var logger = LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Server({uuid})");
var loggerFactory = scope.ServiceProvider.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger($"Servers.Instance.{configuration.Id}.{nameof(Server)}");
var context = scope.ServiceProvider.GetRequiredService<ServerContext>();
context.Identifier = configuration.Id;
context.Configuration = configuration;
context.ServiceScope = scope;
context.Logger = logger;
// Define all required components
IConsole console;
IFileSystem runtimeFs;
IFileSystem installFs;
IInstallation installation;
IOnlineDetector onlineDetector;
IReporter reporter;
IRestorer restorer;
IRuntime runtime;
IStatistics statistics;
// Resolve the components
console = ActivatorUtilities.CreateInstance<DockerConsole>(scope.ServiceProvider);
reporter = ActivatorUtilities.CreateInstance<ServerReporter>(scope.ServiceProvider);
runtimeFs = ActivatorUtilities.CreateInstance<RawRuntimeFs>(scope.ServiceProvider);
installFs = ActivatorUtilities.CreateInstance<RawInstallationFs>(scope.ServiceProvider);
installation = ActivatorUtilities.CreateInstance<DockerInstallation>(scope.ServiceProvider);
onlineDetector = ActivatorUtilities.CreateInstance<RegexOnlineDetector>(scope.ServiceProvider);
restorer = ActivatorUtilities.CreateInstance<DockerRestorer>(scope.ServiceProvider);
runtime = ActivatorUtilities.CreateInstance<DockerRuntime>(scope.ServiceProvider);
statistics = ActivatorUtilities.CreateInstance<DockerStatistics>(scope.ServiceProvider);
// Resolve handlers
var handlers = new List<IServerStateHandler>();
handlers.Add(ActivatorUtilities.CreateInstance<OnlineDetectionHandler>(scope.ServiceProvider));
handlers.Add(ActivatorUtilities.CreateInstance<StartupHandler>(scope.ServiceProvider));
handlers.Add(ActivatorUtilities.CreateInstance<ShutdownHandler>(scope.ServiceProvider));
handlers.Add(ActivatorUtilities.CreateInstance<InstallationHandler>(scope.ServiceProvider));
handlers.Add(ActivatorUtilities.CreateInstance<DebugHandler>(scope.ServiceProvider));
// Resolve additional components
var components = new List<IServerComponent>();
components.Add(ActivatorUtilities.CreateInstance<ConsoleSignalRComponent>(scope.ServiceProvider));
// TODO: Add a plugin hook for dynamically resolving components and checking if any is unset
// Resolve server from di
var server = new Server(
logger,
context,
// Now all components
console,
runtimeFs,
installFs,
installation,
onlineDetector,
reporter,
restorer,
runtime,
statistics,
// And now all the handlers
handlers,
components
return new Server(
uuid,
RuntimeEnvironmentService,
InstallEnvironmentService,
RuntimeStorageService,
InstallStorageService,
ConfigurationService,
logger
);
context.Server = server;
return server;
}
}

View File

@@ -1,4 +1,4 @@
namespace MoonlightServers.Daemon.ServerSystem.Enums;
namespace MoonlightServers.Daemon.ServerSystem;
public enum ServerState
{

View File

@@ -0,0 +1,3 @@
namespace MoonlightServers.Daemon.ServerSystem;
public record ServerStatistics();

View File

@@ -1,83 +0,0 @@
using Docker.DotNet;
using Docker.DotNet.Models;
using MoonCore.Events;
namespace MoonlightServers.Daemon.Services;
public class DockerEventService : BackgroundService
{
private readonly ILogger<DockerEventService> Logger;
private readonly DockerClient DockerClient;
private readonly EventSource<Message> ContainerSource = new();
private readonly EventSource<Message> ImageSource = new();
private readonly EventSource<Message> NetworkSource = new();
public DockerEventService(
ILogger<DockerEventService> logger,
DockerClient dockerClient
)
{
Logger = logger;
DockerClient = dockerClient;
}
public async ValueTask<IAsyncDisposable> SubscribeContainerAsync(Func<Message, ValueTask> callback)
=> await ContainerSource.SubscribeAsync(callback);
public async ValueTask<IAsyncDisposable> SubscribeImageAsync(Func<Message, ValueTask> callback)
=> await ImageSource.SubscribeAsync(callback);
public async ValueTask<IAsyncDisposable> SubscribeNetworkAsync(Func<Message, ValueTask> callback)
=> await NetworkSource.SubscribeAsync(callback);
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Logger.LogInformation("Starting docker event service");
while (!stoppingToken.IsCancellationRequested)
{
try
{
await DockerClient.System.MonitorEventsAsync(
new ContainerEventsParameters(),
new Progress<Message>(async message =>
{
try
{
switch (message.Type)
{
case "container":
await ContainerSource.InvokeAsync(message);
break;
case "image":
await ImageSource.InvokeAsync(message);
break;
case "network":
await NetworkSource.InvokeAsync(message);
break;
}
}
catch (Exception e)
{
Logger.LogError(e, "An error occured while processing docker event");
}
}),
stoppingToken
);
}
catch (TaskCanceledException)
{
// ignored
}
catch (Exception e)
{
Logger.LogError(e, "An error occured while listening for docker events: {message}", e.Message);
}
}
Logger.LogInformation("Stopping docker event service");
}
}

View File

@@ -1,108 +0,0 @@
using Docker.DotNet;
using Docker.DotNet.Models;
using MoonCore.Attributes;
using MoonlightServers.Daemon.Configuration;
namespace MoonlightServers.Daemon.Services;
[Singleton]
public class DockerImageService
{
private readonly DockerClient DockerClient;
private readonly AppConfiguration Configuration;
private readonly ILogger<DockerImageService> Logger;
private readonly Dictionary<string, TaskCompletionSource> PendingDownloads = new();
public DockerImageService(
DockerClient dockerClient,
ILogger<DockerImageService> logger,
AppConfiguration configuration
)
{
Configuration = configuration;
DockerClient = dockerClient;
Logger = logger;
}
public async Task DownloadAsync(string name, Action<string>? onProgressUpdated = null)
{
// If there is already a download for this image occuring, we want to wait for this to complete instead
// of calling docker to download it again
if (PendingDownloads.TryGetValue(name, out var downloadTaskCompletion))
{
await downloadTaskCompletion.Task;
return;
}
var tsc = new TaskCompletionSource();
PendingDownloads.Add(name, tsc);
try
{
// Figure out if and which credentials to use by checking for the domain
AuthConfig credentials = new();
var domain = GetDomainFromDockerImageName(name);
var configuredCredentials = Configuration.Docker.Credentials.FirstOrDefault(x =>
x.Domain.Equals(domain, StringComparison.InvariantCultureIgnoreCase)
);
// Apply credentials configuration if specified
if (configuredCredentials != null)
{
credentials.Username = configuredCredentials.Username;
credentials.Password = configuredCredentials.Password;
credentials.Email = configuredCredentials.Email;
}
// Now we want to pull the image
await DockerClient.Images.CreateImageAsync(new()
{
FromImage = name
},
credentials,
new Progress<JSONMessage>(async message =>
{
if (message.Progress == null)
return;
var line = $"[{message.ID}] {message.ProgressMessage}";
Logger.LogDebug("{line}", line);
if (onProgressUpdated != null)
onProgressUpdated.Invoke(line);
})
);
tsc.SetResult();
PendingDownloads.Remove(name);
}
catch (Exception e)
{
Logger.LogError("An error occured while download image {name}: {e}", name, e);
tsc.SetException(e);
PendingDownloads.Remove(name);
throw;
}
}
private string GetDomainFromDockerImageName(string name) // Method names are my passion ;)
{
var nameParts = name.Split("/");
// If it has 1 part -> just the image name (e.g., "ubuntu")
// If it has 2 parts -> usually "user/image" (e.g., "library/ubuntu")
// If it has 3 or more -> assume first part is the registry domain
if (nameParts.Length >= 3 ||
(nameParts.Length >= 2 && nameParts[0].Contains('.') || nameParts[0].Contains(':')))
return nameParts[0]; // Registry domain is explicitly specified
return "docker.io"; // Default Docker registry
}
}

View File

@@ -1,52 +0,0 @@
using Docker.DotNet;
using MoonCore.Attributes;
using MoonlightServers.Daemon.Helpers;
using MoonlightServers.Daemon.Models.UnsafeDocker;
namespace MoonlightServers.Daemon.Services;
[Singleton]
public class DockerInfoService
{
private readonly DockerClient DockerClient;
private readonly UnsafeDockerClient UnsafeDockerClient;
public DockerInfoService(DockerClient dockerClient, UnsafeDockerClient unsafeDockerClient)
{
DockerClient = dockerClient;
UnsafeDockerClient = unsafeDockerClient;
}
public async Task<string> GetDockerVersionAsync()
{
var version = await DockerClient.System.GetVersionAsync();
return $"{version.Version} commit {version.GitCommit} ({version.APIVersion})";
}
public async Task<UsageDataReport> GetDataUsageAsync()
{
var response = await UnsafeDockerClient.GetDataUsageAsync();
var report = new UsageDataReport()
{
Containers = new UsageData()
{
Used = response.Containers.Sum(x => x.SizeRw),
Reclaimable = 0
},
Images = new UsageData()
{
Used = response.Images.Sum(x => x.Size),
Reclaimable = response.Images.Where(x => x.Containers == 0).Sum(x => x.Size)
},
BuildCache = new UsageData()
{
Used = response.BuildCache.Sum(x => x.Size),
Reclaimable = response.BuildCache.Where(x => !x.InUse).Sum(x => x.Size)
}
};
return report;
}
}

View File

@@ -1,67 +0,0 @@
using MoonCore.Attributes;
using MoonCore.Helpers;
using MoonCore.Models;
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
namespace MoonlightServers.Daemon.Services;
[Singleton]
public class RemoteService
{
private readonly HttpApiClient ApiClient;
public RemoteService(AppConfiguration configuration)
{
ApiClient = CreateHttpClient(configuration);
}
public async Task GetStatusAsync()
{
await ApiClient.Get("api/remote/servers/node/trip");
}
public async Task<CountedData<ServerDataResponse>> GetServersAsync(int startIndex, int count)
{
return await ApiClient.GetJson<CountedData<ServerDataResponse>>(
$"api/remote/servers?startIndex={startIndex}&count={count}"
);
}
public async Task<ServerDataResponse> GetServerAsync(int serverId)
{
return await ApiClient.GetJson<ServerDataResponse>(
$"api/remote/servers/{serverId}"
);
}
public async Task<ServerInstallDataResponse> GetServerInstallationAsync(int serverId)
{
return await ApiClient.GetJson<ServerInstallDataResponse>(
$"api/remote/servers/{serverId}/install"
);
}
#region Helpers
private HttpApiClient CreateHttpClient(AppConfiguration configuration)
{
var formattedUrl = configuration.Remote.Url.EndsWith('/')
? configuration.Remote.Url
: configuration.Remote.Url + "/";
var httpClient = new HttpClient()
{
BaseAddress = new Uri(formattedUrl)
};
httpClient.DefaultRequestHeaders.Add(
"Authorization",
$"Bearer {configuration.Security.TokenId}.{configuration.Security.Token}"
);
return new HttpApiClient(httpClient);
}
#endregion
}

Some files were not shown because too many files have changed in this diff Show More