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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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