From b605de8f26bcdb0a62f41f56eb108d53100a3f1a Mon Sep 17 00:00:00 2001 From: Christian Klutz Date: Wed, 16 Oct 2024 10:44:16 +0200 Subject: [PATCH] Add more unit tests (#22) --- src/LockCheck/ExceptionUtils.cs | 13 +- src/LockCheck/Linux/Extensions.cs | 20 ++- src/LockCheck/Linux/LockInfo.cs | 17 ++- src/LockCheck/Linux/NativeMethods.cs | 3 +- src/LockCheck/Linux/ProcFileSystem.cs | 63 ++++++++- src/LockCheck/Linux/ProcessInfo.Linux.cs | 10 +- src/LockCheck/LockCheck.csproj | 8 +- src/LockCheck/ProcessInfo.cs | 39 +++--- src/LockCheck/Windows/Extensions.cs | 32 +++-- src/LockCheck/Windows/NativeMethods.cs | 25 +++- src/LockCheck/Windows/NtDll.cs | 16 +-- src/LockCheck/Windows/NtException.cs | 19 --- src/LockCheck/Windows/Peb.cs | 25 +++- src/LockCheck/Windows/ProcessInfo.Windows.cs | 6 +- src/LockCheck/Windows/RestartManager.cs | 53 ++------ test/LockCheck.Tests/ExceptionUtilsTests.cs | 60 +++++++-- .../Linux/ProcFileSystemTests.cs | 28 ++++ test/LockCheck.Tests/LockCheck.Tests.csproj | 9 +- test/LockCheck.Tests/LockManagerTests.cs | 36 ++++- test/LockCheck.Tests/ProcessInfoTests.cs | 114 ++++++++++++++++ test/LockCheck.Tests/SupportdOSTestMethod.cs | 88 +++++++++++++ test/LockCheck.Tests/TestHelper.cs | 38 +++++- test/LockCheck.Tests/Windows/NtDllTests.cs | 123 ++++++++++++++++++ .../Windows/ProcessInfoWindowsTests.cs | 78 +++++++++++ .../Windows/RestartManagerTests.cs | 62 +++++++++ 25 files changed, 829 insertions(+), 156 deletions(-) delete mode 100644 src/LockCheck/Windows/NtException.cs create mode 100644 test/LockCheck.Tests/Linux/ProcFileSystemTests.cs create mode 100644 test/LockCheck.Tests/ProcessInfoTests.cs create mode 100644 test/LockCheck.Tests/SupportdOSTestMethod.cs create mode 100644 test/LockCheck.Tests/Windows/NtDllTests.cs create mode 100644 test/LockCheck.Tests/Windows/ProcessInfoWindowsTests.cs create mode 100644 test/LockCheck.Tests/Windows/RestartManagerTests.cs diff --git a/src/LockCheck/ExceptionUtils.cs b/src/LockCheck/ExceptionUtils.cs index 92db1f4..66d8492 100644 --- a/src/LockCheck/ExceptionUtils.cs +++ b/src/LockCheck/ExceptionUtils.cs @@ -1,8 +1,9 @@ -using System; +using System; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; +using static LockCheck.Windows.NativeMethods; #if NETFRAMEWORK using System.Reflection; #endif @@ -74,6 +75,9 @@ public static bool IsFileLocked(this IOException exception) /// public static bool RethrowWithLockingInformation(this Exception ex, string fileName, LockManagerFeatures features = default) { + if (fileName == null) + throw new ArgumentNullException(nameof(fileName)); + return RethrowWithLockingInformation(ex, [fileName], features); } @@ -111,7 +115,10 @@ public static bool RethrowWithLockingInformation(this Exception ex, string fileN /// public static bool RethrowWithLockingInformation(this Exception ex, string[] fileNames, LockManagerFeatures features = default, int? maxProcesses = null) { - if (fileNames?.Length > 0) + if (fileNames == null) + throw new ArgumentNullException(nameof(fileNames)); + + if (fileNames.Length > 0) { if (ex is IOException ioEx && ioEx.IsFileLocked()) { @@ -143,4 +150,4 @@ public static bool RethrowWithLockingInformation(this Exception ex, string[] fil return false; } } -} \ No newline at end of file +} diff --git a/src/LockCheck/Linux/Extensions.cs b/src/LockCheck/Linux/Extensions.cs index 52864ee..42ff86c 100644 --- a/src/LockCheck/Linux/Extensions.cs +++ b/src/LockCheck/Linux/Extensions.cs @@ -5,12 +5,28 @@ namespace LockCheck.Linux { internal static class Extensions { - public static bool IsFileLocked(IOException exception) + public static bool IsFileLocked(Exception exception) { if (exception == null) throw new ArgumentNullException(nameof(exception)); - return exception.HResult == NativeMethods.EWOULDBLOCK; + // Unix is... complicated. For EACCES, the runtime does throw a UnauthorizedAccessException, + // with potentially an inner exception of type IOException. + + if (exception is UnauthorizedAccessException ua && + ua.InnerException is IOException ioException) + { + return ioException.HResult == NativeMethods.EACCES; + } + + // EWOULDBLOCK is directly thrown as an IOException with the respective code. + + if (exception is IOException ioException2) + { + return ioException2.HResult == NativeMethods.EWOULDBLOCK; + } + + return false; } } } diff --git a/src/LockCheck/Linux/LockInfo.cs b/src/LockCheck/Linux/LockInfo.cs index 7ba8d6c..6946fef 100644 --- a/src/LockCheck/Linux/LockInfo.cs +++ b/src/LockCheck/Linux/LockInfo.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; namespace LockCheck.Linux @@ -9,11 +9,15 @@ internal class LockInfo public string LockMode { get; private set; } public string LockAccess { get; private set; } public int ProcessId { get; private set; } - public InodeInfo InodeInfo { get; set; } + public InodeInfo InodeInfo { get; private set; } public static LockInfo ParseLine(string line) { - // Each line has 8 (sometimes 9, see '->') blocks/fields: + // Each line has 8 (or 9 if you count the "->" marker) fields separated by spaces. + // Additional fields after that are possible, but can be ignored. + // The values for the LockType, LockMode, and LockAccess fields are manifold. + // So we don't interpret them, but just store them as strings. They are only + // provided as informational values anyway and not used for program logic. // // 1: POSIX ADVISORY READ 5433 08:01:7864448 128 128 // 2: FLOCK ADVISORY WRITE 2001 08:01:7864554 0 EOF @@ -24,6 +28,13 @@ public static LockInfo ParseLine(string line) // 6: POSIX ADVISORY READ 3548 08:01:7867240 1 1 // 7: POSIX ADVISORY READ 3548 08:01:7865567 1826 2335 // 8: OFDLCK ADVISORY WRITE -1 08:01:8713209 128 191 + // + // The major:minor device numbers (e.g. 08:01:...) might not be actual inode numbers + // in case the respective FS is not a physical one (e.g. not ext4, but tempfs or procfs). + // https://utcc.utoronto.ca/~cks/space/blog/linux/ProcLocksNotes has a nice explanation. + // In our case we don't care, because calling code looks up "in reverse" anyway. That is, + // it has an inode for an actual file/directory and needs this information to see if it + // is "locked". var span = line.AsSpan(); int count = span.Count(' ') + 1; diff --git a/src/LockCheck/Linux/NativeMethods.cs b/src/LockCheck/Linux/NativeMethods.cs index 24d48da..f1839b7 100644 --- a/src/LockCheck/Linux/NativeMethods.cs +++ b/src/LockCheck/Linux/NativeMethods.cs @@ -9,6 +9,7 @@ namespace LockCheck.Linux internal static partial class NativeMethods { public const int EAGAIN = 11; // Resource unavailable, try again (same value as EWOULDBLOCK), + public const int EACCES = 13; // Mandatory lock public const int EWOULDBLOCK = EAGAIN; // Operation would block. public const int ERANGE = 34; @@ -135,4 +136,4 @@ internal unsafe struct Passwd // ---END----------------------------------------------------------------------------------- } -} \ No newline at end of file +} diff --git a/src/LockCheck/Linux/ProcFileSystem.cs b/src/LockCheck/Linux/ProcFileSystem.cs index 922ca4b..46d2f8a 100644 --- a/src/LockCheck/Linux/ProcFileSystem.cs +++ b/src/LockCheck/Linux/ProcFileSystem.cs @@ -1,7 +1,10 @@ -using System; +using System; +using System.Buffers; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; +using System.Text; namespace LockCheck.Linux { @@ -103,5 +106,63 @@ private static Dictionary GetInodeToPaths(HashSet paths) return inodesToPaths; } + + internal static string GetProcessExecutablePathFromCmdLine(int processId) + { + byte[] rentedBuffer = null; + try + { + using (var file = new FileStream($"/proc/{processId}/cmdline", FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1, useAsync: false)) + { + Span buffer = stackalloc byte[256]; + int bytesRead = 0; + while (true) + { + if (bytesRead == buffer.Length) + { + // Resize buffer + uint newLength = (uint)buffer.Length * 2; + // Store what was read into new buffer + byte[] tmp = ArrayPool.Shared.Rent((int)newLength); + buffer.CopyTo(tmp); + // Remember current "rented" buffer (might be null) + byte[] lastRentedBuffer = rentedBuffer; + // From now on, we did rent a buffer. And it will be used for further reads. + buffer = tmp; + rentedBuffer = tmp; + // Return previously rented buffer, if any. + if (lastRentedBuffer != null) + { + ArrayPool.Shared.Return(lastRentedBuffer); + } + } + + Debug.Assert(bytesRead < buffer.Length); + int n = file.Read(buffer.Slice(bytesRead)); + bytesRead += n; + + // "/proc//cmdline" contains the original argument vector (argv), where each part is separated by a null byte. + // See if we have read enough for argv[0], which contains the process name. + ReadOnlySpan argRemainder = buffer.Slice(0, bytesRead); + int argEnd = argRemainder.IndexOf((byte)'\0'); + if (argEnd != -1) + { + return Encoding.UTF8.GetString(argRemainder.Slice(0, argEnd)); + } + } + } + } + catch (IOException) + { + return null; + } + finally + { + if (rentedBuffer != null) + { + ArrayPool.Shared.Return(rentedBuffer); + } + } + } } } diff --git a/src/LockCheck/Linux/ProcessInfo.Linux.cs b/src/LockCheck/Linux/ProcessInfo.Linux.cs index bfd62fe..75d94f6 100644 --- a/src/LockCheck/Linux/ProcessInfo.Linux.cs +++ b/src/LockCheck/Linux/ProcessInfo.Linux.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.IO; @@ -48,9 +48,6 @@ public static ProcessInfoLinux Create(LockInfo li) ApplicationName = process.ProcessName }; - // MainModule may be null, if no permissions, etc. - // Note: alternative of "readlink -f /proc//exe" will - // also yield results in this case. if (process.MainModule != null) { result.ExecutableFullPath = process.MainModule.FileName; @@ -58,7 +55,10 @@ public static ProcessInfoLinux Create(LockInfo li) } else { - result.ExecutableFullPath = process.ProcessName; + // MainModule may be null, if no permissions, etc. + // Using "readlink -f /proc//exe" will also yield no results in this case. + // However, "/proc//cmdline" can work. So attempt to get the executable name from there. + result.ExecutableFullPath = ProcFileSystem.GetProcessExecutablePathFromCmdLine(li.ProcessId) ?? process.ProcessName; result.ExecutableName = process.ProcessName; } } diff --git a/src/LockCheck/LockCheck.csproj b/src/LockCheck/LockCheck.csproj index 78e12a3..ce9186c 100644 --- a/src/LockCheck/LockCheck.csproj +++ b/src/LockCheck/LockCheck.csproj @@ -1,4 +1,4 @@ - + net8.0;net481 @@ -32,6 +32,12 @@ + + + <_Parameter1>LockCheck.Tests + + + diff --git a/src/LockCheck/ProcessInfo.cs b/src/LockCheck/ProcessInfo.cs index d135f14..86cd588 100644 --- a/src/LockCheck/ProcessInfo.cs +++ b/src/LockCheck/ProcessInfo.cs @@ -76,9 +76,13 @@ protected ProcessInfo(int processId, DateTime startTime) public override int GetHashCode() { +#if NET + return HashCode.Combine(ProcessId, StartTime); +#else int h1 = ProcessId.GetHashCode(); int h2 = StartTime.GetHashCode(); return ((h1 << 5) + h1) ^ h2; +#endif } public override bool Equals(object obj) @@ -91,41 +95,28 @@ public override bool Equals(object obj) return false; } - public override string ToString() => ProcessId + "@" + StartTime.ToString("s"); + public override string ToString() => ProcessId + "@" + StartTime.ToString("O"); public string ToString(string format) { - if (format == null) - { - return ToString(); - } - - if (format == "F") - { - return ToString() + "/" + ApplicationName; - } + string baseFormat = ToString(); - if (format == "A") + if (format != null) { - var sb = new StringBuilder(); - sb.Append(nameof(ProcessId)).Append(": ").Append(ProcessId).AppendLine(); - sb.Append(nameof(StartTime)).Append(": ").Append(StartTime).AppendLine(); - sb.Append(nameof(ExecutableName)).Append(": ").Append(ExecutableName).AppendLine(); - sb.Append(nameof(ApplicationName)).Append(": ").Append(ApplicationName).AppendLine(); - sb.Append(nameof(Owner)).Append(": ").Append(Owner).AppendLine(); - sb.Append(nameof(ExecutableFullPath)).Append(": ").Append(ExecutableFullPath).AppendLine(); - sb.Append(nameof(SessionId)).Append(": ").Append(SessionId).AppendLine(); - sb.Append(nameof(LockType)).Append(": ").Append(LockType).AppendLine(); - sb.Append(nameof(LockMode)).Append(": ").Append(LockMode).AppendLine(); - sb.Append(nameof(LockAccess)).Append(": ").Append(LockAccess).AppendLine(); - return sb.ToString(); + if (format == "F") + { + return $"{baseFormat}/{ApplicationName}"; + } } - return ToString(); + return baseFormat; } public static void Format(StringBuilder sb, IEnumerable lockers, IEnumerable fileNames, int? maxProcesses = null, string ownerOverwrite = null) { + if (fileNames == null) + throw new ArgumentNullException(nameof(fileNames)); + if (lockers == null || !lockers.Any()) return; diff --git a/src/LockCheck/Windows/Extensions.cs b/src/LockCheck/Windows/Extensions.cs index cd50dbf..1ce8f52 100644 --- a/src/LockCheck/Windows/Extensions.cs +++ b/src/LockCheck/Windows/Extensions.cs @@ -1,26 +1,36 @@ -using System; +using System; using System.IO; namespace LockCheck.Windows { internal static class Extensions { - public static bool IsFileLocked(IOException exception) + public static bool IsFileLocked(Exception exception) { if (exception == null) throw new ArgumentNullException(nameof(exception)); - // Generally it is not safe / stable to convert HRESULTs to Win32 error codes. It works here, - // because we exactly know where we're at. So resist refactoring the following code into an - // (maybe even externally visible) method. - int errorCode = exception.HResult & ((1 << 16) - 1); - - if (errorCode == NativeMethods.ERROR_LOCK_VIOLATION || - errorCode == NativeMethods.ERROR_SHARING_VIOLATION) + if (exception is IOException ioException) { - return true; - } + // Generally it is not safe / stable to convert HRESULTs to Win32 error codes. It works here, + // because we exactly know where we're at. So resist refactoring the following code into an + // (maybe even externally visible) method. + int errorCode = ioException.HResult & ((1 << 16) - 1); + // Code coverage note: causing a ERROR_LOCK_VIOLATION is rather hard to achieve in a test. + // Basically, you will mostly (always?) get a ERROR_SHARING_VIOLATION, unless you would + // do the test via the network (e.g. using a share). Note that using the "\\\C$" + // share will not cut it. + // + // Also note, that as of current (fall 2024), the .NET runtime does not raise IOException + // with ERROR_LOCK_VIOLATION. Since technically, this error is a potential result of a + // locking issue, we check for it anyway. + if (errorCode == NativeMethods.ERROR_LOCK_VIOLATION || + errorCode == NativeMethods.ERROR_SHARING_VIOLATION) + { + return true; + } + } return false; } } diff --git a/src/LockCheck/Windows/NativeMethods.cs b/src/LockCheck/Windows/NativeMethods.cs index 0e49cc1..c1bc14e 100644 --- a/src/LockCheck/Windows/NativeMethods.cs +++ b/src/LockCheck/Windows/NativeMethods.cs @@ -1,4 +1,6 @@ -using System; +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.InteropServices; using System.Security.Principal; @@ -28,7 +30,6 @@ internal static partial class NativeMethods internal const int ERROR_SHARING_VIOLATION = 32; internal const int ERROR_LOCK_VIOLATION = 33; internal const int ERROR_CANCELLED = 1223; - internal const int ERROR_MR_MID_NOT_FOUND = 317; internal const uint STATUS_SUCCESS = 0; internal const uint STATUS_INFO_LENGTH_MISMATCH = 0xC0000004; @@ -666,7 +667,7 @@ internal struct UNICODE_STRING public short MaximumLength; public IntPtr Buffer; - public string GetEmptyBuffer() => new('\0', Length / 2); + public readonly string GetEmptyBuffer() => new('\0', Length / 2); } // for 32-bit process in a 64-bit OS only @@ -689,7 +690,7 @@ internal struct UNICODE_STRING_WOW64 public short MaximumLength; public long Buffer; - public string GetEmptyBuffer() => new('\0', Length / 2); + public readonly string GetEmptyBuffer() => new('\0', Length / 2); } [StructLayout(LayoutKind.Sequential)] @@ -699,7 +700,21 @@ internal struct UNICODE_STRING_32 public short MaximumLength; public int Buffer; - public string GetEmptyBuffer() => new('\0', Length / 2); + public readonly string GetEmptyBuffer() => new('\0', Length / 2); } + + + internal const int FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100; + internal const int FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200; + internal const int FORMAT_MESSAGE_FROM_STRING = 0x00000400; + internal const int FORMAT_MESSAGE_FROM_HMODULE = 0x00000800; + internal const int FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000; + internal const int FORMAT_MESSAGE_ARGUMENT_ARRAY = 0x00002000; + +#if NET + internal static string GetMessage(int errorCode) => $"{Marshal.GetPInvokeErrorMessage(errorCode)} (0x{errorCode:X8})"; +#else + internal static string GetMessage(int errorCode) => $"{new Win32Exception(errorCode).Message} (0x{errorCode:X8})"; +#endif } } diff --git a/src/LockCheck/Windows/NtDll.cs b/src/LockCheck/Windows/NtDll.cs index 3050a53..33a29ba 100644 --- a/src/LockCheck/Windows/NtDll.cs +++ b/src/LockCheck/Windows/NtDll.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Runtime.InteropServices; using System.Threading; @@ -69,7 +70,7 @@ private static void GetLockingProcessInfo(string path, HashSet resu if (status != STATUS_SUCCESS) { - throw GetException(status, "NtQueryInformationFile", "Failed to get file process IDs"); + throw GetException(status); } // Buffer contains: @@ -100,15 +101,10 @@ private static void GetLockingProcessInfo(string path, HashSet resu } } - private static NtException GetException(uint status, string apiName, string message) + internal static Win32Exception GetException(uint status) { int res = RtlNtStatusToDosError(status); - if (res == ERROR_MR_MID_NOT_FOUND) - { - return new NtException(status, $"{message} ({apiName}() status {status} (0x{status:8X})"); - } - - return new NtException(res, status, $"{message} ({apiName}() status {status} (0x{status:8X})"); + return new Win32Exception(res, GetMessage(res)); } internal static Dictionary<(int, DateTime), ProcessInfo> GetProcessesByWorkingDirectory(List directories) @@ -188,7 +184,7 @@ private static NtException GetException(uint status, string apiName, string mess if (status < 0) { - throw GetException((uint)status, "NtQuerySystemInformation", "Could not get process info"); + throw GetException((uint)status); } // Parse the data block to get process information @@ -265,4 +261,4 @@ static int GetNewBufferSize(int existingBufferSize, int requiredSize) } } } -} \ No newline at end of file +} diff --git a/src/LockCheck/Windows/NtException.cs b/src/LockCheck/Windows/NtException.cs deleted file mode 100644 index 07b4671..0000000 --- a/src/LockCheck/Windows/NtException.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.ComponentModel; - -namespace LockCheck.Windows -{ - internal class NtException : Win32Exception - { - public NtException(uint status, string message) - : base(message) - { - HResult = unchecked((int)status); - } - - public NtException(int error, uint status, string message) - : base(error, message) - { - HResult = unchecked((int)status); - } - } -} \ No newline at end of file diff --git a/src/LockCheck/Windows/Peb.cs b/src/LockCheck/Windows/Peb.cs index 9e515ef..110d568 100644 --- a/src/LockCheck/Windows/Peb.cs +++ b/src/LockCheck/Windows/Peb.cs @@ -1,4 +1,4 @@ -using Microsoft.Win32.SafeHandles; +using Microsoft.Win32.SafeHandles; using System; using System.Diagnostics; using System.Runtime.CompilerServices; @@ -196,8 +196,13 @@ private static string GetStringTarget32(SafeProcessHandle handle, IntPtr pp, int var us = new UNICODE_STRING_32(); if (SUCCEEDED(ReadProcessMemory(handle, pp + offset, ref us, new IntPtr(Marshal.SizeOf(us)), IntPtr.Zero), he)) { - if ((us.Buffer != 0) && (us.Length != 0)) + if (us.Buffer != 0) { + if (us.Length == 0) + { + return string.Empty; + } + string lpBuffer = us.GetEmptyBuffer(); if (SUCCEEDED(ReadProcessMemory(handle, new IntPtr(us.Buffer), lpBuffer, new IntPtr(us.Length), IntPtr.Zero), he)) { @@ -229,8 +234,13 @@ private static string GetStringTarget64(SafeProcessHandle handle, long pp, int o var us = new UNICODE_STRING_WOW64(); if (SUCCEEDED(NtWow64ReadVirtualMemory64(handle, pp + offset, ref us, Marshal.SizeOf(us), IntPtr.Zero), he)) { - if ((us.Buffer != 0) && (us.Length != 0)) + if (us.Buffer != 0) { + if (us.Length == 0) + { + return string.Empty; + } + string lpBuffer = us.GetEmptyBuffer(); if (SUCCEEDED(NtWow64ReadVirtualMemory64(handle, us.Buffer, lpBuffer, us.Length, IntPtr.Zero), he)) { @@ -260,8 +270,13 @@ private static string GetString(SafeProcessHandle handle, IntPtr pp, int offset, var us = new UNICODE_STRING(); if (SUCCEEDED(ReadProcessMemory(handle, pp + offset, ref us, new IntPtr(Marshal.SizeOf(us)), IntPtr.Zero), he)) { - if ((us.Buffer != IntPtr.Zero) && (us.Length != 0)) + if (us.Buffer != IntPtr.Zero) { + if (us.Length == 0) + { + return string.Empty; + } + string lpBuffer = us.GetEmptyBuffer(); if (SUCCEEDED(ReadProcessMemory(handle, us.Buffer, lpBuffer, new IntPtr(us.Length), IntPtr.Zero), he)) { @@ -307,4 +322,4 @@ private static bool SUCCEEDED(bool result, IHasErrorState he, [CallerMemberName] return true; } } -} \ No newline at end of file +} diff --git a/src/LockCheck/Windows/ProcessInfo.Windows.cs b/src/LockCheck/Windows/ProcessInfo.Windows.cs index ebfb3c9..8c11867 100644 --- a/src/LockCheck/Windows/ProcessInfo.Windows.cs +++ b/src/LockCheck/Windows/ProcessInfo.Windows.cs @@ -1,7 +1,5 @@ -using System; -using System.Diagnostics; +using System; using System.IO; -using System.Reflection.Emit; using Microsoft.Win32.SafeHandles; namespace LockCheck.Windows @@ -53,4 +51,4 @@ private ProcessInfoWindows(int processId, DateTime startTime) { } } -} \ No newline at end of file +} diff --git a/src/LockCheck/Windows/RestartManager.cs b/src/LockCheck/Windows/RestartManager.cs index 3c392dc..92cf8ad 100644 --- a/src/LockCheck/Windows/RestartManager.cs +++ b/src/LockCheck/Windows/RestartManager.cs @@ -23,7 +23,7 @@ public static HashSet GetLockingProcessInfos(string[] paths, ref Li uint handle; int res = NativeMethods.RmStartSession(out handle, 0, key); if (res != 0) - throw GetException(res, "RmStartSession", "Failed to begin restart manager session."); + throw GetException(res, "Failed to begin restart manager session"); try { @@ -42,7 +42,7 @@ public static HashSet GetLockingProcessInfos(string[] paths, ref Li res = NativeMethods.RmRegisterResources(handle, (uint)files.Count, files.ToArray(), 0, null, 0, null); if (res != 0) - throw GetException(res, "RmRegisterResources", "Could not register resources."); + throw GetException(res, "Could not register resources"); // // Obtain the list of affected applications/services. @@ -72,7 +72,7 @@ public static HashSet GetLockingProcessInfos(string[] paths, ref Li if (pnProcInfo == 0) return []; - Debug.Assert(rgAffectedApps != null, "rgAffectedApps != null"); + Debug.Assert(rgAffectedApps != null); var lockInfos = new HashSet((int)pnProcInfo); for (int i = 0; i < pnProcInfo; i++) { @@ -82,7 +82,7 @@ public static HashSet GetLockingProcessInfos(string[] paths, ref Li } if (res != NativeMethods.ERROR_MORE_DATA) - throw GetException(res, "RmGetList", string.Format("Failed to get entries (retry {0}).", retry)); + throw GetException(res, $"Failed to get entries (retry {retry})"); pnProcInfo = pnProcInfoNeeded; rgAffectedApps = new NativeMethods.RM_PROCESS_INFO[pnProcInfo]; @@ -92,52 +92,15 @@ public static HashSet GetLockingProcessInfos(string[] paths, ref Li { res = NativeMethods.RmEndSession(handle); if (res != 0) - throw GetException(res, "RmEndSession", "Failed to end the restart manager session."); + throw GetException(res, "Failed to end the restart manager session"); } return []; } - private static Win32Exception GetException(int res, string apiName, string message) + internal static Win32Exception GetException(int res, string message) { - string reason; - switch (res) - { - case NativeMethods.ERROR_ACCESS_DENIED: - reason = "Access is denied."; - break; - case NativeMethods.ERROR_SEM_TIMEOUT: - reason = "A Restart Manager function could not obtain a Registry write mutex in the allotted time. " + - "A system restart is recommended because further use of the Restart Manager is likely to fail."; - break; - case NativeMethods.ERROR_BAD_ARGUMENTS: - reason = "One or more arguments are not correct. This error value is returned by the Restart Manager " + - "function if a NULL pointer or 0 is passed in a parameter that requires a non-null and non-zero value."; - break; - case NativeMethods.ERROR_MAX_SESSIONS_REACHED: - reason = "The maximum number of sessions has been reached."; - break; - case NativeMethods.ERROR_WRITE_FAULT: - reason = "An operation was unable to read or write to the registry."; - break; - case NativeMethods.ERROR_OUTOFMEMORY: - reason = "A Restart Manager operation could not complete because not enough memory was available."; - break; - case NativeMethods.ERROR_CANCELLED: - reason = "The current operation is canceled by user."; - break; - case NativeMethods.ERROR_MORE_DATA: - reason = "More data is available."; - break; - case NativeMethods.ERROR_INVALID_HANDLE: - reason = "No Restart Manager session exists for the handle supplied."; - break; - default: - reason = string.Format("0x{0:x8}", res); - break; - } - - return new Win32Exception(res, string.Format("{0} ({1}() error {2}: {3})", message, apiName, res, reason)); + return new Win32Exception(res, $"{message}: {NativeMethods.GetMessage(res)}"); } } -} \ No newline at end of file +} diff --git a/test/LockCheck.Tests/ExceptionUtilsTests.cs b/test/LockCheck.Tests/ExceptionUtilsTests.cs index b3743fe..dd277a1 100644 --- a/test/LockCheck.Tests/ExceptionUtilsTests.cs +++ b/test/LockCheck.Tests/ExceptionUtilsTests.cs @@ -19,28 +19,43 @@ public void CorePlatformThrowsIOExceptionOnLock() } [TestMethod] - public void IsFileLockedRecognizesLock() + public void RethrowWithLockingInformation_ShouldThrowArgumentNullException_WhenFileNameIsNull() { - bool found = false; - TestHelper.CreateLockSituation((ex, fileName) => - { - if (ex is IOException ioex) - { - found = ioex.IsFileLocked(); - } - }); + Assert.ThrowsException(() => new Exception().RethrowWithLockingInformation((string)null)); + } - Assert.IsTrue(found); + [TestMethod] + public void RethrowWithLockingInformation_ShouldThrowArgumentNullException_WhenFileNamesIsNull() + { + Assert.ThrowsException(() => new Exception().RethrowWithLockingInformation((string[])null)); + } + + [TestMethod] + public void RethrowWithLockingInformation_ShouldNotThrowIOException_WhenFileNamesIsEmpty() + { + Assert.IsFalse(new Exception().RethrowWithLockingInformation(Array.Empty())); + } + + [TestMethod] + public void RethrowWithLockingInformation_ShouldNotThrowIOException_WhenExceptionIsNotIOException() + { + Assert.IsFalse(new Exception().RethrowWithLockingInformation("test.txt")); + } + + [TestMethod] + public void RethrowWithLockingInformation_ShouldNotThrowIOException_WhenIOExceptionIsNotDueToFileLock() + { + Assert.IsFalse(new IOException().RethrowWithLockingInformation("test.txt")); } [DataTestMethod] [DataRow(LockManagerFeatures.None)] [DataRow(LockManagerFeatures.UseLowLevelApi)] - public void RethrownExceptionContainsInformation(LockManagerFeatures features) + public void RethrowWithLockingInformation_ShouldRethrowWithLockingInformation_WhenLockIsFound(LockManagerFeatures features) { TestHelper.CreateLockSituation((ex, fileName) => { - var processInfos = LockManager.GetLockingProcessInfos([ fileName], features).ToList(); + var processInfos = LockManager.GetLockingProcessInfos([fileName], features).ToList(); Assert.AreEqual(1, processInfos.Count); // Sanity, has been tested in LockManagerTests var expectedMessageContents = new StringBuilder(); @@ -67,5 +82,26 @@ public void RethrownExceptionContainsInformation(LockManagerFeatures features) } }); } + + [TestMethod] + public void IsFileLocked_ShouldThrowArgumentNullException_WhenExceptionIsNull() + { + Assert.ThrowsException(() => ((IOException)null).IsFileLocked()); + } + + [TestMethod] + public void IsFileLocked_ShouldReturnTrue_WhenFileIsLocked() + { + bool found = false; + TestHelper.CreateLockSituation((ex, fileName) => + { + if (ex is IOException ioex) + { + found = ioex.IsFileLocked(); + } + }); + + Assert.IsTrue(found); + } } } diff --git a/test/LockCheck.Tests/Linux/ProcFileSystemTests.cs b/test/LockCheck.Tests/Linux/ProcFileSystemTests.cs new file mode 100644 index 0000000..2c3c50c --- /dev/null +++ b/test/LockCheck.Tests/Linux/ProcFileSystemTests.cs @@ -0,0 +1,28 @@ +using System; +using System.Diagnostics; +using LockCheck.Linux; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace LockCheck.Tests.Linux +{ + [SupportedTestClassPlatform("linux")] + public class ProcFileSystemTests + { + [TestMethod] + public void GetProcessExecutablePathFromCmdLine_ShouldReturnExecutableName_WhenNoPermissionToProcess() + { + using var init = Process.GetProcessById(1); + + // Assume unit tests are not run as root. + Assert.IsNull(init.MainModule); + + Assert.AreEqual("/sbin/init", ProcFileSystem.GetProcessExecutablePathFromCmdLine(1)); + } + + [TestMethod] + public void GetProcessExecutablePathFromCmdLine_ShouldReturnNull_WhenProcessDoesNotExist() + { + Assert.IsNull(ProcFileSystem.GetProcessExecutablePathFromCmdLine(-1)); + } + } +} diff --git a/test/LockCheck.Tests/LockCheck.Tests.csproj b/test/LockCheck.Tests/LockCheck.Tests.csproj index ca54bce..5591b16 100644 --- a/test/LockCheck.Tests/LockCheck.Tests.csproj +++ b/test/LockCheck.Tests/LockCheck.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0;net481 @@ -15,6 +15,13 @@ + + + + + diff --git a/test/LockCheck.Tests/LockManagerTests.cs b/test/LockCheck.Tests/LockManagerTests.cs index 6bd8b96..a7e0ac8 100644 --- a/test/LockCheck.Tests/LockManagerTests.cs +++ b/test/LockCheck.Tests/LockManagerTests.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.IO; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -11,7 +12,7 @@ public class LockManagerTests [DataTestMethod] [DataRow(LockManagerFeatures.None)] [DataRow(LockManagerFeatures.UseLowLevelApi)] - public void LockInformationAvailable(LockManagerFeatures features) + public void GetLockingProcessInfos_ShouldReturnProcess_WhenHasLock(LockManagerFeatures features) { var process = Process.GetCurrentProcess(); @@ -37,7 +38,7 @@ public void LockInformationAvailable(LockManagerFeatures features) [DataRow(LockManagerFeatures.None | LockManagerFeatures.CheckDirectories, false)] [DataRow(LockManagerFeatures.UseLowLevelApi | LockManagerFeatures.CheckDirectories, true)] [DataRow(LockManagerFeatures.UseLowLevelApi | LockManagerFeatures.CheckDirectories, false)] - public void LockInformationAvailableForDirectory(LockManagerFeatures features, bool target64Bit) + public void GetLockingProcessInfos_ShouldReturnProcess_WhenWorkingDirectoryMatches(LockManagerFeatures features, bool target64Bit) { if (!Environment.Is64BitOperatingSystem) { @@ -52,7 +53,7 @@ public void LockInformationAvailableForDirectory(LockManagerFeatures features, b // Since "working directory" matches by path prefix, we might actually see multiple matches. // Make sure we find at least the one we explicitly started. var processInfo = processInfos.FirstOrDefault(pi => pi.ProcessId == args.ProcessId); - Assert.IsNotNull(processInfo); + Assert.IsNotNull(processInfo, $"Expected process with ID {args.ProcessId}/{args.ProcessName} not found as a match"); Assert.AreEqual(args.ProcessId, processInfo.ProcessId); Assert.AreEqual(args.SessionId, processInfo.SessionId); Assert.AreEqual(args.ProcessStartTime, processInfo.StartTime); @@ -63,5 +64,34 @@ public void LockInformationAvailableForDirectory(LockManagerFeatures features, b StringAssert.Contains(processInfo.ExecutableName?.ToLowerInvariant(), args.ProcessName.ToLowerInvariant()); }); } + + [TestMethod] + public void GetLockingProcessInfos_ShouldThrowArgumentNullException_WhenPathsIsNull() + { + string[] paths = null; + + Assert.ThrowsException(() => LockManager.GetLockingProcessInfos(paths)); + } + + [TestMethod] + public void GetLockingProcessInfos_ShouldReturnEmpty_WhenPathsIsEmpty() + { + string[] paths = []; + + var result = LockManager.GetLockingProcessInfos(paths); + + Assert.IsFalse(result.Any()); + } + + [TestMethod] + public void GetLockingProcessInfos_ShouldReturnEmpty_WhenFileDoesNotExist() + { + string[] paths = [Path.Combine(Path.GetTempPath(), "nonexistentfile.txt")]; + + var result = LockManager.GetLockingProcessInfos(paths); + + Assert.IsFalse(result.Any()); + } + } } diff --git a/test/LockCheck.Tests/ProcessInfoTests.cs b/test/LockCheck.Tests/ProcessInfoTests.cs new file mode 100644 index 0000000..6abff55 --- /dev/null +++ b/test/LockCheck.Tests/ProcessInfoTests.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace LockCheck.Tests +{ + [TestClass] + public class ProcessInfoTests + { + private class TestProcessInfo : ProcessInfo + { + public TestProcessInfo(int processId, DateTime startTime) : base(processId, startTime) + { + ExecutableName = "TestExecutable"; + ApplicationName = "TestApplication"; + Owner = "TestOwner"; + ExecutableFullPath = "C:\\TestPath\\TestExecutable.exe"; + SessionId = 1; + LockType = "TestLockType"; + LockMode = "TestLockMode"; + LockAccess = "TestLockAccess"; + } + } + + [TestMethod] + public void GetHashCode_ShouldSupportDictionary() + { + var processInfo = new TestProcessInfo(1234, new DateTime(2023, 10, 1)); + + var dictionary = new Dictionary + { + { processInfo, true } + }; + + Assert.IsTrue(dictionary.ContainsKey(processInfo)); + } + + [TestMethod] + public void Equals_ShouldReturnTrueForEqualObjects() + { + var processInfo1 = new TestProcessInfo(1234, new DateTime(2023, 10, 1)); + var processInfo2 = new TestProcessInfo(1234, new DateTime(2023, 10, 1)); + Assert.IsTrue(processInfo1.Equals(processInfo2)); + } + + [TestMethod] + public void Equals_ShouldReturnFalseForDifferentObjects() + { + var processInfo1 = new TestProcessInfo(1234, new DateTime(2023, 10, 1)); + var processInfo2 = new TestProcessInfo(5678, new DateTime(2023, 10, 2)); + Assert.IsFalse(processInfo1.Equals(processInfo2)); + } + + [TestMethod] + public void ToString_ShouldReturnCorrectString() + { + var processInfo = new TestProcessInfo(1234, new DateTime(2023, 10, 1)); + Assert.AreEqual("1234@2023-10-01T00:00:00.0000000", processInfo.ToString()); + Assert.AreEqual("1234@2023-10-01T00:00:00.0000000", processInfo.ToString(null)); + } + + [TestMethod] + public void ToString_WithFormatF_ShouldReturnCorrectString() + { + var processInfo = new TestProcessInfo(1234, new DateTime(2023, 10, 1)); + Assert.AreEqual("1234@2023-10-01T00:00:00.0000000/TestApplication", processInfo.ToString("F")); + } + + [TestMethod] + public void Format_ShouldReturnEmptyString_WithNullLockers() + { + var sb = new StringBuilder(); + ProcessInfo.Format(sb, null, []); + Assert.AreEqual(0, sb.Length); + } + + [TestMethod] + public void Format_ShouldReturnEmptyString_WithEmptyLockers() + { + var sb = new StringBuilder(); + ProcessInfo.Format(sb, [], []); + Assert.AreEqual(0, sb.Length); + } + + [TestMethod] + public void Format_ShouldThrowArgumentNullException_WithNullFileNames() + { + Assert.ThrowsException(() => ProcessInfo.Format(new(), [], null)); + } + + [TestMethod] + public void Format_ShouldReturnCorrectString() + { + var processInfo1 = new TestProcessInfo(1234, new DateTime(2023, 10, 1)); + var processInfo2 = new TestProcessInfo(5678, new DateTime(2023, 10, 2)); + var lockers = new List { processInfo1, processInfo2 }; + var fileNames = new List { "file1.txt", "file2.txt" }; + var sb = new StringBuilder(); + + ProcessInfo.Format(sb, lockers, fileNames, maxProcesses: 1); + + var expected = new TestProcessInfo(processInfo1.ProcessId, processInfo1.StartTime); + + var expectedString = new StringBuilder() + .AppendFormat("File {0} locked by: ", string.Join(", ", fileNames)) + .AppendLine($"[{expected.ApplicationName}, pid={expected.ProcessId}, owner={expected.Owner}, started={expected.StartTime:yyyy-MM-dd HH:mm:ss.fff}]") + .AppendLine("[1 more processes...]") + .ToString(); + + Assert.AreEqual(expectedString, sb.ToString()); + } + } +} diff --git a/test/LockCheck.Tests/SupportdOSTestMethod.cs b/test/LockCheck.Tests/SupportdOSTestMethod.cs new file mode 100644 index 0000000..4bd7f08 --- /dev/null +++ b/test/LockCheck.Tests/SupportdOSTestMethod.cs @@ -0,0 +1,88 @@ +using System; +using System.Runtime.InteropServices; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace LockCheck.Tests +{ + public sealed class SupportedTestClassPlatformAttribute : TestClassAttribute + { + public SupportedTestClassPlatformAttribute(string platformName) + { + PlatformName = platformName; + } + + public string PlatformName { get; } + + public override TestMethodAttribute GetTestMethodAttribute(TestMethodAttribute testMethodAttribute) + { + if (testMethodAttribute is SupportedTestMethodPlatformAttribute ta) + { + return ta; + } + + return new SupportedTestMethodPlatformAttribute(base.GetTestMethodAttribute(testMethodAttribute), PlatformName.ToString()); + } + } + + public sealed class SupportedTestMethodPlatformAttribute : TestMethodAttribute + { + private readonly TestMethodAttribute _attr; + + public SupportedTestMethodPlatformAttribute(string platformName) + { + PlatformName = platformName; + } + + public SupportedTestMethodPlatformAttribute(TestMethodAttribute attr, string platformName) + : this(platformName) + { + _attr = attr; + } + + public string PlatformName { get; } + + public override TestResult[] Execute(ITestMethod testMethod) + { + // Report status passed. Most examples on the Internet use "Inconclusive". + // This is not how we like to have it, because it looks "bad" in test reports + // and might hide actual issues to easily. + var outcomeIfSkipped = UnitTestOutcome.Passed; + OSPlatform platform; + switch (PlatformName.ToLowerInvariant()) + { + case "windows": + platform = OSPlatform.Windows; + break; + case "linux": + platform = OSPlatform.Linux; + break; + default: + platform = OSPlatform.Create(PlatformName); + // A platform we did not really expect. Mark this test as inconclusive + // so it lights up in the results. + outcomeIfSkipped = UnitTestOutcome.Inconclusive; + break; + } + + if (!RuntimeInformation.IsOSPlatform(platform)) + { + return + [ + new() + { + Outcome = outcomeIfSkipped, + TestFailureException = new PlatformNotSupportedException( + $"Test has not been skipped, because it is only supported on platform '{PlatformName}'.") + } + ]; + } + + if (_attr != null) + { + return _attr.Execute(testMethod); + } + + return base.Execute(testMethod); + } + } +} diff --git a/test/LockCheck.Tests/TestHelper.cs b/test/LockCheck.Tests/TestHelper.cs index 90de42a..945a228 100644 --- a/test/LockCheck.Tests/TestHelper.cs +++ b/test/LockCheck.Tests/TestHelper.cs @@ -1,8 +1,10 @@ -using System; +using System; using System.Diagnostics; +using System.Globalization; using System.IO; using System.IO.Pipes; using System.Runtime.InteropServices; +using System.Runtime.Versioning; using System.Threading; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -10,6 +12,40 @@ namespace LockCheck.Tests { internal static class TestHelper { + public static void RunWithInvariantCulture(Action action) + => RunWithCulture(CultureInfo.InvariantCulture, action); + + public static void RunWithCulture(CultureInfo cultureInfo, Action action) + { + var oldUi = CultureInfo.CurrentUICulture; + var old = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentUICulture = cultureInfo; + CultureInfo.CurrentCulture = cultureInfo; + + action(); + } + finally + { + CultureInfo.CurrentUICulture = oldUi; + CultureInfo.CurrentCulture = old; + } + } + + public static Windows.NativeMethods.FILETIME ToNativeFileTime(this DateTime dateTime) + { + // Convert DateTime to a long value representing the file time + long fileTime = dateTime.ToFileTime(); + + // Split the long value into high and low parts + Windows.NativeMethods.FILETIME fileTimeStruct; + fileTimeStruct.dwLowDateTime = (uint)(fileTime & 0xFFFFFFFF); + fileTimeStruct.dwHighDateTime = (uint)(fileTime >> 32); + + return fileTimeStruct; + } + public static void CreateLockSituation(Action action) { string tempFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".test"); diff --git a/test/LockCheck.Tests/Windows/NtDllTests.cs b/test/LockCheck.Tests/Windows/NtDllTests.cs new file mode 100644 index 0000000..457a3e9 --- /dev/null +++ b/test/LockCheck.Tests/Windows/NtDllTests.cs @@ -0,0 +1,123 @@ +using LockCheck.Tests; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Text; + +namespace LockCheck.Windows.Tests +{ + [SupportedTestClassPlatform("windows")] + public class NtDllTests + { + [TestMethod] + public void GetLockingProcessInfos_ShouldThrowArgumentNullException_WhenPathsIsNull() + { + var directories = new List(); + Assert.ThrowsException(() => NtDll.GetLockingProcessInfos(null, ref directories)); + } + + [TestMethod] + public void GetLockingProcessInfos_ShouldAddDirectories_WhenPathIsDirectory() + { + var di = new DirectoryInfo(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString())); + di.Create(); + + try + { + var directories = new List(); + var result = NtDll.GetLockingProcessInfos([di.FullName], ref directories); + + Assert.AreEqual(1, directories.Count); + Assert.AreEqual(di.FullName, directories[0]); + Assert.AreEqual(0, result.Count); + } + finally + { + di.Delete(); + } + } + + [TestMethod] + public void GetLockingProcessInfos_ShouldCallGetLockingProcessInfo_WhenPathIsFile() + { + var fi = new FileInfo(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".txt")); + + try + { + using var stream = fi.Create(); + + var directories = new List(); + var result = NtDll.GetLockingProcessInfos([fi.FullName], ref directories); + + Assert.IsTrue(result.Count >= 0); // Just to ensure the method runs without exceptions + Assert.AreEqual(0, directories.Count); + } + finally + { + fi.Delete(); + } + } + + [TestMethod] + public void GetLockingProcessInfo_ShouldThrowArgumentNullException_WhenPathsIsNull() + { + var directories = new List(); + Assert.ThrowsException(() => NtDll.GetLockingProcessInfos(null, ref directories)); + } + + [TestMethod] + public void GetLockingProcessInfo_ShouldThrowArgumentNullException_WhenAPathIsNull() + { + var directories = new List(); + Assert.ThrowsException(() => NtDll.GetLockingProcessInfos([null], ref directories)); + } + + [TestMethod] + public void EnumerateSystemProcesses_ShouldContainOnlySpecifiedProcess_WhenFilterIsSpecified() + { + using var self = Process.GetCurrentProcess(); + bool found = false; + int count = 0; + var result = NtDll.EnumerateSystemProcesses([self.Id], self.Id, (mp, currentPtr, idx, pi) => + { + if ((int)pi.UniqueProcessId == mp) + { + found = true; + } + count++; + return 0; + }); + Assert.IsTrue(found); + Assert.IsTrue(count == 1); + Assert.IsTrue(result.ContainsKey((self.Id, self.StartTime))); + Assert.IsTrue(result.Count == 1); + } + + [TestMethod] + public void EnumerateSystemProcesses_ShouldContainAllProcesses_WhenNoProcessFilterIsSpecified() + { + using var self = Process.GetCurrentProcess(); + bool found = false; + int count = 0; + var result = NtDll.EnumerateSystemProcesses(null, self.Id, (mp, currentPtr, idx, pi) => + { + if ((int)pi.UniqueProcessId == mp) + { + found = true; + } + count++; + return 0; + }); + // Number of total processes if course highly volatile. We just check that we + // found more than one. + Assert.IsTrue(found); + Assert.IsTrue(count > 1); + Assert.IsTrue(result.ContainsKey((self.Id, self.StartTime))); + Assert.IsTrue(result.Count > 1); + } + } +} diff --git a/test/LockCheck.Tests/Windows/ProcessInfoWindowsTests.cs b/test/LockCheck.Tests/Windows/ProcessInfoWindowsTests.cs new file mode 100644 index 0000000..8d48f23 --- /dev/null +++ b/test/LockCheck.Tests/Windows/ProcessInfoWindowsTests.cs @@ -0,0 +1,78 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; +using LockCheck.Tests; + +namespace LockCheck.Windows.Tests +{ + [SupportedTestClassPlatform("windows")] + public class ProcessInfoWindowsTests + { + [TestMethod] + public void Create_ShouldReturnProcessInfoWindows_WhenGivenValidRMProcessInfo() + { + var ts = DateTime.Now; + var rmProcessInfo = new NativeMethods.RM_PROCESS_INFO + { + Process = new NativeMethods.RM_UNIQUE_PROCESS + { + dwProcessId = (uint)Process.GetCurrentProcess().Id, + ProcessStartTime = ts.ToNativeFileTime() + }, + strAppName = "TestApp", + strServiceShortName = "TestService", + ApplicationType = NativeMethods.RM_APP_TYPE.RmUnknownApp, + AppStatus = 0, + TSSessionId = 1, + bRestartable = true + }; + + var result = ProcessInfoWindows.Create(rmProcessInfo); + + Assert.IsNotNull(result); + Assert.AreEqual(rmProcessInfo.Process.dwProcessId, (uint)result.ProcessId); + Assert.AreEqual(ts, result.StartTime); + } + + [TestMethod] + public void Create_ShouldReturnProcessInfoWindows_WhenGivenValidProcessId() + { + int processId = Process.GetCurrentProcess().Id; + + var result = ProcessInfoWindows.Create(processId); + + Assert.IsNotNull(result); + Assert.AreEqual(processId, result.ProcessId); + } + + [TestMethod] + public void Create_ShouldReturnNull_WhenGivenInvalidProcessId() + { + var result = ProcessInfoWindows.Create(-1); + + Assert.IsNull(result); + } + + [TestMethod] + public void Create_ShouldReturnProcessInfoWindows_WhenGivenValidPeb() + { + var ts = DateTime.Now; + var pi = new NativeMethods.SYSTEM_PROCESS_INFORMATION + { + UniqueProcessId = (IntPtr)Process.GetCurrentProcess().Id, + CreateTime = ts.ToFileTime() + }; + var peb = new Peb(pi); + + var result = ProcessInfoWindows.Create(peb); + + Assert.IsNotNull(result); + Assert.AreEqual(peb.ProcessId, result.ProcessId); + Assert.AreEqual(peb.ProcessId, result.ProcessId); + Assert.AreEqual(ts, result.StartTime); + } + } +} diff --git a/test/LockCheck.Tests/Windows/RestartManagerTests.cs b/test/LockCheck.Tests/Windows/RestartManagerTests.cs new file mode 100644 index 0000000..c173096 --- /dev/null +++ b/test/LockCheck.Tests/Windows/RestartManagerTests.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using LockCheck.Tests; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace LockCheck.Windows.Tests +{ + [SupportedTestClassPlatform("windows")] + public class RestartManagerTests + { + [TestMethod] + public void GetLockingProcessInfos_ShouldThrowArgumentNullException_WhenPathsIsNull() + { + var directories = new List(); + Assert.ThrowsException(() => RestartManager.GetLockingProcessInfos(null, ref directories)); + } + + [TestMethod] + public void GetLockingProcessInfos_ShouldAddDirectories_WhenPathIsDirectory() + { + var di = new DirectoryInfo(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString())); + di.Create(); + + try + { + var directories = new List(); + var result = RestartManager.GetLockingProcessInfos([di.FullName], ref directories); + + Assert.AreEqual(1, directories.Count); + Assert.AreEqual(di.FullName, directories[0]); + Assert.AreEqual(0, result.Count); + } + finally + { + di.Delete(); + } + } + + [TestMethod] + public void GetLockingProcessInfos_ShouldCallGetLockingProcessInfo_WhenPathIsFile() + { + var fi = new FileInfo(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".txt")); + + try + { + using var stream = fi.Create(); + + var directories = new List(); + var result = RestartManager.GetLockingProcessInfos([fi.FullName], ref directories); + + Assert.IsTrue(result.Count >= 0); // Just to ensure the method runs without exceptions + Assert.AreEqual(0, directories.Count); + } + finally + { + fi.Delete(); + } + } + } +}