Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions build.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Member

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.

Copy link
Copy Markdown
Member Author

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?

Copy link
Copy Markdown
Member

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

} finally {
Pop-Location
}
}

}

function Start-PSPester {
Expand Down
25 changes: 25 additions & 0 deletions src/System.Management.Automation/CoreCLR/CorePsStub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,31 @@ public override bool IsInvalid
}
}

/// <summary>
/// Stub for SafeHandleMinusOneIsInvalid
/// </summary>
public abstract class SafeHandleMinusOneIsInvalid : SafeHandle
{
/// <summary>
/// Constructor
/// </summary>
protected SafeHandleMinusOneIsInvalid(bool ownsHandle)
: base(new IntPtr(-1), ownsHandle)
{
}

/// <summary>
/// IsInvalid
/// </summary>
public override bool IsInvalid
{
get
{
return handle == new IntPtr(-1);
}
}
}

#endregion SafeHandle_Related

#region Misc_Types
Expand Down
6 changes: 6 additions & 0 deletions src/System.Management.Automation/utils/PInvokeDllNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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*/
Expand Down Expand Up @@ -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
}
}
62 changes: 62 additions & 0 deletions src/System.Management.Automation/utils/PlatformInvokes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
89 changes: 49 additions & 40 deletions src/System.Management.Automation/utils/PsUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

using System.Collections;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
Expand All @@ -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
{
Expand Down Expand Up @@ -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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Working on how to test this indirectly

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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;
}
}

Expand Down
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
}
}
25 changes: 25 additions & 0 deletions test/tools/CreateChildProcess/CreateChildProcess.cs
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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the 100 second sleep?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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.

}
}
}
28 changes: 28 additions & 0 deletions test/tools/CreateChildProcess/project.json
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": { }
}
}