From abfc3b807dec4970f1614856524d9ef8b348a103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0=20=D0=92=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8=D0=BA=D0=B8=D0=B9?= Date: Fri, 15 Mar 2024 03:48:51 +0500 Subject: [PATCH 1/8] (#8) Implement proc folder reader --- .../ProcessDoctor.Backend.Core.Tests.csproj | 14 +- .../ProcessDoctor.Backend.Core.csproj | 1 + .../GlobalUsings.cs | 1 + .../ProcFileSystemFixture.cs | 10 + .../ProcTests/DirectoryInfoExtensionsTests.cs | 46 ++++ .../ProcTests/ProcessEntryTests.cs | 98 +++++++++ .../ProcTests/ProcessStatusTests.cs | 199 ++++++++++++++++++ .../ProcessDoctor.Backend.Linux.Tests.csproj | 20 ++ .../ProcessFixture.cs | 42 ++++ ProcessDoctor.Backend.Linux/LinuxProcess.cs | 25 +++ .../InvalidProcessDirectoryException.cs | 4 + .../Exceptions/InvalidStatusFileException.cs | 4 + .../InvalidStatusFilePropertyException.cs | 8 + .../Exceptions/StatusFileNotFoundException.cs | 4 + .../Extensions/DirectoryInfoExtensions.cs | 9 + .../Proc/Native/LibC.cs | 35 +++ ProcessDoctor.Backend.Linux/Proc/ProcPaths.cs | 23 ++ .../Proc/ProcessEntry.cs | 82 ++++++++ .../Proc/StatusFile/Enums/ProcessState.cs | 29 +++ .../Proc/StatusFile/Enums/StatusProperty.cs | 8 + .../Proc/StatusFile/ProcessStatus.cs | 103 +++++++++ .../ProcessDoctor.Backend.Linux.csproj | 17 ++ ...ProcessDoctor.Backend.Windows.Tests.csproj | 12 -- .../ProcessDoctor.TestFramework.csproj | 4 +- ProcessDoctor.sln | 13 ++ 25 files changed, 785 insertions(+), 26 deletions(-) create mode 100644 ProcessDoctor.Backend.Linux.Tests/GlobalUsings.cs create mode 100644 ProcessDoctor.Backend.Linux.Tests/ProcFileSystemFixture.cs create mode 100644 ProcessDoctor.Backend.Linux.Tests/ProcTests/DirectoryInfoExtensionsTests.cs create mode 100644 ProcessDoctor.Backend.Linux.Tests/ProcTests/ProcessEntryTests.cs create mode 100644 ProcessDoctor.Backend.Linux.Tests/ProcTests/ProcessStatusTests.cs create mode 100644 ProcessDoctor.Backend.Linux.Tests/ProcessDoctor.Backend.Linux.Tests.csproj create mode 100644 ProcessDoctor.Backend.Linux.Tests/ProcessFixture.cs create mode 100644 ProcessDoctor.Backend.Linux/LinuxProcess.cs create mode 100644 ProcessDoctor.Backend.Linux/Proc/Exceptions/InvalidProcessDirectoryException.cs create mode 100644 ProcessDoctor.Backend.Linux/Proc/Exceptions/InvalidStatusFileException.cs create mode 100644 ProcessDoctor.Backend.Linux/Proc/Exceptions/InvalidStatusFilePropertyException.cs create mode 100644 ProcessDoctor.Backend.Linux/Proc/Exceptions/StatusFileNotFoundException.cs create mode 100644 ProcessDoctor.Backend.Linux/Proc/Extensions/DirectoryInfoExtensions.cs create mode 100644 ProcessDoctor.Backend.Linux/Proc/Native/LibC.cs create mode 100644 ProcessDoctor.Backend.Linux/Proc/ProcPaths.cs create mode 100644 ProcessDoctor.Backend.Linux/Proc/ProcessEntry.cs create mode 100644 ProcessDoctor.Backend.Linux/Proc/StatusFile/Enums/ProcessState.cs create mode 100644 ProcessDoctor.Backend.Linux/Proc/StatusFile/Enums/StatusProperty.cs create mode 100644 ProcessDoctor.Backend.Linux/Proc/StatusFile/ProcessStatus.cs create mode 100644 ProcessDoctor.Backend.Linux/ProcessDoctor.Backend.Linux.csproj diff --git a/ProcessDoctor.Backend.Core.Tests/ProcessDoctor.Backend.Core.Tests.csproj b/ProcessDoctor.Backend.Core.Tests/ProcessDoctor.Backend.Core.Tests.csproj index 60b901c..830d447 100644 --- a/ProcessDoctor.Backend.Core.Tests/ProcessDoctor.Backend.Core.Tests.csproj +++ b/ProcessDoctor.Backend.Core.Tests/ProcessDoctor.Backend.Core.Tests.csproj @@ -10,20 +10,8 @@ - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + diff --git a/ProcessDoctor.Backend.Core/ProcessDoctor.Backend.Core.csproj b/ProcessDoctor.Backend.Core/ProcessDoctor.Backend.Core.csproj index 0c06ca1..de7d31a 100644 --- a/ProcessDoctor.Backend.Core/ProcessDoctor.Backend.Core.csproj +++ b/ProcessDoctor.Backend.Core/ProcessDoctor.Backend.Core.csproj @@ -10,6 +10,7 @@ + diff --git a/ProcessDoctor.Backend.Linux.Tests/GlobalUsings.cs b/ProcessDoctor.Backend.Linux.Tests/GlobalUsings.cs new file mode 100644 index 0000000..c802f44 --- /dev/null +++ b/ProcessDoctor.Backend.Linux.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/ProcessDoctor.Backend.Linux.Tests/ProcFileSystemFixture.cs b/ProcessDoctor.Backend.Linux.Tests/ProcFileSystemFixture.cs new file mode 100644 index 0000000..d447a56 --- /dev/null +++ b/ProcessDoctor.Backend.Linux.Tests/ProcFileSystemFixture.cs @@ -0,0 +1,10 @@ +using JetBrains.Annotations; + +namespace ProcessDoctor.Backend.Linux.Tests; + +[UsedImplicitly] +public sealed class ProcFileSystemFixture +{ + public ProcessFixture CreateProcess(uint id) + => new(id); +} diff --git a/ProcessDoctor.Backend.Linux.Tests/ProcTests/DirectoryInfoExtensionsTests.cs b/ProcessDoctor.Backend.Linux.Tests/ProcTests/DirectoryInfoExtensionsTests.cs new file mode 100644 index 0000000..da72bd5 --- /dev/null +++ b/ProcessDoctor.Backend.Linux.Tests/ProcTests/DirectoryInfoExtensionsTests.cs @@ -0,0 +1,46 @@ +using System.IO.Abstractions.TestingHelpers; +using FluentAssertions; +using ProcessDoctor.Backend.Linux.Proc; +using ProcessDoctor.Backend.Linux.Proc.Extensions; + +namespace ProcessDoctor.Backend.Linux.Tests.ProcTests; + +public sealed class DirectoryInfoExtensionsTests +{ + [Theory] + [InlineData("124152")] + [InlineData("245")] + [InlineData("245612")] + [InlineData("67")] + public void Should_return_true_if_directory_is_process(string expectedProcessId) + { + // Arrange + var fileSystem = new MockFileSystem(); + var path = fileSystem.Path.Combine(ProcPaths.Path, expectedProcessId); + var sut = fileSystem.DirectoryInfo.New(path); + + // Act & Assert + sut.IsProcess() + .Should() + .BeTrue(); + } + + [Theory] + [InlineData("-1")] + [InlineData("-523")] + [InlineData("status")] + [InlineData("cmdline")] + [InlineData("123exe")] + public void Should_return_false_if_directory_is_not_process(string expectedProcessId) + { + // Arrange + var fileSystem = new MockFileSystem(); + var path = fileSystem.Path.Combine(ProcPaths.Path, expectedProcessId); + var sut = fileSystem.DirectoryInfo.New(path); + + // Act & Assert + sut.IsProcess() + .Should() + .BeFalse(); + } +} diff --git a/ProcessDoctor.Backend.Linux.Tests/ProcTests/ProcessEntryTests.cs b/ProcessDoctor.Backend.Linux.Tests/ProcTests/ProcessEntryTests.cs new file mode 100644 index 0000000..253d58d --- /dev/null +++ b/ProcessDoctor.Backend.Linux.Tests/ProcTests/ProcessEntryTests.cs @@ -0,0 +1,98 @@ +using System.IO.Abstractions.TestingHelpers; +using FluentAssertions; +using ProcessDoctor.Backend.Linux.Proc; +using ProcessDoctor.Backend.Linux.Proc.Exceptions; + +namespace ProcessDoctor.Backend.Linux.Tests.ProcTests; + +public sealed class ProcessEntryTests(ProcFileSystemFixture procFileSystem) : IClassFixture +{ + [Theory] + [InlineData("dir")] + [InlineData("123dir")] + [InlineData("dir123")] + [InlineData("-123")] + public void Should_throw_exception_if_directory_is_not_process(string directoryName) + { + // Arrange + var processDirectory = new MockFileSystem() + .DirectoryInfo + .New(directoryName); + + // Act & Assert + this.Invoking(_ => ProcessEntry.Create(processDirectory)) + .Should() + .Throw(); + } + + [Theory] + [InlineData(123u)] + [InlineData(1234u)] + [InlineData(0u)] + public void Should_read_process_id_properly(uint expectedId) + { + // Arrange & Act + var process = procFileSystem.CreateProcess(expectedId); + var sut = ProcessEntry.Create(process.Directory); + + // Assert + sut.Id + .Should() + .Be(expectedId); + } + + [Theory] + [InlineData("cmdline")] + [InlineData(@"C:\NET\ProcessDoctor\ProcessDoctor.exe")] + public void Should_read_process_command_line_properly(string expectedCommandLine) + { + // Arrange + var process = procFileSystem.CreateProcess(123u); + using (var writer = process.CommandLineFile.CreateText()) + writer.Write(expectedCommandLine); + + // Act + var sut = ProcessEntry.Create(process.Directory); + + // Assert + sut.CommandLine + .Should() + .Be(expectedCommandLine); + } + + [Fact] + public void Command_line_should_be_null_if_file_is_empty() + { + // Arrange + var process = procFileSystem.CreateProcess(123u); + + // Act + var sut = ProcessEntry.Create(process.Directory); + + // Assert + sut.CommandLine + .Should() + .BeNull(); + } + + [Fact] + public void Should_read_process_status_section_properly() + { + // Arrange & Act + var process = procFileSystem.CreateProcess(123u); + using (var writer = process.StatusFile.CreateText()) + writer.Write( + """ + Name: ProcessDoctor + ... + ... + """); + + var sut = ProcessEntry.Create(process.Directory); + + // Assert + sut.Status + .Should() + .NotBeNull(); + } +} diff --git a/ProcessDoctor.Backend.Linux.Tests/ProcTests/ProcessStatusTests.cs b/ProcessDoctor.Backend.Linux.Tests/ProcTests/ProcessStatusTests.cs new file mode 100644 index 0000000..b1e4184 --- /dev/null +++ b/ProcessDoctor.Backend.Linux.Tests/ProcTests/ProcessStatusTests.cs @@ -0,0 +1,199 @@ +using System.IO.Abstractions.TestingHelpers; +using FluentAssertions; +using ProcessDoctor.Backend.Linux.Proc.Exceptions; +using ProcessDoctor.Backend.Linux.Proc.StatusFile; +using ProcessDoctor.Backend.Linux.Proc.StatusFile.Enums; + +namespace ProcessDoctor.Backend.Linux.Tests.ProcTests; + +public sealed class ProcessStatusTests(ProcFileSystemFixture procFileSystem) : IClassFixture +{ + [Theory] + [InlineData("stat")] + [InlineData("123")] + [InlineData("cmdline")] + [InlineData("exe")] + public void Should_throw_exception_if_status_file_name_is_invalid(string statusFileName) + { + // Arrange + var statusFile = new MockFileSystem() + .FileInfo + .New(statusFileName); + + // Act & Assert + this.Invoking(_ => ProcessStatus.Create(statusFile)) + .Should() + .Throw(); + } + + [Theory] + [InlineData("ProcessDoctor")] + [InlineData("Rider")] + public void Should_read_name_properly(string expectedName) + { + // Arrange + var process = procFileSystem.CreateProcess(123u); + using (var writer = process.StatusFile.CreateText()) + writer.Write( + $""" + Name: {expectedName} + ... + ... + """); + + var sut = ProcessStatus.Create(process.StatusFile); + + // Act & Assert + sut.Name + .Should() + .Be(expectedName); + } + + [Fact] + public void Should_throw_exception_if_name_is_invalid() + { + // Arrange + var process = procFileSystem.CreateProcess(123u); + using (var writer = process.StatusFile.CreateText()) + writer.Write( + """ + Name: + ... + ... + """); + + var sut = ProcessStatus.Create(process.StatusFile); + + // Act & Assert + sut.Invoking(status => status.Name) + .Should() + .Throw(); + } + + [Theory] + [InlineData(123u)] + [InlineData(1234u)] + public void Should_read_parent_id_properly(uint expectedParentId) + { + // Arrange + var process = procFileSystem.CreateProcess(123u); + using (var writer = process.StatusFile.CreateText()) + writer.Write( + $""" + Name: ProcessDoctor + ... + ... + ... + ... + ... + PPid: {expectedParentId} + """); + + var sut = ProcessStatus.Create(process.StatusFile); + + // Act & Assert + sut.ParentId + .Should() + .Be(expectedParentId); + } + + [Fact] + public void Parent_id_should_be_null_if_value_was_zero() + { + // Arrange + var process = procFileSystem.CreateProcess(123u); + using (var writer = process.StatusFile.CreateText()) + writer.Write( + """ + Name: ProcessDoctor + ... + ... + ... + ... + ... + PPid: 0 + """); + + var sut = ProcessStatus.Create(process.StatusFile); + + // Act & Assert + sut.ParentId + .Should() + .BeNull(); + } + + [Theory] + [InlineData("-123")] + [InlineData("exe")] + [InlineData("")] + public void Should_throw_exception_if_parent_id_is_invalid(string expectedParentId) + { + // Arrange + var process = procFileSystem.CreateProcess(123u); + using (var writer = process.StatusFile.CreateText()) + writer.Write( + $""" + Name: ProcessDoctor + ... + ... + ... + ... + ... + PPid: {expectedParentId} + """); + + var sut = ProcessStatus.Create(process.StatusFile); + + // Act & Assert + sut.Invoking(status => status.ParentId) + .Should() + .Throw(); + } + + [Theory] + [InlineData("R (running)", ProcessState.Running)] + [InlineData("S (sleeping)", ProcessState.Sleeping)] + [InlineData("D", ProcessState.UninterruptibleWait)] + [InlineData("Z", ProcessState.Zombie)] + [InlineData("T", ProcessState.TracedOrStopped)] + public void Should_read_state_properly(string rawState, ProcessState expectedState) + { + // Arrange + var process = procFileSystem.CreateProcess(123u); + using (var writer = process.StatusFile.CreateText()) + writer.Write( + $""" + Name: ProcessDoctor + ... + State: {rawState} + """); + + var sut = ProcessStatus.Create(process.StatusFile); + + // Act & Assert + sut.State + .Should() + .Be(expectedState); + } + + [Fact] + public void Should_throw_exception_if_state_is_invalid() + { + // Arrange + var process = procFileSystem.CreateProcess(123u); + using (var writer = process.StatusFile.CreateText()) + writer.Write( + """ + Name: ProcessDoctor + ... + State: + """); + + var sut = ProcessStatus.Create(process.StatusFile); + + // Act & Assert + sut.Invoking(status => status.State) + .Should() + .Throw(); + } +} diff --git a/ProcessDoctor.Backend.Linux.Tests/ProcessDoctor.Backend.Linux.Tests.csproj b/ProcessDoctor.Backend.Linux.Tests/ProcessDoctor.Backend.Linux.Tests.csproj new file mode 100644 index 0000000..88f4a14 --- /dev/null +++ b/ProcessDoctor.Backend.Linux.Tests/ProcessDoctor.Backend.Linux.Tests.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + false + true + + + + + + + + + + + + diff --git a/ProcessDoctor.Backend.Linux.Tests/ProcessFixture.cs b/ProcessDoctor.Backend.Linux.Tests/ProcessFixture.cs new file mode 100644 index 0000000..b46f2f1 --- /dev/null +++ b/ProcessDoctor.Backend.Linux.Tests/ProcessFixture.cs @@ -0,0 +1,42 @@ +using System.IO.Abstractions; +using System.IO.Abstractions.TestingHelpers; +using ProcessDoctor.Backend.Linux.Proc; + +namespace ProcessDoctor.Backend.Linux.Tests; + +public sealed class ProcessFixture +{ + public IDirectoryInfo Directory { get; } + + public IFileInfo CommandLineFile { get; } + + public IFileInfo ExecutablePathFile { get; } + + public IFileInfo StatusFile { get; } + + public ProcessFixture(uint id) + { + var fileSystem = new MockFileSystem(); + + var directoryPath = fileSystem.Path.Combine(ProcPaths.Path, id.ToString()); + var processDirectory = fileSystem.DirectoryInfo.New(directoryPath); + fileSystem.AddDirectory(directoryPath); + + var exePath = fileSystem.Path.Combine(processDirectory.FullName, ProcPaths.ExecutablePath.FileName); + var exeFile = fileSystem.FileInfo.New(exePath); + fileSystem.AddEmptyFile(exeFile); + + var commandLinePath = fileSystem.Path.Combine(processDirectory.FullName, ProcPaths.CommandLine.FileName); + var commandLineFile = fileSystem.FileInfo.New(commandLinePath); + fileSystem.AddEmptyFile(commandLineFile); + + var statusPath = fileSystem.Path.Combine(processDirectory.FullName, ProcPaths.Status.FileName); + var statusFile = fileSystem.FileInfo.New(statusPath); + fileSystem.AddEmptyFile(statusFile); + + Directory = processDirectory; + CommandLineFile = commandLineFile; + ExecutablePathFile = exeFile; + StatusFile = statusFile; + } +} diff --git a/ProcessDoctor.Backend.Linux/LinuxProcess.cs b/ProcessDoctor.Backend.Linux/LinuxProcess.cs new file mode 100644 index 0000000..58f0b20 --- /dev/null +++ b/ProcessDoctor.Backend.Linux/LinuxProcess.cs @@ -0,0 +1,25 @@ +using ProcessDoctor.Backend.Core; +using ProcessDoctor.Backend.Linux.Proc; +using SkiaSharp; + +namespace ProcessDoctor.Backend.Linux; + +public sealed record LinuxProcess : SystemProcess +{ + public static LinuxProcess Create(ProcessEntry processEntry) + => new( + processEntry.Id, + processEntry.Status.ParentId, + processEntry.Status.Name, + processEntry.CommandLine, + processEntry.ExecutablePath); + + /// + private LinuxProcess(uint Id, uint? ParentId, string Name, string? CommandLine, string? ExecutablePath) + : base(Id, ParentId, Name, CommandLine, ExecutablePath) + { } + + /// + public override SKBitmap ExtractIcon() + => throw new NotImplementedException(); +} diff --git a/ProcessDoctor.Backend.Linux/Proc/Exceptions/InvalidProcessDirectoryException.cs b/ProcessDoctor.Backend.Linux/Proc/Exceptions/InvalidProcessDirectoryException.cs new file mode 100644 index 0000000..c226070 --- /dev/null +++ b/ProcessDoctor.Backend.Linux/Proc/Exceptions/InvalidProcessDirectoryException.cs @@ -0,0 +1,4 @@ +namespace ProcessDoctor.Backend.Linux.Proc.Exceptions; + +public sealed class InvalidProcessDirectoryException(string directoryPath) + : Exception($"Directory is not a process: {directoryPath}"); diff --git a/ProcessDoctor.Backend.Linux/Proc/Exceptions/InvalidStatusFileException.cs b/ProcessDoctor.Backend.Linux/Proc/Exceptions/InvalidStatusFileException.cs new file mode 100644 index 0000000..5293797 --- /dev/null +++ b/ProcessDoctor.Backend.Linux/Proc/Exceptions/InvalidStatusFileException.cs @@ -0,0 +1,4 @@ +namespace ProcessDoctor.Backend.Linux.Proc.Exceptions; + +public sealed class InvalidStatusFileException(string fileName) + : Exception($"File is not a process file: {fileName}"); diff --git a/ProcessDoctor.Backend.Linux/Proc/Exceptions/InvalidStatusFilePropertyException.cs b/ProcessDoctor.Backend.Linux/Proc/Exceptions/InvalidStatusFilePropertyException.cs new file mode 100644 index 0000000..da445eb --- /dev/null +++ b/ProcessDoctor.Backend.Linux/Proc/Exceptions/InvalidStatusFilePropertyException.cs @@ -0,0 +1,8 @@ +using ProcessDoctor.Backend.Linux.Proc.StatusFile.Enums; + +namespace ProcessDoctor.Backend.Linux.Proc.Exceptions; + +public sealed class InvalidStatusFilePropertyException(StatusProperty property, Exception? innerException = null) + : Exception( + $"An error occurred while reading status file property. Property: {property}. Line index: {(int)property}", + innerException); diff --git a/ProcessDoctor.Backend.Linux/Proc/Exceptions/StatusFileNotFoundException.cs b/ProcessDoctor.Backend.Linux/Proc/Exceptions/StatusFileNotFoundException.cs new file mode 100644 index 0000000..701645a --- /dev/null +++ b/ProcessDoctor.Backend.Linux/Proc/Exceptions/StatusFileNotFoundException.cs @@ -0,0 +1,4 @@ +namespace ProcessDoctor.Backend.Linux.Proc.Exceptions; + +public sealed class StatusFileNotFoundException(string processPath) + : Exception($"Status file not found: {processPath}"); diff --git a/ProcessDoctor.Backend.Linux/Proc/Extensions/DirectoryInfoExtensions.cs b/ProcessDoctor.Backend.Linux/Proc/Extensions/DirectoryInfoExtensions.cs new file mode 100644 index 0000000..22f8a95 --- /dev/null +++ b/ProcessDoctor.Backend.Linux/Proc/Extensions/DirectoryInfoExtensions.cs @@ -0,0 +1,9 @@ +using System.IO.Abstractions; + +namespace ProcessDoctor.Backend.Linux.Proc.Extensions; + +public static class DirectoryInfoExtensions +{ + public static bool IsProcess(this IDirectoryInfo directory) + => directory.Name.Length > 0 && directory.Name.All(char.IsDigit); +} diff --git a/ProcessDoctor.Backend.Linux/Proc/Native/LibC.cs b/ProcessDoctor.Backend.Linux/Proc/Native/LibC.cs new file mode 100644 index 0000000..272f40e --- /dev/null +++ b/ProcessDoctor.Backend.Linux/Proc/Native/LibC.cs @@ -0,0 +1,35 @@ +using System.Runtime.InteropServices; + +namespace ProcessDoctor.Backend.Linux.Proc.Native; + +internal static class LibC +{ + private const string Name = "libc"; + + [DllImport(Name, EntryPoint = "readlink", SetLastError = true)] + private static extern int NativeReadLink(string path, byte[] buffer, int bufferSize); // TODO: Make testable + + /// + /// The DllImportAttribute provides a SetLastError property + /// so the runtime knows to immediately capture the last error and + /// store it in a place that the managed code can read using Marshal.GetLastWin32Error. + /// + internal static string? GetLastError() + => Marshal.PtrToStringAnsi(StrError(Marshal.GetLastWin32Error())); + + [DllImport(Name, EntryPoint = "strerror", SetLastError = false)] + private static extern IntPtr StrError(int errorCode); + + public static int ReadLink(string path, byte[] buffer, int bufferSize) + { + try + { + return NativeReadLink(path, buffer, bufferSize); + } + + catch (DllNotFoundException) + { + return -1; + } + } +} diff --git a/ProcessDoctor.Backend.Linux/Proc/ProcPaths.cs b/ProcessDoctor.Backend.Linux/Proc/ProcPaths.cs new file mode 100644 index 0000000..ddd231e --- /dev/null +++ b/ProcessDoctor.Backend.Linux/Proc/ProcPaths.cs @@ -0,0 +1,23 @@ +namespace ProcessDoctor.Backend.Linux.Proc; + +public static class ProcPaths +{ + public const string Path = "/proc"; + + public static class Status + { + public const string FileName = "status"; + } + + public static class CommandLine + { + public const string FileName = "cmdline"; + } + + public static class ExecutablePath + { + public const int MaxSize = 2048; + + public const string FileName = "exe"; + } +} \ No newline at end of file diff --git a/ProcessDoctor.Backend.Linux/Proc/ProcessEntry.cs b/ProcessDoctor.Backend.Linux/Proc/ProcessEntry.cs new file mode 100644 index 0000000..aabe965 --- /dev/null +++ b/ProcessDoctor.Backend.Linux/Proc/ProcessEntry.cs @@ -0,0 +1,82 @@ +using System.IO.Abstractions; +using System.Text; +using ProcessDoctor.Backend.Linux.Proc.Exceptions; +using ProcessDoctor.Backend.Linux.Proc.Extensions; +using ProcessDoctor.Backend.Linux.Proc.Native; +using ProcessDoctor.Backend.Linux.Proc.StatusFile; + +namespace ProcessDoctor.Backend.Linux.Proc; + +public sealed class ProcessEntry +{ + public static ProcessEntry Create(IDirectoryInfo directory) + { + if (!directory.IsProcess()) + throw new InvalidProcessDirectoryException(directory.FullName); + + return new ProcessEntry(directory); + } + + public uint Id { get; } + + public string? CommandLine { get; } + + public string? ExecutablePath { get; } + + public ProcessStatus Status { get; } + + private ProcessEntry(IDirectoryInfo directory) + { + Id = uint.Parse(directory.Name); + CommandLine = ReadCommandLine(directory); + ExecutablePath = ReadExecutablePath(directory); + Status = ReadStatus(directory); + } + + private static string? ReadCommandLine(IDirectoryInfo directory) + { + var path = directory + .FileSystem + .Path + .Combine(directory.FullName, ProcPaths.CommandLine.FileName); + + var value = directory + .FileSystem + .File + .ReadAllText(path) + .Replace('\0', ' '); + + if (string.IsNullOrWhiteSpace(value)) + return null; + + return value; + } + + private static string? ReadExecutablePath(IDirectoryInfo directory) + { + var path = directory + .FileSystem + .Path + .Combine(directory.FullName, ProcPaths.ExecutablePath.FileName); + + var buffer = new byte[ProcPaths.ExecutablePath.MaxSize + 1]; + var count = LibC.ReadLink(path, buffer, ProcPaths.ExecutablePath.MaxSize); + + if (count <= 0) + return null; + + buffer[count] = 0x0; + + return Encoding.UTF8.GetString(buffer, index: 0, count); + } + + private static ProcessStatus ReadStatus(IDirectoryInfo directory) + { + var statusFile = directory + .EnumerateFiles(ProcPaths.Status.FileName) + .FirstOrDefault() + ?? throw new StatusFileNotFoundException(directory.FullName); + + return ProcessStatus.Create(statusFile); + } +} diff --git a/ProcessDoctor.Backend.Linux/Proc/StatusFile/Enums/ProcessState.cs b/ProcessDoctor.Backend.Linux/Proc/StatusFile/Enums/ProcessState.cs new file mode 100644 index 0000000..7ffed5e --- /dev/null +++ b/ProcessDoctor.Backend.Linux/Proc/StatusFile/Enums/ProcessState.cs @@ -0,0 +1,29 @@ +namespace ProcessDoctor.Backend.Linux.Proc.StatusFile.Enums; + +public enum ProcessState +{ + /// + /// Running + /// + Running, + + /// + /// Sleeping + /// + Sleeping, + + /// + /// Sleeping in an uninterruptible wait + /// + UninterruptibleWait, + + /// + /// Zombie + /// + Zombie, + + /// + /// Traced or stopped + /// + TracedOrStopped +} diff --git a/ProcessDoctor.Backend.Linux/Proc/StatusFile/Enums/StatusProperty.cs b/ProcessDoctor.Backend.Linux/Proc/StatusFile/Enums/StatusProperty.cs new file mode 100644 index 0000000..a2a652f --- /dev/null +++ b/ProcessDoctor.Backend.Linux/Proc/StatusFile/Enums/StatusProperty.cs @@ -0,0 +1,8 @@ +namespace ProcessDoctor.Backend.Linux.Proc.StatusFile.Enums; + +public enum StatusProperty +{ + Name = 1, + State = 3, + ParentId = 7 +} diff --git a/ProcessDoctor.Backend.Linux/Proc/StatusFile/ProcessStatus.cs b/ProcessDoctor.Backend.Linux/Proc/StatusFile/ProcessStatus.cs new file mode 100644 index 0000000..70d90c2 --- /dev/null +++ b/ProcessDoctor.Backend.Linux/Proc/StatusFile/ProcessStatus.cs @@ -0,0 +1,103 @@ +using System.IO.Abstractions; +using ProcessDoctor.Backend.Linux.Proc.Exceptions; +using ProcessDoctor.Backend.Linux.Proc.StatusFile.Enums; + +namespace ProcessDoctor.Backend.Linux.Proc.StatusFile; + +public sealed class ProcessStatus +{ + private const char Separator = ':'; + private readonly string[] _lines; + + public static ProcessStatus Create(IFileInfo statusFile) + { + if (statusFile.Name != ProcPaths.Status.FileName) + throw new InvalidStatusFileException(statusFile.Name); + + var lines = statusFile + .FileSystem + .File + .ReadAllLines(statusFile.FullName); + + return new ProcessStatus(lines); + } + + public string Name + => ReadPropertyValue(StatusProperty.Name) + ?? throw new InvalidStatusFilePropertyException(StatusProperty.Name); + + public uint? ParentId + { + get + { + var value = ReadPropertyValue(StatusProperty.ParentId); + + if (!uint.TryParse(value, out var parentId)) + throw new InvalidStatusFilePropertyException(StatusProperty.ParentId); + + if (parentId is 0) + return null; + + return parentId; + } + } + + public ProcessState State + { + get + { + var value = ReadPropertyValue(StatusProperty.State)? + .First(); + + var state = value switch + { + 'R' => ProcessState.Running, + 'S' => ProcessState.Sleeping, + 'D' => ProcessState.UninterruptibleWait, + 'Z' => ProcessState.Zombie, + 'T' => ProcessState.TracedOrStopped, + _ => default(ProcessState?) + }; + + if (state is null) + throw new InvalidStatusFilePropertyException(StatusProperty.State); + + return state.Value; + } + } + + private string? ReadPropertyValue(StatusProperty property) + { + var lineIndex = (int)property; + + if (lineIndex < 0) + throw new ArgumentException( + "Line index cannot be less than 0", + nameof(lineIndex)); + + if (_lines.Length < lineIndex) + throw new InvalidStatusFilePropertyException(property); + + var line = _lines + .Skip(lineIndex - 1) + .FirstOrDefault() + ?? throw new InvalidStatusFilePropertyException(property); + + var separatorIndex = line.IndexOf(Separator, StringComparison.Ordinal); + + if (separatorIndex is -1) + throw new InvalidStatusFilePropertyException(property); + + var value = line + .Substring(separatorIndex + 1, line.Length - separatorIndex - 1) + .Trim(); + + if (string.IsNullOrWhiteSpace(value)) + return null; + + return value; + } + + private ProcessStatus(string[] lines) + => _lines = lines; +} diff --git a/ProcessDoctor.Backend.Linux/ProcessDoctor.Backend.Linux.csproj b/ProcessDoctor.Backend.Linux/ProcessDoctor.Backend.Linux.csproj new file mode 100644 index 0000000..4758a82 --- /dev/null +++ b/ProcessDoctor.Backend.Linux/ProcessDoctor.Backend.Linux.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/ProcessDoctor.Backend.Windows.Tests/ProcessDoctor.Backend.Windows.Tests.csproj b/ProcessDoctor.Backend.Windows.Tests/ProcessDoctor.Backend.Windows.Tests.csproj index 3f6e619..921fb29 100644 --- a/ProcessDoctor.Backend.Windows.Tests/ProcessDoctor.Backend.Windows.Tests.csproj +++ b/ProcessDoctor.Backend.Windows.Tests/ProcessDoctor.Backend.Windows.Tests.csproj @@ -10,18 +10,6 @@ - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - diff --git a/ProcessDoctor.TestFramework/ProcessDoctor.TestFramework.csproj b/ProcessDoctor.TestFramework/ProcessDoctor.TestFramework.csproj index 0bf424a..1763f75 100644 --- a/ProcessDoctor.TestFramework/ProcessDoctor.TestFramework.csproj +++ b/ProcessDoctor.TestFramework/ProcessDoctor.TestFramework.csproj @@ -9,10 +9,12 @@ - + + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/ProcessDoctor.sln b/ProcessDoctor.sln index 95e5858..e4c2f48 100644 --- a/ProcessDoctor.sln +++ b/ProcessDoctor.sln @@ -33,6 +33,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProcessDoctor.TestFramework EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{BD3E309F-5175-4C68-8FFA-5F1A986C3D09}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProcessDoctor.Backend.Linux", "ProcessDoctor.Backend.Linux\ProcessDoctor.Backend.Linux.csproj", "{991FE7A6-8CD3-4712-B8EB-4E83F293CA7E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProcessDoctor.Backend.Linux.Tests", "ProcessDoctor.Backend.Linux.Tests\ProcessDoctor.Backend.Linux.Tests.csproj", "{0AC344D0-ADD1-4FB3-908C-006FCC53D51E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -47,6 +51,7 @@ Global {732E6C91-83E0-4197-9B02-21FD5BB0840F} = {BD3E309F-5175-4C68-8FFA-5F1A986C3D09} {6A05F117-4A4F-4921-9600-77AEED86F0D2} = {BD3E309F-5175-4C68-8FFA-5F1A986C3D09} {F45DBA78-E9A4-4C55-946C-24538B05C664} = {BD3E309F-5175-4C68-8FFA-5F1A986C3D09} + {0AC344D0-ADD1-4FB3-908C-006FCC53D51E} = {BD3E309F-5175-4C68-8FFA-5F1A986C3D09} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {6346D6A2-0AB9-40F7-A191-B1AEA6C90207}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -77,5 +82,13 @@ Global {6A05F117-4A4F-4921-9600-77AEED86F0D2}.Debug|Any CPU.Build.0 = Debug|Any CPU {6A05F117-4A4F-4921-9600-77AEED86F0D2}.Release|Any CPU.ActiveCfg = Release|Any CPU {6A05F117-4A4F-4921-9600-77AEED86F0D2}.Release|Any CPU.Build.0 = Release|Any CPU + {991FE7A6-8CD3-4712-B8EB-4E83F293CA7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {991FE7A6-8CD3-4712-B8EB-4E83F293CA7E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {991FE7A6-8CD3-4712-B8EB-4E83F293CA7E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {991FE7A6-8CD3-4712-B8EB-4E83F293CA7E}.Release|Any CPU.Build.0 = Release|Any CPU + {0AC344D0-ADD1-4FB3-908C-006FCC53D51E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0AC344D0-ADD1-4FB3-908C-006FCC53D51E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0AC344D0-ADD1-4FB3-908C-006FCC53D51E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0AC344D0-ADD1-4FB3-908C-006FCC53D51E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal From 61041187aa35175c400a703a331c97a6ac51cd65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0=20=D0=92=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8=D0=BA=D0=B8=D0=B9?= Date: Fri, 15 Mar 2024 15:31:19 +0500 Subject: [PATCH 2/8] (#8) Implement icon extraction for Linux --- .../FakeProcessEntry.cs | 37 +++++++++++++++ .../FakeProcessStatus.cs | 32 +++++++++++++ .../LinuxProcessTests.cs | 46 +++++++++++++++++++ .../Imaging/IconAttributes.cs | 6 +++ .../Imaging/MimeTypes.cs | 6 +++ ProcessDoctor.Backend.Linux/LinuxProcess.cs | 42 +++++++++++++++-- .../Proc/Interfaces/IProcessEntry.cs | 12 +++++ .../Proc/Interfaces/IProcessStatus.cs | 12 +++++ .../Proc/ProcessEntry.cs | 5 +- .../Proc/StatusFile/ProcessStatus.cs | 3 +- .../ProcessDoctor.Backend.Linux.csproj | 4 +- .../ProcessDoctor.TestFramework.csproj | 1 + 12 files changed, 199 insertions(+), 7 deletions(-) create mode 100644 ProcessDoctor.Backend.Linux.Tests/FakeProcessEntry.cs create mode 100644 ProcessDoctor.Backend.Linux.Tests/FakeProcessStatus.cs create mode 100644 ProcessDoctor.Backend.Linux.Tests/LinuxProcessTests.cs create mode 100644 ProcessDoctor.Backend.Linux/Imaging/IconAttributes.cs create mode 100644 ProcessDoctor.Backend.Linux/Imaging/MimeTypes.cs create mode 100644 ProcessDoctor.Backend.Linux/Proc/Interfaces/IProcessEntry.cs create mode 100644 ProcessDoctor.Backend.Linux/Proc/Interfaces/IProcessStatus.cs diff --git a/ProcessDoctor.Backend.Linux.Tests/FakeProcessEntry.cs b/ProcessDoctor.Backend.Linux.Tests/FakeProcessEntry.cs new file mode 100644 index 0000000..b80304d --- /dev/null +++ b/ProcessDoctor.Backend.Linux.Tests/FakeProcessEntry.cs @@ -0,0 +1,37 @@ +using ProcessDoctor.Backend.Linux.Proc.Interfaces; + +namespace ProcessDoctor.Backend.Linux.Tests; + +public sealed class FakeProcessEntry : IProcessEntry +{ + public static FakeProcessEntry Create( + uint id, + string? commandLine = null, + string? executablePath = null, + IProcessStatus? status = null) + => new( + id, + commandLine, + executablePath, + status ?? FakeProcessStatus.Create()); + + public uint Id { get; } + + public string? CommandLine { get; } + + public string? ExecutablePath { get; } + + public IProcessStatus Status { get; } + + public FakeProcessEntry( + uint id, + string? commandLine, + string? executablePath, + IProcessStatus status) + { + Id = id; + CommandLine = commandLine; + ExecutablePath = executablePath; + Status = status; + } +} diff --git a/ProcessDoctor.Backend.Linux.Tests/FakeProcessStatus.cs b/ProcessDoctor.Backend.Linux.Tests/FakeProcessStatus.cs new file mode 100644 index 0000000..5e35c20 --- /dev/null +++ b/ProcessDoctor.Backend.Linux.Tests/FakeProcessStatus.cs @@ -0,0 +1,32 @@ +using ProcessDoctor.Backend.Linux.Proc.Interfaces; +using ProcessDoctor.Backend.Linux.Proc.StatusFile.Enums; + +namespace ProcessDoctor.Backend.Linux.Tests; + +public sealed class FakeProcessStatus : IProcessStatus +{ + public static FakeProcessStatus Create( + string? name = null, + uint? parentId = null, + ProcessState? state = null) + => new( + name ?? "ProcessDoctor", + parentId, + state ?? ProcessState.Running); + + public string Name { get; } + + public uint? ParentId { get; } + + public ProcessState State { get; } + + private FakeProcessStatus( + string name, + uint? parentId, + ProcessState state) + { + Name = name; + ParentId = parentId; + State = state; + } +} diff --git a/ProcessDoctor.Backend.Linux.Tests/LinuxProcessTests.cs b/ProcessDoctor.Backend.Linux.Tests/LinuxProcessTests.cs new file mode 100644 index 0000000..deed5cd --- /dev/null +++ b/ProcessDoctor.Backend.Linux.Tests/LinuxProcessTests.cs @@ -0,0 +1,46 @@ +using System.Runtime.InteropServices; +using FluentAssertions; + +namespace ProcessDoctor.Backend.Linux.Tests; + +public sealed class LinuxProcessTests +{ + [SkippableTheory] + [InlineData("/usr/bin/htop")] + public void Should_extract_icon(string executablePath) + { + Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)); + + // Arrange + var processEntry = FakeProcessEntry.Create(id: 123u, executablePath: executablePath); + var sut = LinuxProcess.Create(processEntry); + + // Act + using var bitmap = sut.ExtractIcon(); + + // Assert + bitmap + .Bytes + .Should() + .NotBeEmpty(); + } + + [SkippableFact] + public void Should_extract_stock_icon() + { + Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)); + + // Arrange + var processEntry = FakeProcessEntry.Create(id: 123u); + var sut = LinuxProcess.Create(processEntry); + + // Act + using var bitmap = sut.ExtractIcon(); + + // Assert + bitmap + .Bytes + .Should() + .NotBeEmpty(); + } +} diff --git a/ProcessDoctor.Backend.Linux/Imaging/IconAttributes.cs b/ProcessDoctor.Backend.Linux/Imaging/IconAttributes.cs new file mode 100644 index 0000000..3622150 --- /dev/null +++ b/ProcessDoctor.Backend.Linux/Imaging/IconAttributes.cs @@ -0,0 +1,6 @@ +namespace ProcessDoctor.Backend.Linux.Imaging; + +public static class IconAttributes +{ + public const string Standard = "standard::*"; +} diff --git a/ProcessDoctor.Backend.Linux/Imaging/MimeTypes.cs b/ProcessDoctor.Backend.Linux/Imaging/MimeTypes.cs new file mode 100644 index 0000000..11a0f96 --- /dev/null +++ b/ProcessDoctor.Backend.Linux/Imaging/MimeTypes.cs @@ -0,0 +1,6 @@ +namespace ProcessDoctor.Backend.Linux.Imaging; + +public static class MimeTypes +{ + public const string Executable = "application/x-executable"; +} diff --git a/ProcessDoctor.Backend.Linux/LinuxProcess.cs b/ProcessDoctor.Backend.Linux/LinuxProcess.cs index 58f0b20..b729e97 100644 --- a/ProcessDoctor.Backend.Linux/LinuxProcess.cs +++ b/ProcessDoctor.Backend.Linux/LinuxProcess.cs @@ -1,12 +1,15 @@ +using GLib; +using Gtk; using ProcessDoctor.Backend.Core; -using ProcessDoctor.Backend.Linux.Proc; +using ProcessDoctor.Backend.Linux.Imaging; +using ProcessDoctor.Backend.Linux.Proc.Interfaces; using SkiaSharp; namespace ProcessDoctor.Backend.Linux; public sealed record LinuxProcess : SystemProcess { - public static LinuxProcess Create(ProcessEntry processEntry) + public static LinuxProcess Create(IProcessEntry processEntry) => new( processEntry.Id, processEntry.Status.ParentId, @@ -21,5 +24,38 @@ private LinuxProcess(uint Id, uint? ParentId, string Name, string? CommandLine, /// public override SKBitmap ExtractIcon() - => throw new NotImplementedException(); + { + using var iconTheme = new IconTheme(); + + if (string.IsNullOrWhiteSpace(ExecutablePath)) + { + return ExtractStockIcon(iconTheme); + } + + var file = FileFactory.NewForPath(ExecutablePath); + + using var fileMetadata = file.QueryInfo( + IconAttributes.Standard, + FileQueryInfoFlags.None, + cancellable: null); + + using var iconMetadata = iconTheme.LookupIcon( + fileMetadata.Icon, + size: 32, + IconLookupFlags.UseBuiltin); + + return SKBitmap.Decode(iconMetadata.Filename); + } + + private SKBitmap ExtractStockIcon(IconTheme iconTheme) + { + var icon = ContentType.GetIcon(MimeTypes.Executable); + + using var iconMetadata = iconTheme.LookupIcon( + icon, + size: 32, + IconLookupFlags.UseBuiltin); + + return SKBitmap.Decode(iconMetadata.Filename); + } } diff --git a/ProcessDoctor.Backend.Linux/Proc/Interfaces/IProcessEntry.cs b/ProcessDoctor.Backend.Linux/Proc/Interfaces/IProcessEntry.cs new file mode 100644 index 0000000..a4334e1 --- /dev/null +++ b/ProcessDoctor.Backend.Linux/Proc/Interfaces/IProcessEntry.cs @@ -0,0 +1,12 @@ +namespace ProcessDoctor.Backend.Linux.Proc.Interfaces; + +public interface IProcessEntry +{ + uint Id { get; } + + string? CommandLine { get; } + + string? ExecutablePath { get; } + + IProcessStatus Status { get; } +} diff --git a/ProcessDoctor.Backend.Linux/Proc/Interfaces/IProcessStatus.cs b/ProcessDoctor.Backend.Linux/Proc/Interfaces/IProcessStatus.cs new file mode 100644 index 0000000..c0e5bf3 --- /dev/null +++ b/ProcessDoctor.Backend.Linux/Proc/Interfaces/IProcessStatus.cs @@ -0,0 +1,12 @@ +using ProcessDoctor.Backend.Linux.Proc.StatusFile.Enums; + +namespace ProcessDoctor.Backend.Linux.Proc.Interfaces; + +public interface IProcessStatus +{ + string Name { get; } + + uint? ParentId { get; } + + ProcessState State { get; } +} diff --git a/ProcessDoctor.Backend.Linux/Proc/ProcessEntry.cs b/ProcessDoctor.Backend.Linux/Proc/ProcessEntry.cs index aabe965..f689ef7 100644 --- a/ProcessDoctor.Backend.Linux/Proc/ProcessEntry.cs +++ b/ProcessDoctor.Backend.Linux/Proc/ProcessEntry.cs @@ -2,12 +2,13 @@ using System.Text; using ProcessDoctor.Backend.Linux.Proc.Exceptions; using ProcessDoctor.Backend.Linux.Proc.Extensions; +using ProcessDoctor.Backend.Linux.Proc.Interfaces; using ProcessDoctor.Backend.Linux.Proc.Native; using ProcessDoctor.Backend.Linux.Proc.StatusFile; namespace ProcessDoctor.Backend.Linux.Proc; -public sealed class ProcessEntry +public sealed class ProcessEntry : IProcessEntry { public static ProcessEntry Create(IDirectoryInfo directory) { @@ -23,7 +24,7 @@ public static ProcessEntry Create(IDirectoryInfo directory) public string? ExecutablePath { get; } - public ProcessStatus Status { get; } + public IProcessStatus Status { get; } private ProcessEntry(IDirectoryInfo directory) { diff --git a/ProcessDoctor.Backend.Linux/Proc/StatusFile/ProcessStatus.cs b/ProcessDoctor.Backend.Linux/Proc/StatusFile/ProcessStatus.cs index 70d90c2..cbeaa8f 100644 --- a/ProcessDoctor.Backend.Linux/Proc/StatusFile/ProcessStatus.cs +++ b/ProcessDoctor.Backend.Linux/Proc/StatusFile/ProcessStatus.cs @@ -1,10 +1,11 @@ using System.IO.Abstractions; using ProcessDoctor.Backend.Linux.Proc.Exceptions; +using ProcessDoctor.Backend.Linux.Proc.Interfaces; using ProcessDoctor.Backend.Linux.Proc.StatusFile.Enums; namespace ProcessDoctor.Backend.Linux.Proc.StatusFile; -public sealed class ProcessStatus +public sealed class ProcessStatus : IProcessStatus { private const char Separator = ':'; private readonly string[] _lines; diff --git a/ProcessDoctor.Backend.Linux/ProcessDoctor.Backend.Linux.csproj b/ProcessDoctor.Backend.Linux/ProcessDoctor.Backend.Linux.csproj index 4758a82..b28c05f 100644 --- a/ProcessDoctor.Backend.Linux/ProcessDoctor.Backend.Linux.csproj +++ b/ProcessDoctor.Backend.Linux/ProcessDoctor.Backend.Linux.csproj @@ -7,9 +7,11 @@ + + - + diff --git a/ProcessDoctor.TestFramework/ProcessDoctor.TestFramework.csproj b/ProcessDoctor.TestFramework/ProcessDoctor.TestFramework.csproj index 1763f75..b6e3dd9 100644 --- a/ProcessDoctor.TestFramework/ProcessDoctor.TestFramework.csproj +++ b/ProcessDoctor.TestFramework/ProcessDoctor.TestFramework.csproj @@ -24,6 +24,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + From 6a1e7f0fbe37e5c574897546d429d393e83d8d6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0=20=D0=92=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8=D0=BA=D0=B8=D0=B9?= Date: Fri, 15 Mar 2024 15:53:42 +0500 Subject: [PATCH 3/8] Fix parameter name --- .../Interfaces/IProcessProvider.cs | 2 +- ProcessDoctor.Backend.Windows/ProcessProvider.cs | 6 +++--- .../WMI/Extensions/ObservationTargetExtensions.cs | 10 +++++----- .../WMI/Interfaces/IManagementEventWatcherFactory.cs | 2 +- .../WMI/ManagementEventWatcherAdapterFactory.cs | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ProcessDoctor.Backend.Core/Interfaces/IProcessProvider.cs b/ProcessDoctor.Backend.Core/Interfaces/IProcessProvider.cs index 35bfaa2..55a4faa 100644 --- a/ProcessDoctor.Backend.Core/Interfaces/IProcessProvider.cs +++ b/ProcessDoctor.Backend.Core/Interfaces/IProcessProvider.cs @@ -4,7 +4,7 @@ namespace ProcessDoctor.Backend.Core.Interfaces; public interface IProcessProvider { - IObservable ObserveProcesses(ObservationTarget targetState); + IObservable ObserveProcesses(ObservationTarget observationTarget); IProcessListSnapshot CreateSnapshot(); } diff --git a/ProcessDoctor.Backend.Windows/ProcessProvider.cs b/ProcessDoctor.Backend.Windows/ProcessProvider.cs index 5443928..76a33c9 100644 --- a/ProcessDoctor.Backend.Windows/ProcessProvider.cs +++ b/ProcessDoctor.Backend.Windows/ProcessProvider.cs @@ -12,9 +12,9 @@ namespace ProcessDoctor.Backend.Windows; public sealed class ProcessProvider(Lifetime lifetime, ILog logger, IManagementEventWatcherFactory watcherFactory) : IProcessProvider { /// - public IObservable ObserveProcesses(ObservationTarget targetState) + public IObservable ObserveProcesses(ObservationTarget observationTarget) { - var watcher = watcherFactory.Create(targetState); + var watcher = watcherFactory.Create(observationTarget); var lifetimeScope = lifetime.CreateNested(); lifetimeScope @@ -31,7 +31,7 @@ public IObservable ObserveProcesses(ObservationTarget targetState { logger.Error(exception, "An error occurred while processing an event received from WMI"); - return ObserveProcesses(targetState); + return ObserveProcesses(observationTarget); }); } diff --git a/ProcessDoctor.Backend.Windows/WMI/Extensions/ObservationTargetExtensions.cs b/ProcessDoctor.Backend.Windows/WMI/Extensions/ObservationTargetExtensions.cs index 6e4bb71..d6d4bde 100644 --- a/ProcessDoctor.Backend.Windows/WMI/Extensions/ObservationTargetExtensions.cs +++ b/ProcessDoctor.Backend.Windows/WMI/Extensions/ObservationTargetExtensions.cs @@ -5,8 +5,8 @@ namespace ProcessDoctor.Backend.Windows.WMI.Extensions; internal static class ObservationTargetExtensions { - internal static WqlEventQuery ToWqlQuery(this ObservationTarget targetState) - => targetState switch + internal static WqlEventQuery ToWqlQuery(this ObservationTarget observationTarget) + => observationTarget switch { ObservationTarget.Launched => new WqlEventQuery( @@ -17,8 +17,8 @@ internal static WqlEventQuery ToWqlQuery(this ObservationTarget targetState) "select * from __InstanceDeletionEvent within 1 where TargetInstance isa 'Win32_Process'"), _ => throw new ArgumentOutOfRangeException( - nameof(targetState), - targetState, - $"Process state {targetState} is not supported") + nameof(observationTarget), + observationTarget, + $"Process state {observationTarget} is not supported") }; } diff --git a/ProcessDoctor.Backend.Windows/WMI/Interfaces/IManagementEventWatcherFactory.cs b/ProcessDoctor.Backend.Windows/WMI/Interfaces/IManagementEventWatcherFactory.cs index befd0d7..07f7737 100644 --- a/ProcessDoctor.Backend.Windows/WMI/Interfaces/IManagementEventWatcherFactory.cs +++ b/ProcessDoctor.Backend.Windows/WMI/Interfaces/IManagementEventWatcherFactory.cs @@ -4,5 +4,5 @@ namespace ProcessDoctor.Backend.Windows.WMI.Interfaces; public interface IManagementEventWatcherFactory { - IManagementEventWatcher Create(ObservationTarget targetState); + IManagementEventWatcher Create(ObservationTarget observationTarget); } diff --git a/ProcessDoctor.Backend.Windows/WMI/ManagementEventWatcherAdapterFactory.cs b/ProcessDoctor.Backend.Windows/WMI/ManagementEventWatcherAdapterFactory.cs index 120cc63..060737d 100644 --- a/ProcessDoctor.Backend.Windows/WMI/ManagementEventWatcherAdapterFactory.cs +++ b/ProcessDoctor.Backend.Windows/WMI/ManagementEventWatcherAdapterFactory.cs @@ -8,8 +8,8 @@ namespace ProcessDoctor.Backend.Windows.WMI; public sealed class ManagementEventWatcherAdapterFactory : IManagementEventWatcherFactory { /// - public IManagementEventWatcher Create(ObservationTarget targetState) + public IManagementEventWatcher Create(ObservationTarget observationTarget) => new ManagementEventWatcherAdapter( Log.GetLog(), - targetState.ToWqlQuery()); + observationTarget.ToWqlQuery()); } From cea8a29e18c212b380bea4b79e417c5b1bb1d37c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0=20=D0=92=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8=D0=BA=D0=B8=D0=B9?= Date: Fri, 15 Mar 2024 19:32:56 +0500 Subject: [PATCH 4/8] (#8) Implement ProcessProvider for Linux --- .../{ => Fakes}/FakeProcessEntry.cs | 2 +- .../{ => Fakes}/FakeProcessStatus.cs | 2 +- .../Fixtures/ProcFolderFixture.cs | 13 ++ .../{ => Fixtures}/ProcessFixture.cs | 14 +- .../LinuxProcessTests.cs | 1 + .../ProcFileSystemFixture.cs | 10 - .../ProcTests/ProcessEntryTests.cs | 20 +- .../ProcTests/ProcessStatusTests.cs | 21 +- .../ProcessListSnapshotTests.cs | 79 +++++++ .../ProcessProviderTests.cs | 197 ++++++++++++++++++ ProcessDoctor.Backend.Linux/LinuxProcess.cs | 4 +- .../Proc/Interfaces/IProcFolderEntry.cs | 8 + .../Proc/ProcFolderEntry.cs | 15 ++ .../ProcessListSnapshot.cs | 21 ++ .../ProcessProvider.cs | 79 +++++++ ProcessDoctor/App.axaml.cs | 11 +- ProcessDoctor/ProcessDoctor.csproj | 1 + ProcessDoctor/ProcessProviderFactory.cs | 30 +++ .../ViewModels/MainWindowViewModel.cs | 8 +- 19 files changed, 484 insertions(+), 52 deletions(-) rename ProcessDoctor.Backend.Linux.Tests/{ => Fakes}/FakeProcessEntry.cs (94%) rename ProcessDoctor.Backend.Linux.Tests/{ => Fakes}/FakeProcessStatus.cs (93%) create mode 100644 ProcessDoctor.Backend.Linux.Tests/Fixtures/ProcFolderFixture.cs rename ProcessDoctor.Backend.Linux.Tests/{ => Fixtures}/ProcessFixture.cs (89%) delete mode 100644 ProcessDoctor.Backend.Linux.Tests/ProcFileSystemFixture.cs create mode 100644 ProcessDoctor.Backend.Linux.Tests/ProcessListSnapshotTests.cs create mode 100644 ProcessDoctor.Backend.Linux.Tests/ProcessProviderTests.cs create mode 100644 ProcessDoctor.Backend.Linux/Proc/Interfaces/IProcFolderEntry.cs create mode 100644 ProcessDoctor.Backend.Linux/Proc/ProcFolderEntry.cs create mode 100644 ProcessDoctor.Backend.Linux/ProcessListSnapshot.cs create mode 100644 ProcessDoctor.Backend.Linux/ProcessProvider.cs create mode 100644 ProcessDoctor/ProcessProviderFactory.cs diff --git a/ProcessDoctor.Backend.Linux.Tests/FakeProcessEntry.cs b/ProcessDoctor.Backend.Linux.Tests/Fakes/FakeProcessEntry.cs similarity index 94% rename from ProcessDoctor.Backend.Linux.Tests/FakeProcessEntry.cs rename to ProcessDoctor.Backend.Linux.Tests/Fakes/FakeProcessEntry.cs index b80304d..6333787 100644 --- a/ProcessDoctor.Backend.Linux.Tests/FakeProcessEntry.cs +++ b/ProcessDoctor.Backend.Linux.Tests/Fakes/FakeProcessEntry.cs @@ -1,6 +1,6 @@ using ProcessDoctor.Backend.Linux.Proc.Interfaces; -namespace ProcessDoctor.Backend.Linux.Tests; +namespace ProcessDoctor.Backend.Linux.Tests.Fakes; public sealed class FakeProcessEntry : IProcessEntry { diff --git a/ProcessDoctor.Backend.Linux.Tests/FakeProcessStatus.cs b/ProcessDoctor.Backend.Linux.Tests/Fakes/FakeProcessStatus.cs similarity index 93% rename from ProcessDoctor.Backend.Linux.Tests/FakeProcessStatus.cs rename to ProcessDoctor.Backend.Linux.Tests/Fakes/FakeProcessStatus.cs index 5e35c20..61729b6 100644 --- a/ProcessDoctor.Backend.Linux.Tests/FakeProcessStatus.cs +++ b/ProcessDoctor.Backend.Linux.Tests/Fakes/FakeProcessStatus.cs @@ -1,7 +1,7 @@ using ProcessDoctor.Backend.Linux.Proc.Interfaces; using ProcessDoctor.Backend.Linux.Proc.StatusFile.Enums; -namespace ProcessDoctor.Backend.Linux.Tests; +namespace ProcessDoctor.Backend.Linux.Tests.Fakes; public sealed class FakeProcessStatus : IProcessStatus { diff --git a/ProcessDoctor.Backend.Linux.Tests/Fixtures/ProcFolderFixture.cs b/ProcessDoctor.Backend.Linux.Tests/Fixtures/ProcFolderFixture.cs new file mode 100644 index 0000000..acad4c1 --- /dev/null +++ b/ProcessDoctor.Backend.Linux.Tests/Fixtures/ProcFolderFixture.cs @@ -0,0 +1,13 @@ +using System.IO.Abstractions.TestingHelpers; +using JetBrains.Annotations; + +namespace ProcessDoctor.Backend.Linux.Tests.Fixtures; + +[UsedImplicitly] +public sealed class ProcFolderFixture +{ + public MockFileSystem FileSystem { get; } = new(); + + public ProcessFixture CreateProcess(uint id) + => new(FileSystem, id); +} diff --git a/ProcessDoctor.Backend.Linux.Tests/ProcessFixture.cs b/ProcessDoctor.Backend.Linux.Tests/Fixtures/ProcessFixture.cs similarity index 89% rename from ProcessDoctor.Backend.Linux.Tests/ProcessFixture.cs rename to ProcessDoctor.Backend.Linux.Tests/Fixtures/ProcessFixture.cs index b46f2f1..41abf3f 100644 --- a/ProcessDoctor.Backend.Linux.Tests/ProcessFixture.cs +++ b/ProcessDoctor.Backend.Linux.Tests/Fixtures/ProcessFixture.cs @@ -2,26 +2,24 @@ using System.IO.Abstractions.TestingHelpers; using ProcessDoctor.Backend.Linux.Proc; -namespace ProcessDoctor.Backend.Linux.Tests; +namespace ProcessDoctor.Backend.Linux.Tests.Fixtures; public sealed class ProcessFixture { public IDirectoryInfo Directory { get; } - + public IFileInfo CommandLineFile { get; } public IFileInfo ExecutablePathFile { get; } public IFileInfo StatusFile { get; } - - public ProcessFixture(uint id) - { - var fileSystem = new MockFileSystem(); + public ProcessFixture(MockFileSystem fileSystem, uint id) + { var directoryPath = fileSystem.Path.Combine(ProcPaths.Path, id.ToString()); var processDirectory = fileSystem.DirectoryInfo.New(directoryPath); fileSystem.AddDirectory(directoryPath); - + var exePath = fileSystem.Path.Combine(processDirectory.FullName, ProcPaths.ExecutablePath.FileName); var exeFile = fileSystem.FileInfo.New(exePath); fileSystem.AddEmptyFile(exeFile); @@ -33,7 +31,7 @@ public ProcessFixture(uint id) var statusPath = fileSystem.Path.Combine(processDirectory.FullName, ProcPaths.Status.FileName); var statusFile = fileSystem.FileInfo.New(statusPath); fileSystem.AddEmptyFile(statusFile); - + Directory = processDirectory; CommandLineFile = commandLineFile; ExecutablePathFile = exeFile; diff --git a/ProcessDoctor.Backend.Linux.Tests/LinuxProcessTests.cs b/ProcessDoctor.Backend.Linux.Tests/LinuxProcessTests.cs index deed5cd..44bcac0 100644 --- a/ProcessDoctor.Backend.Linux.Tests/LinuxProcessTests.cs +++ b/ProcessDoctor.Backend.Linux.Tests/LinuxProcessTests.cs @@ -1,5 +1,6 @@ using System.Runtime.InteropServices; using FluentAssertions; +using ProcessDoctor.Backend.Linux.Tests.Fakes; namespace ProcessDoctor.Backend.Linux.Tests; diff --git a/ProcessDoctor.Backend.Linux.Tests/ProcFileSystemFixture.cs b/ProcessDoctor.Backend.Linux.Tests/ProcFileSystemFixture.cs deleted file mode 100644 index d447a56..0000000 --- a/ProcessDoctor.Backend.Linux.Tests/ProcFileSystemFixture.cs +++ /dev/null @@ -1,10 +0,0 @@ -using JetBrains.Annotations; - -namespace ProcessDoctor.Backend.Linux.Tests; - -[UsedImplicitly] -public sealed class ProcFileSystemFixture -{ - public ProcessFixture CreateProcess(uint id) - => new(id); -} diff --git a/ProcessDoctor.Backend.Linux.Tests/ProcTests/ProcessEntryTests.cs b/ProcessDoctor.Backend.Linux.Tests/ProcTests/ProcessEntryTests.cs index 253d58d..4a4def8 100644 --- a/ProcessDoctor.Backend.Linux.Tests/ProcTests/ProcessEntryTests.cs +++ b/ProcessDoctor.Backend.Linux.Tests/ProcTests/ProcessEntryTests.cs @@ -1,11 +1,12 @@ -using System.IO.Abstractions.TestingHelpers; using FluentAssertions; using ProcessDoctor.Backend.Linux.Proc; using ProcessDoctor.Backend.Linux.Proc.Exceptions; +using ProcessDoctor.Backend.Linux.Proc.StatusFile; +using ProcessDoctor.Backend.Linux.Tests.Fixtures; namespace ProcessDoctor.Backend.Linux.Tests.ProcTests; -public sealed class ProcessEntryTests(ProcFileSystemFixture procFileSystem) : IClassFixture +public sealed class ProcessEntryTests(ProcFolderFixture procFolderFixture) : IClassFixture { [Theory] [InlineData("dir")] @@ -15,7 +16,8 @@ public sealed class ProcessEntryTests(ProcFileSystemFixture procFileSystem) : IC public void Should_throw_exception_if_directory_is_not_process(string directoryName) { // Arrange - var processDirectory = new MockFileSystem() + var processDirectory = procFolderFixture + .FileSystem .DirectoryInfo .New(directoryName); @@ -32,7 +34,7 @@ public void Should_throw_exception_if_directory_is_not_process(string directoryN public void Should_read_process_id_properly(uint expectedId) { // Arrange & Act - var process = procFileSystem.CreateProcess(expectedId); + var process = procFolderFixture.CreateProcess(expectedId); var sut = ProcessEntry.Create(process.Directory); // Assert @@ -47,7 +49,7 @@ public void Should_read_process_id_properly(uint expectedId) public void Should_read_process_command_line_properly(string expectedCommandLine) { // Arrange - var process = procFileSystem.CreateProcess(123u); + var process = procFolderFixture.CreateProcess(123u); using (var writer = process.CommandLineFile.CreateText()) writer.Write(expectedCommandLine); @@ -64,7 +66,7 @@ public void Should_read_process_command_line_properly(string expectedCommandLine public void Command_line_should_be_null_if_file_is_empty() { // Arrange - var process = procFileSystem.CreateProcess(123u); + var process = procFolderFixture.CreateProcess(123u); // Act var sut = ProcessEntry.Create(process.Directory); @@ -79,7 +81,7 @@ public void Command_line_should_be_null_if_file_is_empty() public void Should_read_process_status_section_properly() { // Arrange & Act - var process = procFileSystem.CreateProcess(123u); + var process = procFolderFixture.CreateProcess(123u); using (var writer = process.StatusFile.CreateText()) writer.Write( """ @@ -93,6 +95,8 @@ public void Should_read_process_status_section_properly() // Assert sut.Status .Should() - .NotBeNull(); + .NotBeNull() + .And + .BeOfType(); } } diff --git a/ProcessDoctor.Backend.Linux.Tests/ProcTests/ProcessStatusTests.cs b/ProcessDoctor.Backend.Linux.Tests/ProcTests/ProcessStatusTests.cs index b1e4184..199a0e3 100644 --- a/ProcessDoctor.Backend.Linux.Tests/ProcTests/ProcessStatusTests.cs +++ b/ProcessDoctor.Backend.Linux.Tests/ProcTests/ProcessStatusTests.cs @@ -1,12 +1,12 @@ -using System.IO.Abstractions.TestingHelpers; using FluentAssertions; using ProcessDoctor.Backend.Linux.Proc.Exceptions; using ProcessDoctor.Backend.Linux.Proc.StatusFile; using ProcessDoctor.Backend.Linux.Proc.StatusFile.Enums; +using ProcessDoctor.Backend.Linux.Tests.Fixtures; namespace ProcessDoctor.Backend.Linux.Tests.ProcTests; -public sealed class ProcessStatusTests(ProcFileSystemFixture procFileSystem) : IClassFixture +public sealed class ProcessStatusTests(ProcFolderFixture procFolderFixture) : IClassFixture { [Theory] [InlineData("stat")] @@ -16,7 +16,8 @@ public sealed class ProcessStatusTests(ProcFileSystemFixture procFileSystem) : I public void Should_throw_exception_if_status_file_name_is_invalid(string statusFileName) { // Arrange - var statusFile = new MockFileSystem() + var statusFile = procFolderFixture + .FileSystem .FileInfo .New(statusFileName); @@ -32,7 +33,7 @@ public void Should_throw_exception_if_status_file_name_is_invalid(string statusF public void Should_read_name_properly(string expectedName) { // Arrange - var process = procFileSystem.CreateProcess(123u); + var process = procFolderFixture.CreateProcess(123u); using (var writer = process.StatusFile.CreateText()) writer.Write( $""" @@ -53,7 +54,7 @@ public void Should_read_name_properly(string expectedName) public void Should_throw_exception_if_name_is_invalid() { // Arrange - var process = procFileSystem.CreateProcess(123u); + var process = procFolderFixture.CreateProcess(123u); using (var writer = process.StatusFile.CreateText()) writer.Write( """ @@ -76,7 +77,7 @@ public void Should_throw_exception_if_name_is_invalid() public void Should_read_parent_id_properly(uint expectedParentId) { // Arrange - var process = procFileSystem.CreateProcess(123u); + var process = procFolderFixture.CreateProcess(123u); using (var writer = process.StatusFile.CreateText()) writer.Write( $""" @@ -101,7 +102,7 @@ public void Should_read_parent_id_properly(uint expectedParentId) public void Parent_id_should_be_null_if_value_was_zero() { // Arrange - var process = procFileSystem.CreateProcess(123u); + var process = procFolderFixture.CreateProcess(123u); using (var writer = process.StatusFile.CreateText()) writer.Write( """ @@ -129,7 +130,7 @@ public void Parent_id_should_be_null_if_value_was_zero() public void Should_throw_exception_if_parent_id_is_invalid(string expectedParentId) { // Arrange - var process = procFileSystem.CreateProcess(123u); + var process = procFolderFixture.CreateProcess(123u); using (var writer = process.StatusFile.CreateText()) writer.Write( $""" @@ -159,7 +160,7 @@ public void Should_throw_exception_if_parent_id_is_invalid(string expectedParent public void Should_read_state_properly(string rawState, ProcessState expectedState) { // Arrange - var process = procFileSystem.CreateProcess(123u); + var process = procFolderFixture.CreateProcess(123u); using (var writer = process.StatusFile.CreateText()) writer.Write( $""" @@ -180,7 +181,7 @@ public void Should_read_state_properly(string rawState, ProcessState expectedSta public void Should_throw_exception_if_state_is_invalid() { // Arrange - var process = procFileSystem.CreateProcess(123u); + var process = procFolderFixture.CreateProcess(123u); using (var writer = process.StatusFile.CreateText()) writer.Write( """ diff --git a/ProcessDoctor.Backend.Linux.Tests/ProcessListSnapshotTests.cs b/ProcessDoctor.Backend.Linux.Tests/ProcessListSnapshotTests.cs new file mode 100644 index 0000000..9525c18 --- /dev/null +++ b/ProcessDoctor.Backend.Linux.Tests/ProcessListSnapshotTests.cs @@ -0,0 +1,79 @@ +using FluentAssertions; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using ProcessDoctor.Backend.Linux.Proc.Interfaces; +using ProcessDoctor.Backend.Linux.Tests.Fixtures; +using ProcessDoctor.TestFramework; +using ProcessDoctor.TestFramework.Logging; +using Xunit.Abstractions; + +namespace ProcessDoctor.Backend.Linux.Tests; + +public sealed class ProcessListSnapshotTests(ProcFolderFixture procFolderFixture, ITestOutputHelper output) : IClassFixture +{ + [Fact] + public void Should_returns_empty_snapshot_if_any_error_occured() + { + // Arrange + using var logger = new ThrowingLoggerAdapter(nameof(ProcessListSnapshotTests), output); + var procFolderEntry = Substitute.For(); + + procFolderEntry + .EnumerateProcessDirectories() + .Throws(); + + var sut = new ProcessListSnapshot(logger, procFolderEntry); + + // Act & Assert + sut.EnumerateProcesses() + .Should() + .BeEmpty(); + } + + [Fact] + public void Should_returns_snapshot_properly() + { + // Arrange + using var logger = new ThrowingLoggerAdapter(nameof(ProcessListSnapshotTests), output); + var procFolderEntry = Substitute.For(); + + var expectedProcesses = new[] + { + 123u, + 567u, + 879u + }; + + var processDirectories = expectedProcesses + .Select(id => + { + var processFixture = procFolderFixture.CreateProcess(id); + using (var writer = processFixture.StatusFile.CreateText()) + writer.Write( + $""" + Name: ProcessDoctor + ... + State: R + ... + ... + Pid: {id} + PPid: 1 + """); + + return processFixture.Directory; + }) + .ToArray(); + + procFolderEntry + .EnumerateProcessDirectories() + .Returns(processDirectories); + + var sut = new ProcessListSnapshot(logger, procFolderEntry); + + // Act & Assert + sut.EnumerateProcesses() + .Select(process => process.Id) + .Should() + .ContainInOrder(expectedProcesses); + } +} diff --git a/ProcessDoctor.Backend.Linux.Tests/ProcessProviderTests.cs b/ProcessDoctor.Backend.Linux.Tests/ProcessProviderTests.cs new file mode 100644 index 0000000..474051d --- /dev/null +++ b/ProcessDoctor.Backend.Linux.Tests/ProcessProviderTests.cs @@ -0,0 +1,197 @@ +using System.IO.Abstractions; +using FluentAssertions; +using Microsoft.Reactive.Testing; +using NSubstitute; +using NSubstitute.ReceivedExtensions; +using ProcessDoctor.Backend.Core; +using ProcessDoctor.Backend.Core.Enums; +using ProcessDoctor.Backend.Linux.Proc.Interfaces; +using ProcessDoctor.Backend.Linux.Tests.Fixtures; +using ProcessDoctor.TestFramework; +using ProcessDoctor.TestFramework.Extensions; +using ProcessDoctor.TestFramework.Logging; +using Xunit.Abstractions; + +namespace ProcessDoctor.Backend.Linux.Tests; + +public sealed class ProcessProviderTests(ProcFolderFixture procFolderFixture, ITestOutputHelper output) : IClassFixture +{ + [Theory] + [InlineData(ObservationTarget.Launched)] + [InlineData(ObservationTarget.Terminated)] + public void Should_restart_observing_if_error_occurrs(ObservationTarget observationTarget) + { + // Arrange + using var logger = new ThrowingLoggerAdapter(nameof(ProcessProviderTests), output); + var procFolderEntry = Substitute.For(); + + procFolderEntry + .EnumerateProcessDirectories() + .Returns( + _ => Enumerable.Empty(), + _ => throw new FakeException(), + _ => Enumerable.Empty()); + + var testObserver = new TestScheduler() + .CreateObserver(); + + var sut = new ProcessProvider(logger, procFolderEntry); + + // Act + sut.ObserveProcesses(observationTarget) + .Subscribe(testObserver) + .Dispose(); + + // Assert + /* + * This is an indirect assertion. + * First, a snapshot is taken (this is the first call). + * Then an error occurs (this is the second call). + * Next, the observing is restarted and the new snapshot is taken (this is the third call). + * Finally, the processes in the loop are retrieved once and subscription is disposed (this is the fourth call). + * + * This test can be improved in the future. + */ + procFolderEntry + .Received(Quantity.Exactly(number: 4)) + .EnumerateProcessDirectories(); + } + + [Fact] + public void Should_observe_launched_processes_properly() + { + // Arrange + using var logger = new ThrowingLoggerAdapter(nameof(ProcessProviderTests), output); + var procFolderEntry = Substitute.For(); + + var expectedProcesses = new[] + { + 123u, + 567u, + 879u + }; + + var processDirectories = expectedProcesses + .Select(id => + { + var processFixture = procFolderFixture.CreateProcess(id); + using (var writer = processFixture.StatusFile.CreateText()) + writer.Write( + $""" + Name: ProcessDoctor + ... + State: R + ... + ... + Pid: {id} + PPid: 1 + """); + + return processFixture.Directory; + }) + .ToArray(); + + procFolderEntry + .EnumerateProcessDirectories() + .Returns( + Enumerable.Empty(), + processDirectories); + + var testObserver = new TestScheduler() + .CreateObserver(); + + var sut = new ProcessProvider(logger, procFolderEntry); + + // Act + using var _ = sut.ObserveProcesses(ObservationTarget.Launched) + .Subscribe(testObserver); + + // Assert + testObserver + .EnumerateMessages() + .Select(process => process.Id) + .Should() + .ContainInOrder(expectedProcesses); + } + + [Fact] + public void Should_observe_terminated_processes_properly() + { + // Arrange + using var logger = new ThrowingLoggerAdapter(nameof(ProcessProviderTests), output); + var procFolderEntry = Substitute.For(); + + var expectedProcesses = new[] + { + 567u, + 879u + }; + + var processDirectories = new[] + { + 123u + } + .Concat(expectedProcesses) + .Select(id => + { + var processFixture = procFolderFixture.CreateProcess(id); + using (var writer = processFixture.StatusFile.CreateText()) + writer.Write( + $""" + Name: ProcessDoctor + ... + State: R + ... + ... + Pid: {id} + PPid: 1 + """); + + return processFixture.Directory; + }) + .ToArray(); + + procFolderEntry + .EnumerateProcessDirectories() + .Returns( + processDirectories, + processDirectories.ExceptBy( + expectedProcesses, + processDirectory => uint.Parse(processDirectory.Name))); + + var testObserver = new TestScheduler() + .CreateObserver(); + + var sut = new ProcessProvider(logger, procFolderEntry); + + // Act + using var _ = sut.ObserveProcesses(ObservationTarget.Terminated) + .Subscribe(testObserver); + + // Assert + testObserver + .EnumerateMessages() + .Select(process => process.Id) + .Should() + .ContainInOrder(expectedProcesses); + } + + [Fact] + public void Should_create_process_snapshot_list_properly() + { + // Arrange + using var logger = new ThrowingLoggerAdapter(nameof(ProcessListSnapshotTests), output); + var procFolderEntry = Substitute.For(); + var sut = new ProcessProvider(logger, procFolderEntry); + + // Act + var snapshot = sut.CreateSnapshot(); + + // Assert + snapshot + .Should() + .NotBeNull() + .And + .BeOfType(); + } +} diff --git a/ProcessDoctor.Backend.Linux/LinuxProcess.cs b/ProcessDoctor.Backend.Linux/LinuxProcess.cs index b729e97..ca72a5f 100644 --- a/ProcessDoctor.Backend.Linux/LinuxProcess.cs +++ b/ProcessDoctor.Backend.Linux/LinuxProcess.cs @@ -41,7 +41,7 @@ public override SKBitmap ExtractIcon() using var iconMetadata = iconTheme.LookupIcon( fileMetadata.Icon, - size: 32, + size: 16, IconLookupFlags.UseBuiltin); return SKBitmap.Decode(iconMetadata.Filename); @@ -53,7 +53,7 @@ private SKBitmap ExtractStockIcon(IconTheme iconTheme) using var iconMetadata = iconTheme.LookupIcon( icon, - size: 32, + size: 16, IconLookupFlags.UseBuiltin); return SKBitmap.Decode(iconMetadata.Filename); diff --git a/ProcessDoctor.Backend.Linux/Proc/Interfaces/IProcFolderEntry.cs b/ProcessDoctor.Backend.Linux/Proc/Interfaces/IProcFolderEntry.cs new file mode 100644 index 0000000..fc1e864 --- /dev/null +++ b/ProcessDoctor.Backend.Linux/Proc/Interfaces/IProcFolderEntry.cs @@ -0,0 +1,8 @@ +using System.IO.Abstractions; + +namespace ProcessDoctor.Backend.Linux.Proc.Interfaces; + +public interface IProcFolderEntry +{ + public IEnumerable EnumerateProcessDirectories(); +} diff --git a/ProcessDoctor.Backend.Linux/Proc/ProcFolderEntry.cs b/ProcessDoctor.Backend.Linux/Proc/ProcFolderEntry.cs new file mode 100644 index 0000000..82c0198 --- /dev/null +++ b/ProcessDoctor.Backend.Linux/Proc/ProcFolderEntry.cs @@ -0,0 +1,15 @@ +using System.IO.Abstractions; +using ProcessDoctor.Backend.Linux.Proc.Extensions; +using ProcessDoctor.Backend.Linux.Proc.Interfaces; + +namespace ProcessDoctor.Backend.Linux.Proc; + +public sealed class ProcFolderEntry(IFileSystem fileSystem) : IProcFolderEntry +{ + public IEnumerable EnumerateProcessDirectories() + => fileSystem + .Directory + .EnumerateDirectories(ProcPaths.Path) + .Select(directoryPath => fileSystem.DirectoryInfo.New(directoryPath)) + .Where(DirectoryInfoExtensions.IsProcess); +} diff --git a/ProcessDoctor.Backend.Linux/ProcessListSnapshot.cs b/ProcessDoctor.Backend.Linux/ProcessListSnapshot.cs new file mode 100644 index 0000000..0d8be16 --- /dev/null +++ b/ProcessDoctor.Backend.Linux/ProcessListSnapshot.cs @@ -0,0 +1,21 @@ +using JetBrains.Diagnostics; +using ProcessDoctor.Backend.Core; +using ProcessDoctor.Backend.Core.Interfaces; +using ProcessDoctor.Backend.Linux.Proc; +using ProcessDoctor.Backend.Linux.Proc.Interfaces; + +namespace ProcessDoctor.Backend.Linux; + +public sealed class ProcessListSnapshot(ILog logger, IProcFolderEntry procFolderEntry) : IProcessListSnapshot +{ + public IEnumerable EnumerateProcesses() + => logger.Catch(() => + procFolderEntry + .EnumerateProcessDirectories() + .Select(ProcessEntry.Create) + .Select(LinuxProcess.Create)) + ?? Enumerable.Empty(); + + public void Dispose() + { } +} diff --git a/ProcessDoctor.Backend.Linux/ProcessProvider.cs b/ProcessDoctor.Backend.Linux/ProcessProvider.cs new file mode 100644 index 0000000..fe2d1fc --- /dev/null +++ b/ProcessDoctor.Backend.Linux/ProcessProvider.cs @@ -0,0 +1,79 @@ +using System.Reactive.Linq; +using JetBrains.Diagnostics; +using ProcessDoctor.Backend.Core; +using ProcessDoctor.Backend.Core.Enums; +using ProcessDoctor.Backend.Core.Interfaces; +using ProcessDoctor.Backend.Linux.Proc; +using ProcessDoctor.Backend.Linux.Proc.Interfaces; + +namespace ProcessDoctor.Backend.Linux; + +public sealed class ProcessProvider(ILog logger, IProcFolderEntry procFolderEntry) : IProcessProvider +{ + public IObservable ObserveProcesses(ObservationTarget observationTarget) + { + var cachedProcesses = CreateSnapshot() + .EnumerateProcesses() + .ToDictionary(process => process.Id); + + logger.Info( + "File system event watcher has been started. Event type: {0}", + observationTarget); + + return Observable + .Create(async (observer, token) => + { + token.Register(observer.OnCompleted); + + var timer = new PeriodicTimer(TimeSpan.FromSeconds(0.5)); + + while (!token.IsCancellationRequested) + { + var processes = procFolderEntry + .EnumerateProcessDirectories() + .ToDictionary(processDirectory => uint.Parse(processDirectory.Name)); + + foreach (var launchedProcess in processes.Where(pair => !cachedProcesses.ContainsKey(pair.Key))) + { + var processEntry = ProcessEntry.Create(launchedProcess.Value); + var linuxProcess = LinuxProcess.Create(processEntry); + + cachedProcesses.Add(linuxProcess.Id, linuxProcess); + + if (observationTarget is ObservationTarget.Launched) + { + observer.OnNext(linuxProcess); + } + } + + var terminatedProcesses = cachedProcesses + .Where(cachedProcess => !processes.ContainsKey(cachedProcess.Key)) + .ToArray(); + + foreach (var terminatedProcess in terminatedProcesses) + { + cachedProcesses.Remove(terminatedProcess.Key); + + if (observationTarget is ObservationTarget.Terminated) + { + observer.OnNext(terminatedProcess.Value); + } + } + + await timer.WaitForNextTickAsync(token); + } + }) + .Finally(() => logger.Info("File system event watcher has been stopped. Event type: {0}", observationTarget)) + .Catch((Exception exception) => + { + logger.Error(exception, "An error occurred while processing an event received from the file system"); + + return ObserveProcesses(observationTarget); + }); + } + + public IProcessListSnapshot CreateSnapshot() + => new ProcessListSnapshot( + Log.GetLog(), + procFolderEntry); +} diff --git a/ProcessDoctor/App.axaml.cs b/ProcessDoctor/App.axaml.cs index f1b10a5..d928a88 100644 --- a/ProcessDoctor/App.axaml.cs +++ b/ProcessDoctor/App.axaml.cs @@ -4,8 +4,6 @@ using JetBrains.Diagnostics; using JetBrains.Lifetimes; using ProcessDoctor.Backend.Core; -using ProcessDoctor.Backend.Windows; -using ProcessDoctor.Backend.Windows.WMI; using ProcessDoctor.ViewModels; using ProcessDoctor.Views; @@ -21,12 +19,13 @@ public override void Initialize() public override void OnFrameworkInitializationCompleted() { var lifetime = CreateAppLifetime(); + + var processProvider = new ProcessProviderFactory() + .Create(Lifetime.Eternal); + var backend = new ProcessMonitor( Log.GetLog(), - new ProcessProvider( - Lifetime.Eternal, - Log.GetLog(), - new ManagementEventWatcherAdapterFactory())); + processProvider); if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { diff --git a/ProcessDoctor/ProcessDoctor.csproj b/ProcessDoctor/ProcessDoctor.csproj index 7b15dc5..dbc8b68 100644 --- a/ProcessDoctor/ProcessDoctor.csproj +++ b/ProcessDoctor/ProcessDoctor.csproj @@ -24,5 +24,6 @@ + diff --git a/ProcessDoctor/ProcessProviderFactory.cs b/ProcessDoctor/ProcessProviderFactory.cs new file mode 100644 index 0000000..c4e9978 --- /dev/null +++ b/ProcessDoctor/ProcessProviderFactory.cs @@ -0,0 +1,30 @@ +using System; +using System.IO.Abstractions; +using System.Runtime.InteropServices; +using JetBrains.Diagnostics; +using JetBrains.Lifetimes; +using ProcessDoctor.Backend.Core.Interfaces; +using ProcessDoctor.Backend.Linux.Proc; +using ProcessDoctor.Backend.Windows.WMI; + +namespace ProcessDoctor; + +public sealed class ProcessProviderFactory +{ + public IProcessProvider Create(Lifetime lifetime) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return new Backend.Windows.ProcessProvider( + lifetime, + Log.GetLog(), + new ManagementEventWatcherAdapterFactory()); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + return new Backend.Linux.ProcessProvider( + Log.GetLog(), + new ProcFolderEntry( + new FileSystem())); + + throw new PlatformNotSupportedException(); + } +} diff --git a/ProcessDoctor/ViewModels/MainWindowViewModel.cs b/ProcessDoctor/ViewModels/MainWindowViewModel.cs index 523d516..0d7d82b 100644 --- a/ProcessDoctor/ViewModels/MainWindowViewModel.cs +++ b/ProcessDoctor/ViewModels/MainWindowViewModel.cs @@ -8,8 +8,6 @@ using JetBrains.Lifetimes; using ProcessDoctor.Backend.Core; using ProcessDoctor.Backend.Core.Interfaces; -using ProcessDoctor.Backend.Windows; -using ProcessDoctor.Backend.Windows.WMI; using SkiaImageView; namespace ProcessDoctor.ViewModels; @@ -21,10 +19,8 @@ public class MainWindowViewModel : ViewModelBase Log.GetLog(), new ProcessMonitor( Log.GetLog(), - new ProcessProvider( - Lifetime.Eternal, - Log.GetLog(), - new ManagementEventWatcherAdapterFactory()))); + new ProcessProviderFactory() + .Create(Lifetime.Eternal))); public HierarchicalTreeDataGridSource ItemSource { get; } From ca6de794f33df5f6fb5c1f99efe0d34d97702c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0=20=D0=92=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8=D0=BA=D0=B8=D0=B9?= Date: Fri, 15 Mar 2024 20:38:31 +0500 Subject: [PATCH 5/8] (#8) Add icon display for installed applications --- ProcessDoctor.Backend.Linux/LinuxProcess.cs | 25 ++++++++++++++------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/ProcessDoctor.Backend.Linux/LinuxProcess.cs b/ProcessDoctor.Backend.Linux/LinuxProcess.cs index ca72a5f..212c992 100644 --- a/ProcessDoctor.Backend.Linux/LinuxProcess.cs +++ b/ProcessDoctor.Backend.Linux/LinuxProcess.cs @@ -32,19 +32,28 @@ public override SKBitmap ExtractIcon() return ExtractStockIcon(iconTheme); } - var file = FileFactory.NewForPath(ExecutablePath); + // TODO: Understand how to bind ExecutablePath and data from AppInfo + var application = AppInfoAdapter + .GetAll() + .FirstOrDefault(application => application.Executable.Contains(ExecutablePath)); - using var fileMetadata = file.QueryInfo( - IconAttributes.Standard, - FileQueryInfoFlags.None, - cancellable: null); + if (application is null) + { + return ExtractStockIcon(iconTheme); + } - using var iconMetadata = iconTheme.LookupIcon( - fileMetadata.Icon, + // TODO: Fix quality, color, size + using var icon = iconTheme.LookupIcon( + application.Icon, size: 16, IconLookupFlags.UseBuiltin); - return SKBitmap.Decode(iconMetadata.Filename); + using var buffer = icon.LoadIcon(); + + return SKBitmap.FromImage( + SKImage.FromPixels( + new SKImageInfo(width: 16, height: 16), + buffer.Pixels)); } private SKBitmap ExtractStockIcon(IconTheme iconTheme) From 0451c04f5160bc41ea3138c2856a9f8aa62131d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0=20=D0=92=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8=D0=BA=D0=B8=D0=B9?= Date: Sat, 16 Mar 2024 17:28:24 +0500 Subject: [PATCH 6/8] Replace RuntimeInformation usage with OperatingSystem --- ProcessDoctor.Backend.Linux.Tests/LinuxProcessTests.cs | 5 ++--- ProcessDoctor/ProcessProviderFactory.cs | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/ProcessDoctor.Backend.Linux.Tests/LinuxProcessTests.cs b/ProcessDoctor.Backend.Linux.Tests/LinuxProcessTests.cs index 44bcac0..490e4c3 100644 --- a/ProcessDoctor.Backend.Linux.Tests/LinuxProcessTests.cs +++ b/ProcessDoctor.Backend.Linux.Tests/LinuxProcessTests.cs @@ -1,4 +1,3 @@ -using System.Runtime.InteropServices; using FluentAssertions; using ProcessDoctor.Backend.Linux.Tests.Fakes; @@ -10,7 +9,7 @@ public sealed class LinuxProcessTests [InlineData("/usr/bin/htop")] public void Should_extract_icon(string executablePath) { - Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)); + Skip.IfNot(OperatingSystem.IsLinux()); // Arrange var processEntry = FakeProcessEntry.Create(id: 123u, executablePath: executablePath); @@ -29,7 +28,7 @@ public void Should_extract_icon(string executablePath) [SkippableFact] public void Should_extract_stock_icon() { - Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)); + Skip.IfNot(OperatingSystem.IsLinux()); // Arrange var processEntry = FakeProcessEntry.Create(id: 123u); diff --git a/ProcessDoctor/ProcessProviderFactory.cs b/ProcessDoctor/ProcessProviderFactory.cs index c4e9978..cf2c392 100644 --- a/ProcessDoctor/ProcessProviderFactory.cs +++ b/ProcessDoctor/ProcessProviderFactory.cs @@ -1,6 +1,5 @@ using System; using System.IO.Abstractions; -using System.Runtime.InteropServices; using JetBrains.Diagnostics; using JetBrains.Lifetimes; using ProcessDoctor.Backend.Core.Interfaces; @@ -13,13 +12,13 @@ public sealed class ProcessProviderFactory { public IProcessProvider Create(Lifetime lifetime) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (OperatingSystem.IsWindows()) return new Backend.Windows.ProcessProvider( lifetime, Log.GetLog(), new ManagementEventWatcherAdapterFactory()); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + if (OperatingSystem.IsLinux()) return new Backend.Linux.ProcessProvider( Log.GetLog(), new ProcFolderEntry( From 0eeea967ab98402e5e1b2c0844aa0edc3c30cbb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0=20=D0=92=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8=D0=BA=D0=B8=D0=B9?= Date: Sat, 16 Mar 2024 17:40:46 +0500 Subject: [PATCH 7/8] (#8) Add cache for status file properties --- .../Proc/StatusFile/ProcessStatus.cs | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/ProcessDoctor.Backend.Linux/Proc/StatusFile/ProcessStatus.cs b/ProcessDoctor.Backend.Linux/Proc/StatusFile/ProcessStatus.cs index cbeaa8f..4f67e50 100644 --- a/ProcessDoctor.Backend.Linux/Proc/StatusFile/ProcessStatus.cs +++ b/ProcessDoctor.Backend.Linux/Proc/StatusFile/ProcessStatus.cs @@ -9,6 +9,9 @@ public sealed class ProcessStatus : IProcessStatus { private const char Separator = ':'; private readonly string[] _lines; + private string? _cachedName; + private uint? _cachedParentId; + private ProcessState? _cachedState; public static ProcessStatus Create(IFileInfo statusFile) { @@ -24,13 +27,26 @@ public static ProcessStatus Create(IFileInfo statusFile) } public string Name - => ReadPropertyValue(StatusProperty.Name) - ?? throw new InvalidStatusFilePropertyException(StatusProperty.Name); + { + get + { + if (_cachedName is not null) + return _cachedName; + + var name = ReadPropertyValue(StatusProperty.Name) + ?? throw new InvalidStatusFilePropertyException(StatusProperty.Name); + + return _cachedName ??= name; + } + } public uint? ParentId { get { + if (_cachedParentId is not null) + return _cachedParentId; + var value = ReadPropertyValue(StatusProperty.ParentId); if (!uint.TryParse(value, out var parentId)) @@ -39,7 +55,7 @@ public uint? ParentId if (parentId is 0) return null; - return parentId; + return _cachedParentId ??= parentId; } } @@ -47,10 +63,13 @@ public ProcessState State { get { - var value = ReadPropertyValue(StatusProperty.State)? + if (_cachedState is not null) + return _cachedState.Value; + + var acronym = ReadPropertyValue(StatusProperty.State)? .First(); - var state = value switch + var state = acronym switch { 'R' => ProcessState.Running, 'S' => ProcessState.Sleeping, @@ -63,7 +82,7 @@ public ProcessState State if (state is null) throw new InvalidStatusFilePropertyException(StatusProperty.State); - return state.Value; + return _cachedState ??= state.Value; } } From 085d1cb16ca78a508d424553355a037cc0d7bbcb Mon Sep 17 00:00:00 2001 From: Friedrich von Never Date: Sat, 16 Mar 2024 17:05:42 +0100 Subject: [PATCH 8/8] (#26, #27,#28, #29) TODO: connect with the issues --- ProcessDoctor.Backend.Linux/LinuxProcess.cs | 4 ++-- ProcessDoctor.Backend.Linux/Proc/Native/LibC.cs | 2 +- ProcessDoctor.Tests/ProcessTreeViewModelTests.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ProcessDoctor.Backend.Linux/LinuxProcess.cs b/ProcessDoctor.Backend.Linux/LinuxProcess.cs index 212c992..c2a8f55 100644 --- a/ProcessDoctor.Backend.Linux/LinuxProcess.cs +++ b/ProcessDoctor.Backend.Linux/LinuxProcess.cs @@ -32,7 +32,7 @@ public override SKBitmap ExtractIcon() return ExtractStockIcon(iconTheme); } - // TODO: Understand how to bind ExecutablePath and data from AppInfo + // TODO[#27]: Understand how to bind ExecutablePath and data from AppInfo var application = AppInfoAdapter .GetAll() .FirstOrDefault(application => application.Executable.Contains(ExecutablePath)); @@ -42,7 +42,7 @@ public override SKBitmap ExtractIcon() return ExtractStockIcon(iconTheme); } - // TODO: Fix quality, color, size + // TODO[#28]: Fix quality, color, size using var icon = iconTheme.LookupIcon( application.Icon, size: 16, diff --git a/ProcessDoctor.Backend.Linux/Proc/Native/LibC.cs b/ProcessDoctor.Backend.Linux/Proc/Native/LibC.cs index 272f40e..047c1ed 100644 --- a/ProcessDoctor.Backend.Linux/Proc/Native/LibC.cs +++ b/ProcessDoctor.Backend.Linux/Proc/Native/LibC.cs @@ -7,7 +7,7 @@ internal static class LibC private const string Name = "libc"; [DllImport(Name, EntryPoint = "readlink", SetLastError = true)] - private static extern int NativeReadLink(string path, byte[] buffer, int bufferSize); // TODO: Make testable + private static extern int NativeReadLink(string path, byte[] buffer, int bufferSize); // TODO[#26]: Make testable /// /// The DllImportAttribute provides a SetLastError property diff --git a/ProcessDoctor.Tests/ProcessTreeViewModelTests.cs b/ProcessDoctor.Tests/ProcessTreeViewModelTests.cs index 5db7b39..e3eac67 100644 --- a/ProcessDoctor.Tests/ProcessTreeViewModelTests.cs +++ b/ProcessDoctor.Tests/ProcessTreeViewModelTests.cs @@ -8,7 +8,7 @@ namespace ProcessDoctor.Tests; -// TODO: Rewrite assertions using FluentAssertions +// TODO[#29]: Rewrite assertions using FluentAssertions public class ProcessTreeViewModelTests(ITestOutputHelper output) { [Fact]