diff --git a/src/Vantage.Core/Collectors/Processes/IProcessProvider.cs b/src/Vantage.Core/Collectors/Processes/IProcessProvider.cs new file mode 100644 index 0000000..6a0acd2 --- /dev/null +++ b/src/Vantage.Core/Collectors/Processes/IProcessProvider.cs @@ -0,0 +1,8 @@ +using Vantage.Core.Models; + +namespace Vantage.Core.Collectors.Processes; + +public interface IProcessProvider +{ + Task> GetTopProcessesAsync(HostConfig host, int count, CancellationToken cancellationToken); +} diff --git a/src/Vantage.Core/Collectors/Processes/MockProcessProvider.cs b/src/Vantage.Core/Collectors/Processes/MockProcessProvider.cs new file mode 100644 index 0000000..232d9fa --- /dev/null +++ b/src/Vantage.Core/Collectors/Processes/MockProcessProvider.cs @@ -0,0 +1,44 @@ +using Vantage.Core.Models; + +namespace Vantage.Core.Collectors.Processes; + +public sealed class MockProcessProvider : IProcessProvider +{ + private static readonly (string User, string Command, double Cpu, double Mem)[] Catalog = + { + ("root", "/usr/lib/systemd/systemd", 0.3, 0.4), + ("postgres", "postgres: writer", 3.1, 8.7), + ("root", "dockerd", 5.4, 2.1), + ("www-data", "nginx: worker", 1.2, 0.9), + ("root", "containerd", 2.8, 1.5), + ("node", "node server.js", 12.6, 6.3), + ("root", "sshd: foksiu", 0.1, 0.2), + ("prometheus", "prometheus", 4.7, 3.9), + ("grafana", "grafana-server", 1.9, 4.2), + ("root", "journald", 0.2, 0.3) + }; + + private readonly Random random; + + public MockProcessProvider(int seed = 1337) + { + random = new Random(seed); + } + + public Task> GetTopProcessesAsync(HostConfig host, int count, CancellationToken cancellationToken) + { + var pid = 100; + var processes = Catalog + .Select(entry => new ProcessInfo( + pid += random.Next(1, 900), + entry.User, + Math.Round(entry.Cpu + random.NextDouble() * 4, 1), + Math.Round(entry.Mem + random.NextDouble() * 2, 1), + entry.Command)) + .OrderByDescending(p => p.CpuPercent) + .Take(count) + .ToArray(); + + return Task.FromResult>(processes); + } +} diff --git a/src/Vantage.Core/Collectors/Processes/ProcessInfo.cs b/src/Vantage.Core/Collectors/Processes/ProcessInfo.cs new file mode 100644 index 0000000..59f5d1f --- /dev/null +++ b/src/Vantage.Core/Collectors/Processes/ProcessInfo.cs @@ -0,0 +1,8 @@ +namespace Vantage.Core.Collectors.Processes; + +public readonly record struct ProcessInfo( + int Pid, + string User, + double CpuPercent, + double MemoryPercent, + string Command); diff --git a/src/Vantage.Core/Collectors/Processes/ProcessListParser.cs b/src/Vantage.Core/Collectors/Processes/ProcessListParser.cs new file mode 100644 index 0000000..febcb78 --- /dev/null +++ b/src/Vantage.Core/Collectors/Processes/ProcessListParser.cs @@ -0,0 +1,39 @@ +using System.Globalization; + +namespace Vantage.Core.Collectors.Processes; + +public static class ProcessListParser +{ + public static IReadOnlyList Parse(string psOutput) + { + var processes = new List(); + + foreach (var rawLine in psOutput.Replace("\r\n", "\n").Split('\n')) + { + var line = rawLine.Trim(); + if (line.Length == 0) + continue; + + var fields = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (fields.Length < 5) + continue; + + if (!int.TryParse(fields[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var pid)) + continue; + + if (!TryParsePercent(fields[2], out var cpu) || !TryParsePercent(fields[3], out var mem)) + continue; + + var command = string.Join(' ', fields[4..]); + processes.Add(new ProcessInfo(pid, fields[1], cpu, mem, command)); + } + + return processes; + } + + public static IReadOnlyList TopByCpu(string psOutput, int count) => + Parse(psOutput).OrderByDescending(p => p.CpuPercent).Take(count).ToArray(); + + private static bool TryParsePercent(string value, out double parsed) => + double.TryParse(value.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out parsed); +} diff --git a/src/Vantage.Core/Collectors/Processes/SshProcessProvider.cs b/src/Vantage.Core/Collectors/Processes/SshProcessProvider.cs new file mode 100644 index 0000000..06eafb1 --- /dev/null +++ b/src/Vantage.Core/Collectors/Processes/SshProcessProvider.cs @@ -0,0 +1,26 @@ +using Vantage.Core.Models; +using Vantage.Core.Ssh; + +namespace Vantage.Core.Collectors.Processes; + +public sealed class SshProcessProvider : IProcessProvider +{ + private readonly ISshCommandRunner runner; + + public SshProcessProvider(ISshCommandRunner runner) + { + this.runner = runner; + } + + public static string BuildCommand(int count) + { + var limit = Math.Max(1, count); + return $"ps -eo pid,user:20,pcpu,pmem,comm --sort=-pcpu --no-headers | head -n {limit}"; + } + + public async Task> GetTopProcessesAsync(HostConfig host, int count, CancellationToken cancellationToken) + { + var output = await runner.RunAsync(host, BuildCommand(count), cancellationToken).ConfigureAwait(false); + return ProcessListParser.TopByCpu(output, count); + } +}