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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,50 @@
using MoonlightServers.Daemon.Models;
namespace MoonlightServers.Daemon.Services;
public class ServerConfigurationService
{
public async Task<RuntimeConfiguration> GetRuntimeConfigurationAsync(string uuid)
{
return new RuntimeConfiguration(
new RuntimeLimitsConfig(400, null, 4096, null),
new RuntimeStorageConfig("local", [], 10240),
new RuntimeTemplateConfig(
"ghcr.io/pterodactyl/yolks:java_21",
"java -jar -Xmx{{SERVER_MEMORY}}M server.jar",
"stop",
["Done"]
),
new RuntimeNetworkConfig(
[],
null,
null,
new RuntimePortConfig("0.0.0.0", 25565),
[
new RuntimePortConfig("0.0.0.0", 25565)
]
),
new RuntimeEnvironmentConfig(
new Dictionary<string, string>()
{
{ "testy", "ytset" }
},
new Dictionary<string, string>()
{
{ "SERVER_MEMORY", "4096" },
{"MINECRAFT_VERSION", "latest"},
{"SERVER_JARFILE", "server.jar"}
}
)
);
}
public async Task<InstallConfiguration> GetInstallConfigurationAsync(string uuid)
{
return new InstallConfiguration(
"bash",
"installer",
await File.ReadAllTextAsync("/home/chiara/Documents/daemonScripts/install.sh")
);
}
}

View File

@@ -1,111 +0,0 @@
using System.Collections.Concurrent;
using MoonCore.Models;
using MoonlightServers.Daemon.Mappers;
using MoonlightServers.Daemon.Models.Cache;
using MoonlightServers.Daemon.ServerSystem;
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
namespace MoonlightServers.Daemon.Services;
public class ServerService : IHostedLifecycleService
{
private readonly ConcurrentDictionary<int, Server> Servers = new();
private readonly ILogger<ServerService> Logger;
private readonly ServerFactory ServerFactory;
private readonly RemoteService RemoteService;
private readonly ServerConfigurationMapper ConfigurationMapper;
public ServerService(
ILogger<ServerService> logger,
ServerFactory serverFactory,
RemoteService remoteService,
ServerConfigurationMapper configurationMapper
)
{
Logger = logger;
ServerFactory = serverFactory;
RemoteService = remoteService;
ConfigurationMapper = configurationMapper;
}
public Server? GetById(int id)
=> Servers.GetValueOrDefault(id);
public async Task InitializeAsync(ServerConfiguration configuration)
{
var existingServer = Servers.GetValueOrDefault(configuration.Id);
if (existingServer != null)
{
existingServer.Context.Configuration = configuration;
// TODO: Implement a way for components to get notified
}
else
{
var server = await ServerFactory.CreateAsync(configuration);
Servers[configuration.Id] = server;
await server.InitializeAsync();
}
}
public async Task InitializeByIdAsync(int id)
{
var serverData = await RemoteService.GetServerAsync(id);
var config = ConfigurationMapper.FromServerDataResponse(serverData);
await InitializeAsync(config);
}
private async Task InitializeAllAsync()
{
Logger.LogDebug("Initialing servers from panel");
var servers = await CountedData<ServerDataResponse>.LoadAllAsync(async (startIndex, count) =>
await RemoteService.GetServersAsync(startIndex, count)
);
foreach (var serverData in servers)
{
try
{
var config = ConfigurationMapper.FromServerDataResponse(serverData);
await InitializeAsync(config);
}
catch (Exception e)
{
Logger.LogError(e, "An error occured while initializing server {id}", serverData.Id);
}
}
}
#region Lifetime handlers
public Task StartAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
public async Task StartedAsync(CancellationToken cancellationToken)
{
await InitializeAllAsync();
}
public Task StartingAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
public Task StoppedAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
public async Task StoppingAsync(CancellationToken cancellationToken)
{
Logger.LogDebug("Stopping server service. Disposing servers");
foreach (var server in Servers.Values)
await server.DisposeAsync();
}
#endregion
}