From 425bae360b1ba85ebbc82e596bc6c297fcb853c9 Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Mon, 26 May 2025 22:19:53 -0500 Subject: [PATCH] Abstract server state to support other engines, add Docker container stats --- LANCommander.SDK/ProcessExecutionContext.cs | 2 + .../Abstractions/IServerEngine.cs | 2 +- .../Abstractions/IServerState.cs | 13 +++++ .../Models/ServerDockerContainerState.cs | 17 +++++++ .../Models/ServerProcessState.cs | 6 ++- .../ServerEngines/DockerServerEngine.cs | 49 ++++++++++++++----- .../ServerEngines/LocalServerEngine.cs | 4 +- 7 files changed, 77 insertions(+), 16 deletions(-) create mode 100644 LANCommander.Server.Services/Abstractions/IServerState.cs create mode 100644 LANCommander.Server.Services/Models/ServerDockerContainerState.cs diff --git a/LANCommander.SDK/ProcessExecutionContext.cs b/LANCommander.SDK/ProcessExecutionContext.cs index 747c9ae1..d0ca9bbd 100644 --- a/LANCommander.SDK/ProcessExecutionContext.cs +++ b/LANCommander.SDK/ProcessExecutionContext.cs @@ -159,6 +159,8 @@ namespace LANCommander.SDK await terminator.WaitForExitAsync(); } } + + Process = null; } public async Task ExecuteGameActionAsync(string installDirectory, Guid gameId, Models.Action action, string args = "", CancellationToken cancellationToken = default) diff --git a/LANCommander.Server.Services/Abstractions/IServerEngine.cs b/LANCommander.Server.Services/Abstractions/IServerEngine.cs index f234d4d2..8eb4f916 100644 --- a/LANCommander.Server.Services/Abstractions/IServerEngine.cs +++ b/LANCommander.Server.Services/Abstractions/IServerEngine.cs @@ -9,7 +9,7 @@ public interface IServerEngine public bool IsManaging(Guid serverId); public Task StartAsync(Guid serverId); public Task StopAsync(Guid serverId); - public Task GetStateAsync(Guid serverId); + public Task GetStateAsync(Guid serverId); public event EventHandler OnServerStatusUpdate; public event EventHandler OnServerLog; } \ No newline at end of file diff --git a/LANCommander.Server.Services/Abstractions/IServerState.cs b/LANCommander.Server.Services/Abstractions/IServerState.cs new file mode 100644 index 00000000..55564738 --- /dev/null +++ b/LANCommander.Server.Services/Abstractions/IServerState.cs @@ -0,0 +1,13 @@ +using System.Diagnostics; +using LANCommander.Server.Services.Enums; + +namespace LANCommander.Server.Services.Abstractions; + +public interface IServerState +{ + public ServerProcessStatus Status { get; set; } + public ulong MemoryUsage { get; set; } + public ulong TotalMemory { get; set; } + public double ProcessorLoad { get; set; } + public CancellationTokenSource CancellationToken { get; set; } +} \ No newline at end of file diff --git a/LANCommander.Server.Services/Models/ServerDockerContainerState.cs b/LANCommander.Server.Services/Models/ServerDockerContainerState.cs new file mode 100644 index 00000000..d9be73dd --- /dev/null +++ b/LANCommander.Server.Services/Models/ServerDockerContainerState.cs @@ -0,0 +1,17 @@ +using System.Diagnostics; +using Docker.DotNet; +using LANCommander.Server.Services.Abstractions; +using LANCommander.Server.Services.Enums; + +namespace LANCommander.Server.Services.Models; + +public class ServerDockerContainerState : IServerState +{ + public ServerProcessStatus Status { get; set; } = ServerProcessStatus.Stopped; + public ulong MemoryUsage { get; set; } + public ulong TotalMemory { get; set; } + public double ProcessorLoad { get; set; } + public DockerClient Client { get; set; } + public DockerContainer Container { get; set; } + public CancellationTokenSource CancellationToken { get; set; } = new(); +} \ No newline at end of file diff --git a/LANCommander.Server.Services/Models/ServerProcessState.cs b/LANCommander.Server.Services/Models/ServerProcessState.cs index 6a1c83ac..88d78c7c 100644 --- a/LANCommander.Server.Services/Models/ServerProcessState.cs +++ b/LANCommander.Server.Services/Models/ServerProcessState.cs @@ -1,12 +1,14 @@ using System.Diagnostics; +using LANCommander.Server.Services.Abstractions; using LANCommander.Server.Services.Enums; namespace LANCommander.Server.Services.Models; -public class ServerProcessState +public class ServerProcessState : IServerState { public ServerProcessStatus Status { get; set; } = ServerProcessStatus.Stopped; - public long MemoryUsage { get; set; } + public ulong MemoryUsage { get; set; } + public ulong TotalMemory { get; set; } public double ProcessorLoad { get; set; } public TimeSpan LastMeasuredProcessorTime { get; set; } public Stopwatch ProcessTimer { get; set; } = new(); diff --git a/LANCommander.Server.Services/ServerEngines/DockerServerEngine.cs b/LANCommander.Server.Services/ServerEngines/DockerServerEngine.cs index 5030c18b..5b462480 100644 --- a/LANCommander.Server.Services/ServerEngines/DockerServerEngine.cs +++ b/LANCommander.Server.Services/ServerEngines/DockerServerEngine.cs @@ -21,8 +21,8 @@ public class DockerServerEngine( SDK.Client client) : IServerEngine { private ServerEngineConfiguration _config; + private Dictionary _state = new(); private Dictionary _dockerClients = new(); - private Dictionary _tracked { get; set; } = new(); public event EventHandler? OnServerStatusUpdate; public event EventHandler? OnServerLog; @@ -46,7 +46,7 @@ public class DockerServerEngine( foreach (var server in servers) { if (server.DockerHostId.HasValue && _dockerClients.ContainsKey(server.DockerHostId.Value)) - _tracked[server.Id] = new DockerContainer + _state[server.Id].Container = new DockerContainer { HostId = server.DockerHostId.Value, Id = server.ContainerId, @@ -76,14 +76,14 @@ public class DockerServerEngine( public bool IsManaging(Guid serverId) { - return _tracked.ContainsKey(serverId); + return _state.ContainsKey(serverId); } public async Task StartAsync(Guid serverId) { Data.Models.Server server; - if (!_tracked.ContainsKey(serverId)) + if (!IsManaging(serverId)) throw new Exception("Server is not being tracked by this engine."); using (var scope = serviceProvider.CreateScope()) @@ -141,7 +141,7 @@ public class DockerServerEngine( public async Task StopAsync(Guid serverId) { - if (!_tracked.ContainsKey(serverId)) + if (!IsManaging(serverId)) throw new Exception("Server is not being tracked by this engine."); using (var scope = serviceProvider.CreateScope()) @@ -182,13 +182,13 @@ public class DockerServerEngine( } } - public async Task GetStatusAsync(Guid serverId) + private async Task GetStatusAsync(Guid serverId) { Data.Models.Server server = null; using (var scope = serviceProvider.CreateScope()) { - if (!_tracked.ContainsKey(serverId)) + if (!IsManaging(serverId)) { var serverService = scope.ServiceProvider.GetRequiredService(); @@ -203,7 +203,7 @@ public class DockerServerEngine( if (String.IsNullOrWhiteSpace(server.ContainerId)) return ServerProcessStatus.Stopped; - _tracked[serverId] = new DockerContainer + _state[serverId].Container = new DockerContainer { Id = server.ContainerId, HostId = server.DockerHostId.Value, @@ -213,7 +213,7 @@ public class DockerServerEngine( try { - var container = await _dockerClients[_tracked[serverId].HostId].Containers.InspectContainerAsync(_tracked[serverId].Id); + var container = await _dockerClients[_state[serverId].Container.HostId].Containers.InspectContainerAsync(_state[serverId].Container.Id); if (container.State.Running) return ServerProcessStatus.Running; @@ -235,8 +235,35 @@ public class DockerServerEngine( } } - public Task GetStateAsync(Guid serverId) + public async Task GetStateAsync(Guid serverId) { - return Task.FromResult(new ServerProcessState()); + if (!IsManaging(serverId)) + _state[serverId] = new ServerDockerContainerState(); + + var statsProgress = new Progress(stats => + { + // Calculate CPU usage + var cpuDelta = stats.CPUStats.CPUUsage.TotalUsage - stats.PreCPUStats.CPUUsage.TotalUsage; + var systemDelta = stats.CPUStats.SystemUsage - stats.PreCPUStats.SystemUsage; + double cpuPercentage = 0; + + if (systemDelta > 0 && cpuDelta > 0) + cpuPercentage = ((double)cpuDelta / systemDelta) * stats.CPUStats.OnlineCPUs * 100.0; + + _state[serverId].ProcessorLoad = cpuPercentage; + _state[serverId].MemoryUsage = stats.MemoryStats.Usage; + }); + + try + { + _dockerClients[_state[serverId].Container.HostId].Containers.GetContainerStatsAsync(_state[serverId].Container.Id, new ContainerStatsParameters { Stream = false}, statsProgress); + } + catch + { + _state[serverId].MemoryUsage = 0; + _state[serverId].ProcessorLoad = 0; + } + + return await Task.FromResult(_state[serverId]); } } \ No newline at end of file diff --git a/LANCommander.Server.Services/ServerEngines/LocalServerEngine.cs b/LANCommander.Server.Services/ServerEngines/LocalServerEngine.cs index 5401bf55..e9489abb 100644 --- a/LANCommander.Server.Services/ServerEngines/LocalServerEngine.cs +++ b/LANCommander.Server.Services/ServerEngines/LocalServerEngine.cs @@ -196,7 +196,7 @@ public class LocalServerEngine( } } - public async Task GetStateAsync(Guid serverId) + public async Task GetStateAsync(Guid serverId) { if (serverId == Guid.Empty) return null!; @@ -208,7 +208,7 @@ public class LocalServerEngine( { _state[serverId].Status = ServerProcessStatus.Running; - _state[serverId].MemoryUsage = _state[serverId].Process.PrivateMemorySize64; + _state[serverId].MemoryUsage = (ulong)_state[serverId].Process.PrivateMemorySize64; _state[serverId].ProcessorLoad = (_state[serverId].Process.TotalProcessorTime - _state[serverId].LastMeasuredProcessorTime).TotalMilliseconds / (Environment.ProcessorCount * _state[serverId].ProcessTimer.ElapsedMilliseconds); _state[serverId].ProcessTimer.Restart();