diff --git a/build.psm1 b/build.psm1 index df54a82e9ea..89fd8a5292a 100644 --- a/build.psm1 +++ b/build.psm1 @@ -597,13 +597,19 @@ function Publish-PSTestTools { Find-Dotnet + $tools = "$PSScriptRoot/test/tools/EchoArgs","$PSScriptRoot/test/tools/CreateChildProcess" # Publish EchoArgs so it can be run by tests - Push-Location "$PSScriptRoot/test/tools/EchoArgs" - try { - dotnet publish --output bin - } finally { - Pop-Location + + foreach ($tool in $tools) + { + Push-Location $tool + try { + dotnet publish --output bin + } finally { + Pop-Location + } } + } function Start-PSPester { diff --git a/src/System.Management.Automation/CoreCLR/CorePsStub.cs b/src/System.Management.Automation/CoreCLR/CorePsStub.cs index dd67a97f8f1..faf91ed3c9b 100644 --- a/src/System.Management.Automation/CoreCLR/CorePsStub.cs +++ b/src/System.Management.Automation/CoreCLR/CorePsStub.cs @@ -502,6 +502,31 @@ public override bool IsInvalid } } + /// + /// Stub for SafeHandleMinusOneIsInvalid + /// + public abstract class SafeHandleMinusOneIsInvalid : SafeHandle + { + /// + /// Constructor + /// + protected SafeHandleMinusOneIsInvalid(bool ownsHandle) + : base(new IntPtr(-1), ownsHandle) + { + } + + /// + /// IsInvalid + /// + public override bool IsInvalid + { + get + { + return handle == new IntPtr(-1); + } + } + } + #endregion SafeHandle_Related #region Misc_Types diff --git a/src/System.Management.Automation/utils/PInvokeDllNames.cs b/src/System.Management.Automation/utils/PInvokeDllNames.cs index 11df77185e5..54a46aef186 100644 --- a/src/System.Management.Automation/utils/PInvokeDllNames.cs +++ b/src/System.Management.Automation/utils/PInvokeDllNames.cs @@ -132,6 +132,9 @@ internal static class PinvokeDllNames internal const string ReadConsoleInputDllName = "api-ms-win-core-console-l1-1-0.dll"; /*117*/ internal const string GetVersionExDllName = "api-ms-win-core-sysinfo-l1-1-0.dll"; /*118*/ internal const string FormatMessageDllName = "api-ms-win-core-localization-l1-2-0.dll"; /*119*/ + internal const string CreateToolhelp32SnapshotDllName = "api-ms-win-core-toolhelp-l1-1-0"; /*120*/ + internal const string Process32FirstDllName = "api-ms-win-core-toolhelp-l1-1-0"; /*121*/ + internal const string Process32NextDllName = "api-ms-win-core-toolhelp-l1-1-0"; /*122*/ #else internal const string QueryDosDeviceDllName = "kernel32.dll"; /*1*/ internal const string CreateSymbolicLinkDllName = "kernel32.dll"; /*2*/ @@ -251,6 +254,9 @@ internal static class PinvokeDllNames internal const string ReadConsoleInputDllName = "kernel32.dll"; /*117*/ internal const string GetVersionExDllName = "kernel32.dll"; /*118*/ internal const string FormatMessageDllName = "wevtapi.dll"; /*119*/ + internal const string CreateToolhelp32SnapshotDllName = "kernel32.dll"; /*120*/ + internal const string Process32FirstDllName = "kernel32.dll"; /*121*/ + internal const string Process32NextDllName = "kernel32.dll"; /*122*/ #endif } } diff --git a/src/System.Management.Automation/utils/PlatformInvokes.cs b/src/System.Management.Automation/utils/PlatformInvokes.cs index 95abb623d34..6416092eed0 100644 --- a/src/System.Management.Automation/utils/PlatformInvokes.cs +++ b/src/System.Management.Automation/utils/PlatformInvokes.cs @@ -736,6 +736,68 @@ internal enum StandardHandleId : uint [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern IntPtr GetStdHandle(uint handleId); +#endif + + #endregion + + #region CreateToolhelp32Snapshot + +#if !UNIX + + [DllImport(PinvokeDllNames.CreateToolhelp32SnapshotDllName, SetLastError = true)] + internal static extern SafeSnapshotHandle CreateToolhelp32Snapshot(SnapshotFlags flags, uint id); + [DllImport(PinvokeDllNames.Process32FirstDllName, SetLastError = true)] + internal static extern bool Process32First(SafeSnapshotHandle hSnapshot, ref PROCESSENTRY32 lppe); + [DllImport(PinvokeDllNames.Process32NextDllName, SetLastError = true)] + internal static extern bool Process32Next(SafeSnapshotHandle hSnapshot, ref PROCESSENTRY32 lppe); + + internal sealed class SafeSnapshotHandle : SafeHandleMinusOneIsInvalid + { + internal SafeSnapshotHandle() : base(true) + { + } + + [SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)] + internal SafeSnapshotHandle(IntPtr handle) : base(true) + { + base.SetHandle(handle); + } + + protected override bool ReleaseHandle() + { + return CloseHandle(base.handle); + } + } + + [Flags] + internal enum SnapshotFlags : uint + { + HeapList = 0x00000001, + Process = 0x00000002, + Thread = 0x00000004, + Module = 0x00000008, + Module32 = 0x00000010, + All = (HeapList | Process | Thread | Module), + Inherit = 0x80000000, + NoHeaps = 0x40000000 + } + [StructLayout(LayoutKind.Sequential)] + internal struct PROCESSENTRY32 + { + public uint dwSize; + public uint cntUsage; + public uint th32ProcessID; + public IntPtr th32DefaultHeapID; + public uint th32ModuleID; + public uint cntThreads; + public uint th32ParentProcessID; + public int pcPriClassBase; + public uint dwFlags; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string szExeFile; + }; + + internal const int ERROR_NO_MORE_FILES = 0x12; + #endif #endregion diff --git a/src/System.Management.Automation/utils/PsUtils.cs b/src/System.Management.Automation/utils/PsUtils.cs index 33bfeaa7221..c06ab86345c 100644 --- a/src/System.Management.Automation/utils/PsUtils.cs +++ b/src/System.Management.Automation/utils/PsUtils.cs @@ -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) /// /// 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. /// /// /// The process we want to find the /// parent of 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 processCollection = - cimSession.QueryInstances("root/cimv2", "WQL", wmiQuery); - - int parentPid = - processCollection.Select( - cimProcess => - Convert.ToInt32(cimProcess.CimInstanceProperties["ParentProcessId"].Value, - CultureInfo.CurrentCulture)).FirstOrDefault(); + int parentPid = 0; - if (parentPid == 0) - return null; +#if !UNIX + PlatformInvokes.PROCESSENTRY32 pe32 = new PlatformInvokes.PROCESSENTRY32 { }; + pe32.dwSize = (uint)ClrFacade.SizeOf(); - 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; } } diff --git a/test/powershell/Language/Scripting/NativeExecution/NativeCommandProcessor.Tests.ps1 b/test/powershell/Language/Scripting/NativeExecution/NativeCommandProcessor.Tests.ps1 new file mode 100644 index 00000000000..d39fb55f2fe --- /dev/null +++ b/test/powershell/Language/Scripting/NativeExecution/NativeCommandProcessor.Tests.ps1 @@ -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 + } +} diff --git a/test/tools/CreateChildProcess/CreateChildProcess.cs b/test/tools/CreateChildProcess/CreateChildProcess.cs new file mode 100644 index 00000000000..c37272cf63b --- /dev/null +++ b/test/tools/CreateChildProcess/CreateChildProcess.cs @@ -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; + child.Start(); + } + } + // sleep is needed so the process doesn't exit before the test case kill it + Thread.Sleep(100000); + } + } +} diff --git a/test/tools/CreateChildProcess/project.json b/test/tools/CreateChildProcess/project.json new file mode 100644 index 00000000000..ec9f31427b3 --- /dev/null +++ b/test/tools/CreateChildProcess/project.json @@ -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": { } + } +}