Recreated plugin with new project template. Started implementing server system daemon
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace MoonlightServers.Daemon.Enums;
|
||||
|
||||
public enum ServerState
|
||||
{
|
||||
Offline = 0,
|
||||
Starting = 1,
|
||||
Online = 2,
|
||||
Stopping = 3,
|
||||
Installing = 4
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
76
MoonlightServers.Daemon/Helpers/AppConsoleFormatter.cs
Normal file
76
MoonlightServers.Daemon/Helpers/AppConsoleFormatter.cs
Normal 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", "")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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('/');
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
||||
namespace MoonlightServers.Daemon.Helpers;
|
||||
|
||||
public class TokenAuthOptions : AuthenticationSchemeOptions
|
||||
{
|
||||
public string Token { get; set; }
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
3
MoonlightServers.Daemon/Models/InstallConfiguration.cs
Normal file
3
MoonlightServers.Daemon/Models/InstallConfiguration.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace MoonlightServers.Daemon.Models;
|
||||
|
||||
public record InstallConfiguration(string Shell, string DockerImage, string Script);
|
||||
@@ -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; }
|
||||
}
|
||||
47
MoonlightServers.Daemon/Models/RuntimeConfiguration.cs
Normal file
47
MoonlightServers.Daemon/Models/RuntimeConfiguration.cs
Normal 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
|
||||
);
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace MoonlightServers.Daemon.Models.UnsafeDocker;
|
||||
|
||||
public class UsageData
|
||||
{
|
||||
public long Used { get; set; }
|
||||
public long Reclaimable { get; set; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
public interface IInstallStorage
|
||||
{
|
||||
public Task<string> GetHostPathAsync();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
public interface IRuntimeStorage
|
||||
{
|
||||
public Task<string> GetHostPathAsync();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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}";
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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[]>([]);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker.Events;
|
||||
|
||||
public record ContainerDieEvent(string ContainerId, int ExitCode);
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
|
||||
public interface IServerComponent : IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes the server component
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task InitializeAsync();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Models;
|
||||
|
||||
public class StatisticsData
|
||||
{
|
||||
|
||||
}
|
||||
37
MoonlightServers.Daemon/ServerSystem/Server.Delete.cs
Normal file
37
MoonlightServers.Daemon/ServerSystem/Server.Delete.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
157
MoonlightServers.Daemon/ServerSystem/Server.Install.cs
Normal file
157
MoonlightServers.Daemon/ServerSystem/Server.Install.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
163
MoonlightServers.Daemon/ServerSystem/Server.Power.cs
Normal file
163
MoonlightServers.Daemon/ServerSystem/Server.Power.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
69
MoonlightServers.Daemon/ServerSystem/Server.Restore.cs
Normal file
69
MoonlightServers.Daemon/ServerSystem/Server.Restore.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Enums;
|
||||
namespace MoonlightServers.Daemon.ServerSystem;
|
||||
|
||||
public enum ServerState
|
||||
{
|
||||
3
MoonlightServers.Daemon/ServerSystem/ServerStatistics.cs
Normal file
3
MoonlightServers.Daemon/ServerSystem/ServerStatistics.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem;
|
||||
|
||||
public record ServerStatistics();
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user