Implemented factory pattern for server abstraction creation. Implemented raw fs and docker provisioner. Implemented docker event service with observer pattern
This commit is contained in:
@@ -44,10 +44,10 @@ public class AppConfiguration
|
|||||||
|
|
||||||
public class StorageData
|
public class StorageData
|
||||||
{
|
{
|
||||||
public string Volumes { get; set; } = PathBuilder.Dir("volumes");
|
public string Volumes { get; set; } = Path.Combine("storage", "volumes");
|
||||||
public string VirtualDisks { get; set; } = PathBuilder.Dir("virtualDisks");
|
public string VirtualDisks { get; set; } = Path.Combine("storage", "virtualDisks");
|
||||||
public string Backups { get; set; } = PathBuilder.Dir("backups");
|
public string Backups { get; set; } = Path.Combine("storage", "backups");
|
||||||
public string Install { get; set; } = PathBuilder.Dir("install");
|
public string Install { get; set; } =Path.Combine("storage", "install");
|
||||||
|
|
||||||
public VirtualDiskData VirtualDiskOptions { get; set; } = new();
|
public VirtualDiskData VirtualDiskOptions { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|||||||
24
MoonlightServers.Daemon/Helpers/CompositeServiceProvider.cs
Normal file
24
MoonlightServers.Daemon/Helpers/CompositeServiceProvider.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
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)
|
||||||
|
{
|
||||||
|
var service = provider.GetService(serviceType);
|
||||||
|
|
||||||
|
if (service != null)
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
284
MoonlightServers.Daemon/Mappers/ServerConfigurationMapper.cs
Normal file
284
MoonlightServers.Daemon/Mappers/ServerConfigurationMapper.cs
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
using Docker.DotNet.Models;
|
||||||
|
using Mono.Unix.Native;
|
||||||
|
using MoonCore.Helpers;
|
||||||
|
using MoonlightServers.Daemon.Configuration;
|
||||||
|
using MoonlightServers.Daemon.Models.Cache;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Daemon.Mappers;
|
||||||
|
|
||||||
|
public class ServerConfigurationMapper
|
||||||
|
{
|
||||||
|
private readonly AppConfiguration AppConfiguration;
|
||||||
|
|
||||||
|
public ServerConfigurationMapper(AppConfiguration appConfiguration)
|
||||||
|
{
|
||||||
|
AppConfiguration = appConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ public interface IConsole : IServerComponent
|
|||||||
|
|
||||||
public Task WriteToOutput(string content);
|
public Task WriteToOutput(string content);
|
||||||
public Task WriteToInput(string content);
|
public Task WriteToInput(string content);
|
||||||
|
public Task WriteToMoonlight(string content);
|
||||||
|
|
||||||
public Task ClearOutput();
|
public Task ClearOutput();
|
||||||
public string[] GetOutput();
|
public string[] GetOutput();
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ public interface IProvisioner : IServerComponent
|
|||||||
{
|
{
|
||||||
public IAsyncObservable<object> OnExited { get; set; }
|
public IAsyncObservable<object> OnExited { get; set; }
|
||||||
|
|
||||||
|
public Task Provision();
|
||||||
public Task Start();
|
public Task Start();
|
||||||
public Task Stop();
|
public Task Stop();
|
||||||
public Task Kill();
|
public Task Kill();
|
||||||
public Task Cleanup();
|
public Task Deprovision();
|
||||||
|
|
||||||
public Task<ServerCrash?> SearchForCrash();
|
public Task<ServerCrash?> SearchForCrash();
|
||||||
}
|
}
|
||||||
@@ -119,8 +119,59 @@ public class Server : IAsyncDisposable
|
|||||||
|
|
||||||
// Handle transitions
|
// Handle transitions
|
||||||
|
|
||||||
|
StateMachine.Configure(ServerState.Starting)
|
||||||
|
.OnActivateAsync(HandleStart);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region State machine handlers
|
||||||
|
|
||||||
|
private async Task HandleStart()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Plan for starting the server:
|
||||||
|
// 1. Fetch latest configuration from panel (maybe: and perform sync)
|
||||||
|
// 2. Ensure that the file system exists
|
||||||
|
// 3. Mount the file system
|
||||||
|
// 4. Provision the container
|
||||||
|
// 5. Attach console to container
|
||||||
|
// 6. Start the container
|
||||||
|
|
||||||
|
// 1. Fetch latest configuration from panel
|
||||||
|
// TODO: Implement
|
||||||
|
|
||||||
|
// 2. Ensure that the file system exists
|
||||||
|
if (!FileSystem.Exists)
|
||||||
|
{
|
||||||
|
await Console.WriteToMoonlight("Creating storage");
|
||||||
|
await FileSystem.Create();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Mount the file system
|
||||||
|
if (!FileSystem.IsMounted)
|
||||||
|
{
|
||||||
|
await Console.WriteToMoonlight("Mounting storage");
|
||||||
|
await FileSystem.Mount();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Provision the container
|
||||||
|
await Console.WriteToMoonlight("Provisioning runtime");
|
||||||
|
await Provisioner.Provision();
|
||||||
|
|
||||||
|
// 5. Attach console to container
|
||||||
|
await Console.AttachToRuntime();
|
||||||
|
|
||||||
|
// 6. Start the container
|
||||||
|
await Provisioner.Start();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError(e, "An error occured while starting the server");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
if (ProvisionExitSubscription != null)
|
if (ProvisionExitSubscription != null)
|
||||||
|
|||||||
10
MoonlightServers.Daemon/ServerSys/Abstractions/ServerMeta.cs
Normal file
10
MoonlightServers.Daemon/ServerSys/Abstractions/ServerMeta.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using MoonlightServers.Daemon.Models.Cache;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Daemon.ServerSys.Abstractions;
|
||||||
|
|
||||||
|
public record ServerMeta
|
||||||
|
{
|
||||||
|
public ServerConfiguration Configuration { get; set; }
|
||||||
|
public IServiceCollection ServiceCollection { get; set; }
|
||||||
|
public IServiceProvider ServiceProvider { get; set; }
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
using System.Reactive.Subjects;
|
using System.Reactive.Subjects;
|
||||||
|
using System.Text;
|
||||||
using Docker.DotNet;
|
using Docker.DotNet;
|
||||||
|
using Docker.DotNet.Models;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
using MoonlightServers.Daemon.ServerSys.Abstractions;
|
using MoonlightServers.Daemon.ServerSys.Abstractions;
|
||||||
|
|
||||||
@@ -15,12 +18,18 @@ public class DockerConsole : IConsole
|
|||||||
private readonly AsyncSubject<string> OnInputSubject = new();
|
private readonly AsyncSubject<string> OnInputSubject = new();
|
||||||
|
|
||||||
private readonly ConcurrentList<string> OutputCache = new();
|
private readonly ConcurrentList<string> OutputCache = new();
|
||||||
|
|
||||||
private readonly DockerClient DockerClient;
|
private readonly DockerClient DockerClient;
|
||||||
private readonly ILogger<DockerConsole> Logger;
|
private readonly ILogger<DockerConsole> Logger;
|
||||||
|
private readonly ServerMeta Meta;
|
||||||
|
|
||||||
private MultiplexedStream? CurrentStream;
|
private MultiplexedStream? CurrentStream;
|
||||||
private CancellationTokenSource Cts = new();
|
private CancellationTokenSource Cts = new();
|
||||||
|
|
||||||
|
public DockerConsole(ServerMeta meta)
|
||||||
|
{
|
||||||
|
Meta = meta;
|
||||||
|
}
|
||||||
|
|
||||||
public Task Initialize()
|
public Task Initialize()
|
||||||
=> Task.CompletedTask;
|
=> Task.CompletedTask;
|
||||||
|
|
||||||
@@ -29,12 +38,101 @@ public class DockerConsole : IConsole
|
|||||||
|
|
||||||
public async Task AttachToRuntime()
|
public async Task AttachToRuntime()
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
var containerName = $"moonlight-runtime-{Meta.Configuration.Id}";
|
||||||
|
await AttachStream(containerName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AttachToInstallation()
|
public async Task AttachToInstallation()
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
var containerName = $"moonlight-install-{Meta.Configuration.Id}";
|
||||||
|
await AttachStream(containerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task AttachStream(string containerName)
|
||||||
|
{
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
// This loop is here to reconnect to the container if for some reason the container
|
||||||
|
// attach stream fails before the server tasks have been canceled i.e. the before the server
|
||||||
|
// goes offline
|
||||||
|
|
||||||
|
while (!Cts.Token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CurrentStream = await DockerClient.Containers.AttachContainerAsync(
|
||||||
|
containerName,
|
||||||
|
true,
|
||||||
|
new ContainerAttachParameters()
|
||||||
|
{
|
||||||
|
Stderr = true,
|
||||||
|
Stdin = true,
|
||||||
|
Stdout = true,
|
||||||
|
Stream = true
|
||||||
|
},
|
||||||
|
Cts.Token
|
||||||
|
);
|
||||||
|
|
||||||
|
var buffer = new byte[1024];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Read while server tasks are not canceled
|
||||||
|
while (!Cts.Token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var readResult = await CurrentStream.ReadOutputAsync(
|
||||||
|
buffer,
|
||||||
|
0,
|
||||||
|
buffer.Length,
|
||||||
|
Cts.Token
|
||||||
|
);
|
||||||
|
|
||||||
|
if (readResult.EOF)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var resizedBuffer = new byte[readResult.Count];
|
||||||
|
Array.Copy(buffer, resizedBuffer, readResult.Count);
|
||||||
|
buffer = new byte[buffer.Length];
|
||||||
|
|
||||||
|
var decodedText = Encoding.UTF8.GetString(resizedBuffer);
|
||||||
|
await WriteToOutput(decodedText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
// Ignored
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Ignored
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("An unhandled error occured while reading from container stream: {e}", e);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CurrentStream.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError("An error occured while attaching to container: {e}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Reset stream so no further inputs will be piped to it
|
||||||
|
CurrentStream = null;
|
||||||
|
|
||||||
|
Logger.LogDebug("Disconnected from container stream");
|
||||||
|
}, Cts.Token);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task WriteToOutput(string content)
|
public Task WriteToOutput(string content)
|
||||||
@@ -56,9 +154,22 @@ public class DockerConsole : IConsole
|
|||||||
|
|
||||||
public async Task WriteToInput(string content)
|
public async Task WriteToInput(string content)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
if (CurrentStream == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var contentBuffer = Encoding.UTF8.GetBytes(content);
|
||||||
|
|
||||||
|
await CurrentStream.WriteAsync(
|
||||||
|
contentBuffer,
|
||||||
|
0,
|
||||||
|
contentBuffer.Length,
|
||||||
|
Cts.Token
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task WriteToMoonlight(string content)
|
||||||
|
=> await WriteToOutput($"\x1b[0;38;2;255;255;255;48;2;124;28;230m Moonlight \x1b[0m\x1b[38;5;250m\x1b[3m {content}\x1b[0m\n\r");
|
||||||
|
|
||||||
public Task ClearOutput()
|
public Task ClearOutput()
|
||||||
{
|
{
|
||||||
OutputCache.Clear();
|
OutputCache.Clear();
|
||||||
|
|||||||
@@ -0,0 +1,246 @@
|
|||||||
|
using System.Reactive.Subjects;
|
||||||
|
using Docker.DotNet;
|
||||||
|
using Docker.DotNet.Models;
|
||||||
|
using MoonlightServers.Daemon.Mappers;
|
||||||
|
using MoonlightServers.Daemon.ServerSys.Abstractions;
|
||||||
|
using MoonlightServers.Daemon.Services;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Daemon.ServerSys.Implementations;
|
||||||
|
|
||||||
|
public class DockerProvisioner : IProvisioner
|
||||||
|
{
|
||||||
|
public IAsyncObservable<object> OnExited { get; set; }
|
||||||
|
|
||||||
|
private readonly DockerClient DockerClient;
|
||||||
|
private readonly ILogger<DockerProvisioner> Logger;
|
||||||
|
private readonly DockerEventService EventService;
|
||||||
|
private readonly ServerMeta Meta;
|
||||||
|
private readonly IConsole Console;
|
||||||
|
private readonly DockerImageService ImageService;
|
||||||
|
private readonly ServerConfigurationMapper Mapper;
|
||||||
|
private readonly IFileSystem FileSystem;
|
||||||
|
|
||||||
|
private AsyncSubject<object> OnExitedSubject = new();
|
||||||
|
|
||||||
|
private string? ContainerId;
|
||||||
|
private string ContainerName;
|
||||||
|
private IAsyncDisposable? ContainerEventSubscription;
|
||||||
|
|
||||||
|
public DockerProvisioner(
|
||||||
|
DockerClient dockerClient,
|
||||||
|
ILogger<DockerProvisioner> logger,
|
||||||
|
DockerEventService eventService,
|
||||||
|
ServerMeta meta,
|
||||||
|
IConsole console,
|
||||||
|
DockerImageService imageService,
|
||||||
|
ServerConfigurationMapper mapper,
|
||||||
|
IFileSystem fileSystem
|
||||||
|
)
|
||||||
|
{
|
||||||
|
DockerClient = dockerClient;
|
||||||
|
Logger = logger;
|
||||||
|
EventService = eventService;
|
||||||
|
Meta = meta;
|
||||||
|
Console = console;
|
||||||
|
ImageService = imageService;
|
||||||
|
Mapper = mapper;
|
||||||
|
FileSystem = fileSystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Initialize()
|
||||||
|
{
|
||||||
|
ContainerName = $"moonlight-runtime-{Meta.Configuration.Id}";
|
||||||
|
|
||||||
|
ContainerEventSubscription = await EventService
|
||||||
|
.OnContainerEvent
|
||||||
|
.SubscribeAsync(HandleContainerEvent);
|
||||||
|
|
||||||
|
// Check for any already existing runtime container
|
||||||
|
// TODO: Implement a way for restoring the state
|
||||||
|
// Needs to be able to be consumed by the restorer
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValueTask HandleContainerEvent(Message message)
|
||||||
|
{
|
||||||
|
// Only handle events for our own container
|
||||||
|
if (message.ID != ContainerId)
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
|
||||||
|
// Only handle die events
|
||||||
|
if (message.Action != "die")
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
|
||||||
|
OnExitedSubject.OnNext(message);
|
||||||
|
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Sync()
|
||||||
|
{
|
||||||
|
return Task.CompletedTask; // TODO: Implement
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Provision()
|
||||||
|
{
|
||||||
|
// Plan of action:
|
||||||
|
// 1. Ensure no other container with that name exist
|
||||||
|
// 2. Ensure the docker image has been downloaded
|
||||||
|
// 3. Create the container from the configuration in the meta
|
||||||
|
|
||||||
|
// 1. Ensure no other container with that name exist
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Searching for orphan container");
|
||||||
|
|
||||||
|
var possibleContainer = await DockerClient.Containers.InspectContainerAsync(ContainerName);
|
||||||
|
|
||||||
|
Logger.LogDebug("Orphan container found. Removing it");
|
||||||
|
await Console.WriteToMoonlight("Found orphan container. Removing it");
|
||||||
|
|
||||||
|
await EnsureContainerOffline(possibleContainer);
|
||||||
|
|
||||||
|
Logger.LogInformation("Removing orphan container");
|
||||||
|
await DockerClient.Containers.RemoveContainerAsync(ContainerName, new());
|
||||||
|
}
|
||||||
|
catch (DockerContainerNotFoundException)
|
||||||
|
{
|
||||||
|
// Ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Ensure the docker image has been downloaded
|
||||||
|
await Console.WriteToMoonlight("Downloading docker image");
|
||||||
|
|
||||||
|
await ImageService.Download(Meta.Configuration.DockerImage, async message =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Console.WriteToMoonlight(message);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Ignored. Not handling it here could cause an application wide crash afaik
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Create the container from the configuration in the meta
|
||||||
|
var hostFsPath = FileSystem.GetExternalPath();
|
||||||
|
|
||||||
|
var parameters = Mapper.ToRuntimeParameters(
|
||||||
|
Meta.Configuration,
|
||||||
|
hostFsPath,
|
||||||
|
ContainerName
|
||||||
|
);
|
||||||
|
|
||||||
|
var createdContainer = await DockerClient.Containers.CreateContainerAsync(parameters);
|
||||||
|
|
||||||
|
ContainerId = createdContainer.ID;
|
||||||
|
|
||||||
|
Logger.LogInformation("Created container");
|
||||||
|
await Console.WriteToMoonlight("Created container");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Start()
|
||||||
|
{
|
||||||
|
if(string.IsNullOrEmpty(ContainerId))
|
||||||
|
throw new ArgumentNullException(nameof(ContainerId), "Container id of runtime is unknown");
|
||||||
|
|
||||||
|
await Console.WriteToMoonlight("Starting container");
|
||||||
|
await DockerClient.Containers.StartContainerAsync(ContainerId, new());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Stop()
|
||||||
|
{
|
||||||
|
if (Meta.Configuration.StopCommand.StartsWith('^'))
|
||||||
|
{
|
||||||
|
await DockerClient.Containers.KillContainerAsync(ContainerId, new()
|
||||||
|
{
|
||||||
|
Signal = Meta.Configuration.StopCommand.Substring(1)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
await Console.WriteToInput(Meta.Configuration.StopCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Kill()
|
||||||
|
{
|
||||||
|
await EnsureContainerOffline();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Deprovision()
|
||||||
|
{
|
||||||
|
// Plan of action:
|
||||||
|
// 1. Search for the container by id or name
|
||||||
|
// 2. Ensure container is offline
|
||||||
|
// 3. Remove the container
|
||||||
|
|
||||||
|
// 1. Search for the container by id or name
|
||||||
|
ContainerInspectResponse? container = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(ContainerId))
|
||||||
|
container = await DockerClient.Containers.InspectContainerAsync(ContainerName);
|
||||||
|
else
|
||||||
|
container = await DockerClient.Containers.InspectContainerAsync(ContainerId);
|
||||||
|
}
|
||||||
|
catch (DockerContainerNotFoundException)
|
||||||
|
{
|
||||||
|
// Ignored
|
||||||
|
|
||||||
|
Logger.LogDebug("Runtime container could not be found. Reporting deprovision success");
|
||||||
|
}
|
||||||
|
|
||||||
|
// No container found? We are done here then
|
||||||
|
if (container == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// 2. Ensure container is offline
|
||||||
|
await EnsureContainerOffline(container);
|
||||||
|
|
||||||
|
// 3. Remove the container
|
||||||
|
Logger.LogInformation("Removing container");
|
||||||
|
await Console.WriteToMoonlight("Removing container");
|
||||||
|
|
||||||
|
await DockerClient.Containers.RemoveContainerAsync(container.ID, new());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureContainerOffline(ContainerInspectResponse? container = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(ContainerId))
|
||||||
|
container = await DockerClient.Containers.InspectContainerAsync(ContainerName);
|
||||||
|
else
|
||||||
|
container = await DockerClient.Containers.InspectContainerAsync(ContainerId);
|
||||||
|
}
|
||||||
|
catch (DockerContainerNotFoundException)
|
||||||
|
{
|
||||||
|
// Ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
// No container found? We are done here then
|
||||||
|
if (container == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Check if container is running
|
||||||
|
if (!container.State.Running)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await Console.WriteToMoonlight("Killing container");
|
||||||
|
await DockerClient.Containers.KillContainerAsync(ContainerId, new());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ServerCrash?> SearchForCrash()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
OnExitedSubject.Dispose();
|
||||||
|
|
||||||
|
if (ContainerEventSubscription != null)
|
||||||
|
await ContainerEventSubscription.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using MoonlightServers.Daemon.Configuration;
|
||||||
|
using MoonlightServers.Daemon.ServerSys.Abstractions;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Daemon.ServerSys.Implementations;
|
||||||
|
|
||||||
|
public class RawFileSystem : IFileSystem
|
||||||
|
{
|
||||||
|
public bool IsMounted { get; private set; }
|
||||||
|
public bool Exists { get; private set; }
|
||||||
|
|
||||||
|
private readonly ServerMeta Meta;
|
||||||
|
private readonly AppConfiguration Configuration;
|
||||||
|
private string HostPath => Path.Combine(Configuration.Storage.Volumes, Meta.Configuration.Id.ToString());
|
||||||
|
|
||||||
|
public RawFileSystem(ServerMeta meta, AppConfiguration configuration)
|
||||||
|
{
|
||||||
|
Meta = meta;
|
||||||
|
Configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Initialize()
|
||||||
|
=> Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task Sync()
|
||||||
|
=> Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task Create()
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Mount()
|
||||||
|
{
|
||||||
|
IsMounted = true;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Unmount()
|
||||||
|
{
|
||||||
|
IsMounted = false;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Delete()
|
||||||
|
{
|
||||||
|
Directory.Delete(HostPath, true);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetExternalPath()
|
||||||
|
=> HostPath;
|
||||||
|
|
||||||
|
public ValueTask DisposeAsync()
|
||||||
|
=> ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
39
MoonlightServers.Daemon/ServerSys/ServerFactory.cs
Normal file
39
MoonlightServers.Daemon/ServerSys/ServerFactory.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using MoonlightServers.Daemon.Helpers;
|
||||||
|
using MoonlightServers.Daemon.Models.Cache;
|
||||||
|
using MoonlightServers.Daemon.ServerSys.Abstractions;
|
||||||
|
using MoonlightServers.Daemon.ServerSys.Implementations;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Daemon.ServerSys;
|
||||||
|
|
||||||
|
public class ServerFactory
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider ServiceProvider;
|
||||||
|
|
||||||
|
public ServerFactory(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
ServiceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Server CreateServer(ServerConfiguration configuration)
|
||||||
|
{
|
||||||
|
var serverMeta = new ServerMeta();
|
||||||
|
serverMeta.Configuration = configuration;
|
||||||
|
|
||||||
|
// Build service collection
|
||||||
|
serverMeta.ServiceCollection = new ServiceCollection();
|
||||||
|
|
||||||
|
// Configure service pipeline for the server components
|
||||||
|
serverMeta.ServiceCollection.AddSingleton<IConsole, DockerConsole>();
|
||||||
|
serverMeta.ServiceCollection.AddSingleton<IFileSystem, RawFileSystem>();
|
||||||
|
|
||||||
|
// TODO: Handle implementation configurations (e.g. virtual disk) here
|
||||||
|
|
||||||
|
// Combine both app service provider and our server instance specific one
|
||||||
|
serverMeta.ServiceProvider = new CompositeServiceProvider([
|
||||||
|
serverMeta.ServiceCollection.BuildServiceProvider(),
|
||||||
|
ServiceProvider
|
||||||
|
]);
|
||||||
|
|
||||||
|
return serverMeta.ServiceProvider.GetRequiredService<Server>();
|
||||||
|
}
|
||||||
|
}
|
||||||
81
MoonlightServers.Daemon/Services/DockerEventService.cs
Normal file
81
MoonlightServers.Daemon/Services/DockerEventService.cs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
using System.Reactive.Linq;
|
||||||
|
using System.Reactive.Subjects;
|
||||||
|
using Docker.DotNet;
|
||||||
|
using Docker.DotNet.Models;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Daemon.Services;
|
||||||
|
|
||||||
|
public class DockerEventService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly ILogger<DockerEventService> Logger;
|
||||||
|
private readonly DockerClient DockerClient;
|
||||||
|
|
||||||
|
public IAsyncObservable<Message> OnContainerEvent => OnContainerSubject.ToAsyncObservable();
|
||||||
|
public IAsyncObservable<Message> OnImageEvent => OnImageSubject.ToAsyncObservable();
|
||||||
|
public IAsyncObservable<Message> OnNetworkEvent => OnNetworkSubject.ToAsyncObservable();
|
||||||
|
|
||||||
|
private readonly AsyncSubject<Message> OnContainerSubject = new();
|
||||||
|
private readonly AsyncSubject<Message> OnImageSubject = new();
|
||||||
|
private readonly AsyncSubject<Message> OnNetworkSubject = new();
|
||||||
|
|
||||||
|
public DockerEventService(
|
||||||
|
ILogger<DockerEventService> logger,
|
||||||
|
DockerClient dockerClient
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Logger = logger;
|
||||||
|
DockerClient = dockerClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>(message =>
|
||||||
|
{
|
||||||
|
switch (message.Type)
|
||||||
|
{
|
||||||
|
case "container":
|
||||||
|
OnContainerSubject.OnNext(message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "image":
|
||||||
|
OnImageSubject.OnNext(message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "network":
|
||||||
|
OnNetworkSubject.OnNext(message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
base.Dispose();
|
||||||
|
|
||||||
|
OnContainerSubject.Dispose();
|
||||||
|
OnImageSubject.Dispose();
|
||||||
|
OnNetworkSubject.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,9 +8,12 @@ using MoonCore.EnvConfiguration;
|
|||||||
using MoonCore.Extended.Extensions;
|
using MoonCore.Extended.Extensions;
|
||||||
using MoonCore.Extensions;
|
using MoonCore.Extensions;
|
||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
|
using MoonCore.Logging;
|
||||||
using MoonlightServers.Daemon.Configuration;
|
using MoonlightServers.Daemon.Configuration;
|
||||||
using MoonlightServers.Daemon.Helpers;
|
using MoonlightServers.Daemon.Helpers;
|
||||||
using MoonlightServers.Daemon.Http.Hubs;
|
using MoonlightServers.Daemon.Http.Hubs;
|
||||||
|
using MoonlightServers.Daemon.Mappers;
|
||||||
|
using MoonlightServers.Daemon.ServerSys;
|
||||||
using MoonlightServers.Daemon.ServerSystem;
|
using MoonlightServers.Daemon.ServerSystem;
|
||||||
using MoonlightServers.Daemon.Services;
|
using MoonlightServers.Daemon.Services;
|
||||||
|
|
||||||
@@ -68,8 +71,8 @@ public class Startup
|
|||||||
private Task SetupStorage()
|
private Task SetupStorage()
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory("storage");
|
Directory.CreateDirectory("storage");
|
||||||
Directory.CreateDirectory(PathBuilder.Dir("storage", "logs"));
|
Directory.CreateDirectory(Path.Combine("storage", "logs"));
|
||||||
Directory.CreateDirectory(PathBuilder.Dir("storage", "plugins"));
|
Directory.CreateDirectory(Path.Combine("storage", "plugins"));
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
@@ -100,7 +103,7 @@ public class Startup
|
|||||||
private Task UseBase()
|
private Task UseBase()
|
||||||
{
|
{
|
||||||
WebApplication.UseRouting();
|
WebApplication.UseRouting();
|
||||||
WebApplication.UseApiExceptionHandler();
|
WebApplication.UseExceptionHandler();
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
@@ -141,7 +144,7 @@ public class Startup
|
|||||||
var configurationBuilder = new ConfigurationBuilder();
|
var configurationBuilder = new ConfigurationBuilder();
|
||||||
|
|
||||||
// Ensure configuration file exists
|
// Ensure configuration file exists
|
||||||
var jsonFilePath = PathBuilder.File(Directory.GetCurrentDirectory(), "storage", "app.json");
|
var jsonFilePath = Path.Combine(Directory.GetCurrentDirectory(), "storage", "app.json");
|
||||||
|
|
||||||
if (!File.Exists(jsonFilePath))
|
if (!File.Exists(jsonFilePath))
|
||||||
await File.WriteAllTextAsync(jsonFilePath, JsonSerializer.Serialize(new AppConfiguration()));
|
await File.WriteAllTextAsync(jsonFilePath, JsonSerializer.Serialize(new AppConfiguration()));
|
||||||
@@ -187,20 +190,8 @@ public class Startup
|
|||||||
|
|
||||||
private Task SetupLogging()
|
private Task SetupLogging()
|
||||||
{
|
{
|
||||||
LoggerProviders = LoggerBuildHelper.BuildFromConfiguration(configuration =>
|
|
||||||
{
|
|
||||||
configuration.Console.Enable = true;
|
|
||||||
configuration.Console.EnableAnsiMode = true;
|
|
||||||
|
|
||||||
configuration.FileLogging.Enable = true;
|
|
||||||
configuration.FileLogging.EnableLogRotation = true;
|
|
||||||
configuration.FileLogging.Path = PathBuilder.File("storage", "logs", "WebAppTemplate.log");
|
|
||||||
configuration.FileLogging.RotateLogNameTemplate =
|
|
||||||
PathBuilder.File("storage", "logs", "WebAppTemplate.log.{0}");
|
|
||||||
});
|
|
||||||
|
|
||||||
LoggerFactory = new LoggerFactory();
|
LoggerFactory = new LoggerFactory();
|
||||||
LoggerFactory.AddProviders(LoggerProviders);
|
LoggerFactory.AddAnsiConsole();
|
||||||
|
|
||||||
Logger = LoggerFactory.CreateLogger<Startup>();
|
Logger = LoggerFactory.CreateLogger<Startup>();
|
||||||
|
|
||||||
@@ -211,30 +202,35 @@ public class Startup
|
|||||||
{
|
{
|
||||||
// Configure application logging
|
// Configure application logging
|
||||||
WebApplicationBuilder.Logging.ClearProviders();
|
WebApplicationBuilder.Logging.ClearProviders();
|
||||||
WebApplicationBuilder.Logging.AddProviders(LoggerProviders);
|
WebApplicationBuilder.Logging.AddAnsiConsole();
|
||||||
|
WebApplicationBuilder.Logging.AddFile(
|
||||||
|
Path.Combine(Directory.GetCurrentDirectory(), "storage", "logs", "MoonlightServer.Daemon.log")
|
||||||
|
);
|
||||||
|
|
||||||
// Logging levels
|
// Logging levels
|
||||||
var logConfigPath = PathBuilder.File("storage", "logConfig.json");
|
var logConfigPath = Path.Combine("storage", "logConfig.json");
|
||||||
|
|
||||||
// Ensure logging config, add a default one is missing
|
// Ensure logging config, add a default one is missing
|
||||||
if (!File.Exists(logConfigPath))
|
if (!File.Exists(logConfigPath))
|
||||||
{
|
{
|
||||||
var logLevels = new Dictionary<string, string>
|
var defaultLogLevels = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
{ "Default", "Information" },
|
{ "Default", "Information" },
|
||||||
{ "Microsoft.AspNetCore", "Warning" },
|
{ "Microsoft.AspNetCore", "Warning" },
|
||||||
{ "System.Net.Http.HttpClient", "Warning" }
|
{ "System.Net.Http.HttpClient", "Warning" }
|
||||||
};
|
};
|
||||||
|
|
||||||
var logLevelsJson = JsonSerializer.Serialize(logLevels);
|
var json = JsonSerializer.Serialize(defaultLogLevels);
|
||||||
var logConfig = "{\"LogLevel\":" + logLevelsJson + "}";
|
await File.WriteAllTextAsync(logConfigPath, json);
|
||||||
await File.WriteAllTextAsync(logConfigPath, logConfig);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure logging configuration
|
var logLevels = JsonSerializer.Deserialize<Dictionary<string, string>>(
|
||||||
WebApplicationBuilder.Logging.AddConfiguration(
|
|
||||||
await File.ReadAllTextAsync(logConfigPath)
|
await File.ReadAllTextAsync(logConfigPath)
|
||||||
);
|
)!;
|
||||||
|
|
||||||
|
// Configure logging configuration
|
||||||
|
foreach (var logLevel in logLevels)
|
||||||
|
WebApplicationBuilder.Logging.AddFilter(logLevel.Key, Enum.Parse<LogLevel>(logLevel.Value));
|
||||||
|
|
||||||
// Mute exception handler middleware
|
// Mute exception handler middleware
|
||||||
// https://github.com/dotnet/aspnetcore/issues/19740
|
// https://github.com/dotnet/aspnetcore/issues/19740
|
||||||
@@ -255,10 +251,16 @@ public class Startup
|
|||||||
|
|
||||||
private Task RegisterServers()
|
private Task RegisterServers()
|
||||||
{
|
{
|
||||||
WebApplicationBuilder.Services.AddHostedService(
|
WebApplicationBuilder.Services.AddHostedService(sp => sp.GetRequiredService<ServerService>()
|
||||||
sp => sp.GetRequiredService<ServerService>()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
WebApplicationBuilder.Services.AddSingleton<DockerEventService>();
|
||||||
|
WebApplicationBuilder.Services.AddHostedService(sp => sp.GetRequiredService<DockerEventService>());
|
||||||
|
|
||||||
|
WebApplicationBuilder.Services.AddSingleton<ServerConfigurationMapper>();
|
||||||
|
|
||||||
|
WebApplicationBuilder.Services.AddSingleton<ServerFactory>();
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user