-
Notifications
You must be signed in to change notification settings - Fork 8.4k
Fixes #2534 by replacing expensive WMI query with Win32 API calls #2535
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b1a5f66
d716464
83a04b9
85f0cfe
43662ff
25ed36b
9df77b0
4211da7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,6 @@ | |
|
|
||
| using System.Collections; | ||
| using System.Diagnostics; | ||
| using System.Globalization; | ||
| using System.IO; | ||
| using System.Linq; | ||
| using System.Reflection; | ||
|
|
@@ -16,7 +15,10 @@ | |
| using Microsoft.Win32; | ||
| using System.Collections.Generic; | ||
| using System.Management.Automation.Language; | ||
| using Microsoft.Management.Infrastructure; | ||
| #if CORECLR | ||
| // Use stubs for SerializableAttribute, SecurityPermissionAttribute, ReliabilityContractAttribute and ISerializable related types. | ||
| using Microsoft.PowerShell.CoreClr.Stubs; | ||
| #endif | ||
|
|
||
| namespace System.Management.Automation | ||
| { | ||
|
|
@@ -91,57 +93,64 @@ internal static ProcessModule GetMainModule(Process targetProcess) | |
| /// <summary> | ||
| /// Retrieve the parent process of a process. | ||
| /// | ||
| /// This is an extremely expensive operation, as WMI | ||
| /// needs to work with an ugly Win32 API. The Win32 API | ||
| /// creates a snapshot of every process in the system, which | ||
| /// you then need to iterate through to find your process and | ||
| /// its parent PID. | ||
| /// | ||
| /// Also, since this is PID based, this API is only reliable | ||
| /// when the process has not yet exited. | ||
| /// Previously this code used WMI, but WMI is causing a CPU spike whenever the query gets called as it results in | ||
| /// tzres.dll and tzres.mui.dll being loaded into every process to conver the time information to local format. | ||
| /// For perf reasons, we result to P/Invoke. | ||
| /// </summary> | ||
| /// | ||
| /// <param name="current">The process we want to find the | ||
| /// parent of</param> | ||
| internal static Process GetParentProcess(Process current) | ||
| { | ||
| string wmiQuery = String.Format(CultureInfo.CurrentCulture, | ||
| "Select * From Win32_Process Where Handle='{0}'", | ||
| current.Id); | ||
|
|
||
| using (CimSession cimSession = CimSession.Create(null)) | ||
| { | ||
| IEnumerable<CimInstance> processCollection = | ||
| cimSession.QueryInstances("root/cimv2", "WQL", wmiQuery); | ||
|
|
||
| int parentPid = | ||
| processCollection.Select( | ||
| cimProcess => | ||
| Convert.ToInt32(cimProcess.CimInstanceProperties["ParentProcessId"].Value, | ||
| CultureInfo.CurrentCulture)).FirstOrDefault(); | ||
| int parentPid = 0; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have any Pester tests that cover this code change? If not, please create a new test or port some internal tests to the new environment.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Working on how to test this indirectly
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test added |
||
|
|
||
| if (parentPid == 0) | ||
| return null; | ||
| #if !UNIX | ||
| PlatformInvokes.PROCESSENTRY32 pe32 = new PlatformInvokes.PROCESSENTRY32 { }; | ||
| pe32.dwSize = (uint)ClrFacade.SizeOf<PlatformInvokes.PROCESSENTRY32>(); | ||
|
|
||
| try | ||
| using (PlatformInvokes.SafeSnapshotHandle hSnapshot = PlatformInvokes.CreateToolhelp32Snapshot(PlatformInvokes.SnapshotFlags.Process, (uint)current.Id)) | ||
| { | ||
| if (!PlatformInvokes.Process32First(hSnapshot, ref pe32)) | ||
| { | ||
| Process returnProcess = Process.GetProcessById(parentPid); | ||
|
|
||
| // Ensure the process started before the current | ||
| // process, as it could have gone away and had the | ||
| // PID recycled. | ||
| if (returnProcess.StartTime <= current.StartTime) | ||
| return returnProcess; | ||
| else | ||
| int errno = Marshal.GetLastWin32Error(); | ||
| if (errno == PlatformInvokes.ERROR_NO_MORE_FILES) | ||
| { | ||
| return null; | ||
| } | ||
| } | ||
| catch (ArgumentException) | ||
| do | ||
| { | ||
| // GetProcessById throws an ArgumentException when | ||
| // you reach the top of the chain -- Explorer.exe | ||
| // has a parent process, but you cannot retrieve it. | ||
| if (pe32.th32ProcessID == (uint)current.Id) | ||
| { | ||
| parentPid = (int)pe32.th32ParentProcessID; | ||
| break; | ||
| } | ||
|
|
||
| } while (PlatformInvokes.Process32Next(hSnapshot, ref pe32)); | ||
| } | ||
| #endif | ||
|
|
||
| if (parentPid == 0) | ||
| return null; | ||
|
|
||
| try | ||
| { | ||
| Process returnProcess = Process.GetProcessById(parentPid); | ||
|
|
||
| // Ensure the process started before the current | ||
| // process, as it could have gone away and had the | ||
| // PID recycled. | ||
| if (returnProcess.StartTime <= current.StartTime) | ||
| return returnProcess; | ||
| else | ||
| return null; | ||
| } | ||
| } | ||
| catch (ArgumentException) | ||
| { | ||
| // GetProcessById throws an ArgumentException when | ||
| // you reach the top of the chain -- Explorer.exe | ||
| // has a parent process, but you cannot retrieve it. | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| Describe "Native Command Processor" -tags "Feature" { | ||
|
|
||
| BeforeAll { | ||
| # Find where test/powershell is so we can find the createchildprocess command relative to it | ||
| $powershellTestDir = $PSScriptRoot | ||
| while ($powershellTestDir -notmatch 'test[\\/]powershell$') { | ||
| $powershellTestDir = Split-Path $powershellTestDir | ||
| } | ||
| $createchildprocess = Join-Path (Split-Path $powershellTestDir) tools/CreateChildProcess/bin/createchildprocess | ||
| } | ||
|
|
||
| # If powershell receives a StopProcessing, it should kill the native process and all child processes | ||
|
|
||
| # this test should pass and no longer Penidng when #2561 is fixed | ||
| It "Should kill native process tree" { | ||
|
|
||
| Test-Path $createchildprocess | Should Be $true | ||
|
|
||
| # make sure no test processes are running | ||
| # on Linux, the Process class truncates the name so filter using Where-Object | ||
| Get-Process | Where-Object {$_.Name -like 'createchildproc'} | Stop-Process | ||
|
|
||
| [int] $numToCreate = 2 | ||
|
|
||
| $ps = [PowerShell]::Create().AddCommand($createchildprocess) | ||
| $ps.AddParameter($numToCreate) | ||
| $async = $ps.BeginInvoke() | ||
| $ps.InvocationStateInfo.State | Should Be "Running" | ||
|
|
||
| [bool] $childrenCreated = $false | ||
| while (-not $childrenCreated) | ||
| { | ||
| $childprocesses = Get-Process | Where-Object {$_.Name -like 'createchildproc'} | ||
| if ($childprocesses.count -eq $numToCreate+1) | ||
| { | ||
| $childrenCreated = $true | ||
| } | ||
| } | ||
|
|
||
| $startTime = Get-Date | ||
| $beginsync = $ps.BeginStop($null, $async) | ||
| # wait no more than 5 secs for the processes to be terminated, otherwise test has failed | ||
| while (((Get-Date) - $startTime).TotalSeconds -lt 5) | ||
| { | ||
| if (($childprocesses.hasexited -eq $true).count -eq $numToCreate+1) | ||
| { | ||
| break | ||
| } | ||
| } | ||
| $childprocesses = Get-Process | Where-Object {$_.Name -like 'createchildproc'} | ||
| $count = $childprocesses.count | ||
| $childprocesses | Stop-Process | ||
| $count | Should Be 0 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| using System; | ||
| using System.Diagnostics; | ||
| using System.Threading; | ||
|
|
||
| namespace CreateChildProcess | ||
| { | ||
| class Program | ||
| { | ||
| static void Main(string[] args) | ||
| { | ||
| if (args.Length > 0) | ||
| { | ||
| uint num = UInt32.Parse(args[0]); | ||
| for (uint i = 0; i < num; i++) | ||
| { | ||
| Process child = new Process(); | ||
| child.StartInfo.FileName = Process.GetCurrentProcess().MainModule.FileName; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about implementing this within a PowerShell function? It would simplify the publishing and execution of it.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need this as a native executable to execute a particular code path in powershell for handing native commands |
||
| child.Start(); | ||
| } | ||
| } | ||
| // sleep is needed so the process doesn't exit before the test case kill it | ||
| Thread.Sleep(100000); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why the 100 second sleep?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought it was better to have it "timeout" rather than hang if the process wasn't successfully terminated. Maybe I should just leave it to the CI system to kill if it takes too long, but may be better to have this test fail and allow other tests to run within the CI time limit. |
||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| { | ||
| "name": "createchildprocess", | ||
| "version": "1.0.0-*", | ||
| "description": "Very simple little console app that creates child processes of itself", | ||
|
|
||
| "buildOptions": { | ||
| "emitEntryPoint": true | ||
| }, | ||
|
|
||
| "frameworks": { | ||
| "netcoreapp1.0": { | ||
| "dependencies": { | ||
| "Microsoft.NETCore.App": "1.1.0-preview1-001100-00" | ||
| } | ||
| } | ||
| }, | ||
|
|
||
| "runtimes": { | ||
| "ubuntu.16.04-x64": { }, | ||
| "ubuntu.14.04-x64": { }, | ||
| "debian.8-x64": { }, | ||
| "centos.7-x64": { }, | ||
| "win7-x64": { }, | ||
| "win81-x64": { }, | ||
| "win10-x64": { }, | ||
| "osx.10.11-x64": { } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does not seem like a good solution to your test issue. Is there a better way to handle it? Packaging scoops up all files in the publish directory, so these tools will get shipped along with PowerShell Core during release.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was just following an existing precedent used by echoargs.exe tests. However, I checked my install and it doesn't include the echoargs.exe test binary. Are you sure it gets picked up?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://docs.microsoft.com/en-us/dotnet/articles/core/tools/dotnet-publish
It looks like it works because it specifies a different output directory