-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathProcessExtensions.cs
136 lines (121 loc) · 10 KB
/
ProcessExtensions.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
using System.Diagnostics;
using System.Management;
using System.Runtime.InteropServices;
using System.Text;
namespace VolumeControl.TypeExtensions
{
/// <summary>
/// Extension methods for the <see cref="Process"/> class.
/// </summary>
public static class ProcessExtensions
{
/// <summary>Opens an existing local process object.</summary>
/// <param name="dwDesiredAccess">The access to the process object. This access right is checked against the security descriptor for the process. This parameter can be one or more of the process access rights.If the caller has enabled the SeDebugPrivilege privilege, the requested access is granted regardless of the contents of the security descriptor.</param>
/// <param name="bInheritHandle">If this value is TRUE, processes created by this process will inherit the handle. Otherwise, the processes do not inherit this handle.</param>
/// <param name="dwProcessId">The identifier of the local process to be opened.If the specified process is the System Idle Process(0x00000000), the function fails and the last error code is ERROR_INVALID_PARAMETER.If the specified process is the System process or one of the Client Server Run-Time Subsystem(CSRSS) processes, this function fails and the last error code is ERROR_ACCESS_DENIED because their access restrictions prevent user-level code from opening them.If you are using GetCurrentProcessId as an argument to this function, consider using GetCurrentProcess instead of OpenProcess, for improved performance.</param>
/// <returns>If the function succeeds, the return value is an open handle to the specified process. If the function fails, the return value is NULL.To get extended error information, call GetLastError.</returns>
[DllImport("Kernel32.dll")]
private static extern IntPtr OpenProcess(int dwDesiredAccess, int bInheritHandle, int dwProcessId);
private const int PROCESS_QUERY_LIMITED_INFORMATION = 4096;
private const int SYNCHRONIZE = 1048576;
/// <summary>
/// Retrieves timing information for the specified process.
/// </summary>
/// <param name="hProcess">A handle to the process whose timing information is sought. The handle must have the PROCESS_QUERY_INFORMATION or PROCESS_QUERY_LIMITED_INFORMATION access right. For more information, see Process Security and Access Rights.<br/>Windows Server 2003 and Windows XP: The handle must have the PROCESS_QUERY_INFORMATION access right.</param>
/// <param name="lpCreationTime">A pointer to a FILETIME structure that receives the creation time of the process.</param>
/// <param name="lpExitTime">A pointer to a FILETIME structure that receives the exit time of the process. If the process has not exited, the content of this structure is undefined.</param>
/// <param name="lpKernelTime">A pointer to a FILETIME structure that receives the amount of time that the process has executed in kernel mode. The time that each of the threads of the process has executed in kernel mode is determined, and then all of those times are summed together to obtain this value.</param>
/// <param name="lpUserTime">A pointer to a FILETIME structure that receives the amount of time that the process has executed in user mode. The time that each of the threads of the process has executed in user mode is determined, and then all of those times are summed together to obtain this value. Note that this value can exceed the amount of real time elapsed (between lpCreationTime and lpExitTime) if the process executes across multiple CPU cores.</param>
/// <returns>If the function succeeds, the return value is nonzero.<br/>If the function fails, the return value is zero. To get extended error information, call GetLastError.</returns>
[DllImport("Kernel32.dll")]
private static extern int GetProcessTimes(IntPtr hProcess, out long lpCreationTime, out long lpExitTime, out long lpKernelTime, out long lpUserTime);
/// <inheritdoc cref="GetProcessTimes(IntPtr, out long, out long, out long, out long)"/>
/// <param name="hProcess">A process handle.</param>
/// <param name="creationTime">A pointer to a FILETIME structure that receives the creation time of the process.</param>
/// <param name="exitTime">A pointer to a FILETIME structure that receives the exit time of the process. If the process has not exited, the content of this structure is undefined.</param>
/// <param name="kernelTime">A pointer to a FILETIME structure that receives the amount of time that the process has executed in kernel mode. The time that each of the threads of the process has executed in kernel mode is determined, and then all of those times are summed together to obtain this value.</param>
/// <param name="userTime">A pointer to a FILETIME structure that receives the amount of time that the process has executed in user mode. The time that each of the threads of the process has executed in user mode is determined, and then all of those times are summed together to obtain this value. Note that this value can exceed the amount of real time elapsed (between <paramref name="creationTime"/> and <paramref name="exitTime"/>) if the process executes across multiple CPU cores.</param>
private static void GetProcessTimes(IntPtr hProcess, out DateTime? creationTime, out DateTime? exitTime, out DateTime? kernelTime, out DateTime? userTime)
{
if (GetProcessTimes(hProcess, out long creation, out long exit, out long kernel, out long user) == 0)
{
creationTime = null;
exitTime = null;
kernelTime = null;
userTime = null;
return;
}
creationTime = DateTime.FromFileTime(creation);
exitTime = DateTime.FromFileTime(exit);
kernelTime = DateTime.FromFileTime(kernel);
userTime = DateTime.FromFileTime(user);
}
/// <summary>
/// Closes an open object handle.
/// </summary>
/// <returns>If the function succeeds, the return value is nonzero.<br/>If the function fails, the return value is zero. To get extended error information, call GetLastError.</returns>
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(IntPtr hObject);
/// <summary>
/// This fixes a bug that microsoft hasn't fixed since at least Windows Vista that occurs when calling <see cref="Process.HasExited"/>.<br/>
/// <b>DO NOT USE THE <see cref="Process.HasExited"/> PROPERTY, USE THIS INSTEAD!</b>
/// </summary>
/// <remarks>Sources:<br/>
/// https://www.giorgi.dev/net/access-denied-process-bugs/ <br/>
/// https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess <br/>
/// https://docs.microsoft.com/en-ca/windows/win32/api/processthreadsapi/nf-processthreadsapi-getprocesstimes <br/>
/// https://docs.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-closehandle <br/>
/// </remarks>
/// <param name="p">Process instance.</param>
/// <returns>True if the process has exited, false if the process is still running.</returns>
public static bool HasExited(this Process p)
{
if (p.Id == 0)
return false;
IntPtr handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, 0, p.Id);
if (handle == IntPtr.Zero)
return false;
GetProcessTimes(handle, out DateTime? _, out DateTime? exitTime, out DateTime? _, out DateTime? _);
_ = CloseHandle(handle);
return exitTime != null && DateTime.Now < exitTime;
}
[DllImport("psapi.dll", CharSet = CharSet.Unicode)]
private static extern uint GetModuleFileNameEx(IntPtr hProcess, IntPtr hModule, [Out] StringBuilder lpBaseName, [In][MarshalAs(UnmanagedType.U4)] int nSize);
/// <summary>
/// Gets the full path to the specified <see cref="Process"/>, <paramref name="process"/>.
/// </summary>
/// <remarks>
/// Using this instead of accessing <see cref="Process.MainModule"/> directly prevents exceptions when accessing a process built for a different architecture (32/64 bit)
/// </remarks>
/// <param name="process"><see cref="Process"/></param>
/// <returns>The filepath of <paramref name="process"/> if successful; otherwise <see langword="null"/>.</returns>
public static string? GetMainModulePath(this Process process)
{
if (process == null) return null;
IntPtr hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, process.Id);
if (hProc == IntPtr.Zero)
return null;
const int bufLen = 261;
var sbuf = new StringBuilder(bufLen);
string? result = null;
if (GetModuleFileNameEx(hProc, IntPtr.Zero, sbuf, bufLen) > 0)
result = sbuf.ToString();
return result;
}
/// <summary>
/// Gets the full path to the specified <see cref="Process"/>, <paramref name="process"/>.
/// </summary>
/// <remarks>
/// This method uses WMI to make an SQL query, and as such, is very slow! Use <see cref="GetMainModulePath(Process)"/> instead wherever possible.
/// </remarks>
/// <param name="process"><see cref="Process"/></param>
/// <returns>The filepath of <paramref name="process"/> if successful; otherwise <see langword="null"/>.</returns>
public static string? GetMainModulePathWMI(this Process process)
{
using var searcher = new ManagementObjectSearcher($"SELECT ProcessId, ExecutablePath FROM Win32_Process WHERE ProcessId = {process.Id}");
using ManagementObjectCollection? results = searcher.Get();
return results.Cast<ManagementObject>().FirstOrDefault() is ManagementObject mo ? (string)mo["ExecutablePath"] : null;
}
}
}