Add support to ShellExecute in powershell core when it's running on Windows full SKUs (#3281)

* Add ShellExecute support to PowerShell core on windows full desktop
* Add tests for Start-Process and update existing related tests
This commit is contained in:
Dongbo Wang
2017-03-14 11:11:41 -07:00
committed by Travis Plunk
parent 438d8d9359
commit 7f83c48ca5
8 changed files with 588 additions and 377 deletions
@@ -1542,6 +1542,18 @@ namespace Microsoft.PowerShell.Commands
private ManualResetEvent _waithandle = null;
private bool _isDefaultSetParameterSpecified = false;
private bool _useShellExecute;
private readonly bool _isOnFullWinSku;
/// <summary>
/// Constructor
/// </summary>
public StartProcessCommand()
{
_isOnFullWinSku = Platform.IsWindows && !Platform.IsNanoServer && !Platform.IsIoT;
_useShellExecute = _isOnFullWinSku;
}
#region Parameters
/// <summary>
@@ -1692,13 +1704,9 @@ namespace Microsoft.PowerShell.Commands
[ValidateNotNullOrEmpty]
public string Verb { get; set; }
#if !CORECLR
/// <summary>
/// Window style of the process window
/// </summary>
/// <remarks>
/// The 'WindowStyle' is not supported in CoreCLR
/// </remarks>
[Parameter]
[ValidateNotNullOrEmpty]
public ProcessWindowStyle WindowStyle
@@ -1712,7 +1720,6 @@ namespace Microsoft.PowerShell.Commands
}
private ProcessWindowStyle _windowstyle = ProcessWindowStyle.Normal;
private bool _windowstyleSpecified = false;
#endif
/// <summary>
/// wait for th eprocess to terminate
@@ -1735,29 +1742,49 @@ namespace Microsoft.PowerShell.Commands
}
private SwitchParameter _UseNewEnvironment;
private StreamWriter _outputWriter;
private StreamWriter _errorWriter;
#endregion
#region overrides
/// <summary>
///
/// BeginProcessing
/// </summary>
protected override void BeginProcessing()
{
#if CORECLR
if(this.ParameterSetName.Equals("UseShellExecute"))
string message = string.Empty;
// -Verb and -WindowStyle are not supported on non-Windows platforms as well as Windows headless SKUs
if (_isOnFullWinSku)
{
String errorMessage = StringUtil.Format(ProcessResources.ParameterNotSupportedOnPSEdition, "-Verb", "Start-Process");
ErrorRecord er = new ErrorRecord(new NotSupportedException(errorMessage), "NotSupportedException", ErrorCategory.NotImplemented, null);
ThrowTerminatingError(er);
// Parameters '-NoNewWindow' and '-WindowStyle' are both valid on full windows SKUs.
if (_nonewwindow && _windowstyleSpecified)
{
message = StringUtil.Format(ProcessResources.ContradictParametersSpecified, "-NoNewWindow", "-WindowStyle");
ErrorRecord er = new ErrorRecord(new InvalidOperationException(message), "InvalidOperationException", ErrorCategory.InvalidOperation, null);
WriteError(er);
return;
}
}
#endif
else
{
if (this.ParameterSetName.Equals("UseShellExecute"))
{
message = StringUtil.Format(ProcessResources.ParameterNotSupportedOnPSEdition, "-Verb", "Start-Process");
}
else if (_windowstyleSpecified)
{
message = StringUtil.Format(ProcessResources.ParameterNotSupportedOnPSEdition, "-WindowStyle", "Start-Process");
}
if (!string.IsNullOrEmpty(message))
{
ErrorRecord er = new ErrorRecord(new NotSupportedException(message), "NotSupportedException", ErrorCategory.NotImplemented, null);
ThrowTerminatingError(er);
}
}
//create an instance of the ProcessStartInfo Class
ProcessStartInfo startInfo = new ProcessStartInfo();
string message = String.Empty;
//Path = Mandatory parameter -> Will not be empty.
try
@@ -1809,6 +1836,7 @@ namespace Microsoft.PowerShell.Commands
{
if (_isDefaultSetParameterSpecified)
{
_useShellExecute = false;
startInfo.UseShellExecute = false;
}
@@ -1819,16 +1847,7 @@ namespace Microsoft.PowerShell.Commands
LoadEnvironmentVariable(startInfo, Environment.GetEnvironmentVariables(EnvironmentVariableTarget.Machine));
LoadEnvironmentVariable(startInfo, Environment.GetEnvironmentVariables(EnvironmentVariableTarget.User));
}
#if !CORECLR // 'WindowStyle' not supported in CoreCLR
if (_nonewwindow && _windowstyleSpecified)
{
message = StringUtil.Format(ProcessResources.ContradictParametersSpecified, "-NoNewWindow", "-WindowStyle");
ErrorRecord er = new ErrorRecord(new InvalidOperationException(message), "InvalidOperationException", ErrorCategory.InvalidOperation, null);
WriteError(er);
return;
}
#if !CORECLR
//WindowStyle
startInfo.WindowStyle = _windowstyle;
#endif
@@ -1837,13 +1856,10 @@ namespace Microsoft.PowerShell.Commands
{
startInfo.CreateNoWindow = _nonewwindow;
}
#if !UNIX
//LoadUserProfile.
if (Platform.IsWindows)
{
startInfo.LoadUserProfile = _loaduserprofile;
}
startInfo.LoadUserProfile = _loaduserprofile;
#endif
if (_credential != null)
{
//Gets NetworkCredentials
@@ -1919,47 +1935,19 @@ namespace Microsoft.PowerShell.Commands
}
}
}
#if !CORECLR // 'UseShellExecute' is not supported in CoreCLR
#if !CORECLR // Properties 'Verb' and 'WindowStyle' are missing in CoreCLR
else if (ParameterSetName.Equals("UseShellExecute"))
{
startInfo.UseShellExecute = true;
//Verb
if (Verb != null)
{
startInfo.Verb = Verb;
}
if (Verb != null) { startInfo.Verb = Verb; }
//WindowStyle
startInfo.WindowStyle = _windowstyle;
}
#endif
//Starts the Process
Process process;
if (Platform.IsWindows)
{
process = start(startInfo);
}
else
{
process = new Process();
process.StartInfo = startInfo;
SetupInputOutputRedirection(process);
process.Start();
if (process.StartInfo.RedirectStandardOutput)
{
process.BeginOutputReadLine();
}
if (process.StartInfo.RedirectStandardError)
{
process.BeginErrorReadLine();
}
if (process.StartInfo.RedirectStandardInput)
{
WriteToStandardInput(process);
}
}
//Wait and Passthru Implementation.
Process process = Start(startInfo);
//Wait and Passthru Implementation.
if (PassThru.IsPresent)
{
if (process != null)
@@ -1980,29 +1968,26 @@ namespace Microsoft.PowerShell.Commands
{
if (!process.HasExited)
{
if (Platform.IsWindows)
{
_waithandle = new ManualResetEvent(false);
#if UNIX
process.WaitForExit();
#else
_waithandle = new ManualResetEvent(false);
// Create and start the job object
ProcessCollection jobObject = new ProcessCollection();
if (jobObject.AssignProcessToJobObject(process))
{
// Wait for the job object to finish
jobObject.WaitOne(_waithandle);
}
else if (!process.HasExited)
{
// WinBlue: 27537 Start-Process -Wait doesn't work in a remote session on Windows 7 or lower.
process.Exited += new EventHandler(myProcess_Exited);
process.EnableRaisingEvents = true;
process.WaitForExit();
}
}
else
// Create and start the job object
ProcessCollection jobObject = new ProcessCollection();
if (jobObject.AssignProcessToJobObject(process))
{
// Wait for the job object to finish
jobObject.WaitOne(_waithandle);
}
else if (!process.HasExited)
{
// WinBlue: 27537 Start-Process -Wait doesn't work in a remote session on Windows 7 or lower.
process.Exited += new EventHandler(myProcess_Exited);
process.EnableRaisingEvents = true;
process.WaitForExit();
}
#endif
}
}
else
@@ -2024,11 +2009,35 @@ namespace Microsoft.PowerShell.Commands
}
}
#endregion
#region IDisposable Overrides
/// <summary>
/// Dispose WaitHandle used to honor -Wait parameter
/// </summary>
public void Dispose()
{
Dispose(true);
System.GC.SuppressFinalize(this);
}
private void Dispose(bool isDisposing)
{
if (_waithandle != null)
{
_waithandle.Dispose();
_waithandle = null;
}
}
#endregion
#region Private Methods
/// <summary>
/// When Process exits the wait handle is set.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void myProcess_Exited(object sender, System.EventArgs e)
{
if (_waithandle != null)
@@ -2037,7 +2046,11 @@ namespace Microsoft.PowerShell.Commands
}
}
#region Private Methods
private string ResolveFilePath(string path)
{
string filepath = PathUtils.ResolveFilePath(path, this);
return filepath;
}
private void LoadEnvironmentVariable(ProcessStartInfo startinfo, IDictionary EnvironmentVariables)
{
@@ -2059,6 +2072,43 @@ namespace Microsoft.PowerShell.Commands
}
}
private Process Start(ProcessStartInfo startInfo)
{
#if UNIX
Process process = new Process() { StartInfo = startInfo };
SetupInputOutputRedirection(process);
process.Start();
if (process.StartInfo.RedirectStandardOutput)
{
process.BeginOutputReadLine();
}
if (process.StartInfo.RedirectStandardError)
{
process.BeginErrorReadLine();
}
if (process.StartInfo.RedirectStandardInput)
{
WriteToStandardInput(process);
}
return process;
#else
Process process = null;
if (_useShellExecute)
{
process = StartWithShellExecute(startInfo);
}
else
{
process = StartWithCreateProcess(startInfo);
}
return process;
#endif
}
#if UNIX
private StreamWriter _outputWriter;
private StreamWriter _errorWriter;
private void StdOutputHandler(object sendingProcess, DataReceivedEventArgs outLine)
{
if (!String.IsNullOrEmpty(outLine.Data))
@@ -2156,7 +2206,89 @@ namespace Microsoft.PowerShell.Commands
}
writer.Dispose();
}
#else
private SafeFileHandle GetSafeFileHandleForRedirection(string RedirectionPath, uint dwCreationDisposition)
{
System.IntPtr hFileHandle = System.IntPtr.Zero;
ProcessNativeMethods.SECURITY_ATTRIBUTES lpSecurityAttributes = new ProcessNativeMethods.SECURITY_ATTRIBUTES();
hFileHandle = ProcessNativeMethods.CreateFileW(RedirectionPath,
ProcessNativeMethods.GENERIC_READ | ProcessNativeMethods.GENERIC_WRITE,
ProcessNativeMethods.FILE_SHARE_WRITE | ProcessNativeMethods.FILE_SHARE_READ,
lpSecurityAttributes,
dwCreationDisposition,
ProcessNativeMethods.FILE_ATTRIBUTE_NORMAL,
System.IntPtr.Zero);
if (hFileHandle == System.IntPtr.Zero)
{
int error = Marshal.GetLastWin32Error();
Win32Exception win32ex = new Win32Exception(error);
string message = StringUtil.Format(ProcessResources.InvalidStartProcess, win32ex.Message);
ErrorRecord er = new ErrorRecord(new InvalidOperationException(message), "InvalidOperationException", ErrorCategory.InvalidOperation, null);
ThrowTerminatingError(er);
}
SafeFileHandle sf = new SafeFileHandle(hFileHandle, true);
return sf;
}
private static StringBuilder BuildCommandLine(string executableFileName, string arguments)
{
StringBuilder builder = new StringBuilder();
string str = executableFileName.Trim();
bool flag = str.StartsWith("\"", StringComparison.Ordinal) && str.EndsWith("\"", StringComparison.Ordinal);
if (!flag)
{
builder.Append("\"");
}
builder.Append(str);
if (!flag)
{
builder.Append("\"");
}
if (!string.IsNullOrEmpty(arguments))
{
builder.Append(" ");
builder.Append(arguments);
}
return builder;
}
private static byte[] ConvertEnvVarsToByteArray(
#if CORECLR
IDictionary<string, string> sd)
#else
StringDictionary sd)
#endif
{
string[] array = new string[sd.Count];
byte[] bytes = null;
sd.Keys.CopyTo(array, 0);
string[] strArray2 = new string[sd.Count];
sd.Values.CopyTo(strArray2, 0);
Array.Sort(array, strArray2, StringComparer.OrdinalIgnoreCase);
StringBuilder builder = new StringBuilder();
for (int i = 0; i < sd.Count; i++)
{
builder.Append(array[i]);//
builder.Append('=');
builder.Append(strArray2[i]);
builder.Append('\0');
}
builder.Append('\0');
// Use Unicode encoding
bytes = Encoding.Unicode.GetBytes(builder.ToString());
if (bytes.Length > 0xffff)
{
throw new InvalidOperationException("EnvironmentBlockTooLong");
}
return bytes;
}
/// <summary>
/// This method will be used on all windows platforms, both full desktop and headless SKUs.
/// </summary>
private Process StartWithCreateProcess(ProcessStartInfo startinfo)
{
ProcessNativeMethods.STARTUPINFO lpStartupInfo = new ProcessNativeMethods.STARTUPINFO();
@@ -2220,11 +2352,8 @@ namespace Microsoft.PowerShell.Commands
//STARTF_USESHOWWINDOW
lpStartupInfo.dwFlags |= 0x00000001;
#if CORECLR
//SW_SHOWNORMAL
lpStartupInfo.wShowWindow = 1;
#else
switch (startinfo.WindowStyle)
// On headless SKUs like NanoServer and IoT, window style can only be the default value 'Normal'.
switch (WindowStyle)
{
case ProcessWindowStyle.Normal:
//SW_SHOWNORMAL
@@ -2243,7 +2372,6 @@ namespace Microsoft.PowerShell.Commands
lpStartupInfo.wShowWindow = 0;
break;
}
#endif
}
// Create the new process suspended so we have a chance to get a corresponding Process object in case it terminates quickly.
@@ -2255,13 +2383,9 @@ namespace Microsoft.PowerShell.Commands
{
if (this.UseNewEnvironment)
{
bool unicode = false;
if (ProcessManager.IsNt)
{
creationFlags |= 0x400;
unicode = true;
}
pinnedEnvironmentBlock = GCHandle.Alloc(EnvironmentBlock.ToByteArray(environmentVars, unicode), GCHandleType.Pinned);
// All Windows Operating Systems that we support are Windows NT systems, so we use Unicode for environment.
creationFlags |= 0x400;
pinnedEnvironmentBlock = GCHandle.Alloc(ConvertEnvVarsToByteArray(environmentVars), GCHandleType.Pinned);
AddressOfEnvironmentBlock = pinnedEnvironmentBlock.AddrOfPinnedObject();
}
}
@@ -2353,176 +2477,33 @@ namespace Microsoft.PowerShell.Commands
}
}
/// <summary>
/// This method will be used only on Windows full desktop.
/// </summary>
private Process StartWithShellExecute(ProcessStartInfo startInfo)
{
string message = String.Empty;
Process result = null;
try
{
#if CORECLR
result = ShellExecuteHelper.Start(startInfo, WindowStyle, Verb);
#else
result = Process.Start(startInfo);
#endif
}
catch (Win32Exception ex)
{
message = StringUtil.Format(ProcessResources.InvalidStartProcess, ex.Message);
string message = StringUtil.Format(ProcessResources.InvalidStartProcess, ex.Message);
ErrorRecord er = new ErrorRecord(new InvalidOperationException(message), "InvalidOperationException", ErrorCategory.InvalidOperation, null);
ThrowTerminatingError(er);
}
return result;
return result;
}
private Process start(ProcessStartInfo startInfo)
{
Process process = null;
if (startInfo.UseShellExecute)
{
process = StartWithShellExecute(startInfo);
}
else
{
process = StartWithCreateProcess(startInfo);
}
return process;
}
#endregion
#endregion
#region IDisposable Overrides
/// <summary>
/// Dispose WaitHandle used to honor -Wait parameter
/// </summary>
public void Dispose()
{
Dispose(true);
System.GC.SuppressFinalize(this);
}
private void Dispose(bool isDisposing)
{
if (_waithandle != null)
{
_waithandle.Dispose();
_waithandle = null;
}
}
#endregion
#region Private Methods
private string ResolveFilePath(string path)
{
string filepath = PathUtils.ResolveFilePath(path, this);
return filepath;
}
private SafeFileHandle GetSafeFileHandleForRedirection(string RedirectionPath, uint dwCreationDisposition)
{
System.IntPtr hFileHandle = System.IntPtr.Zero;
ProcessNativeMethods.SECURITY_ATTRIBUTES lpSecurityAttributes = new ProcessNativeMethods.SECURITY_ATTRIBUTES();
hFileHandle = ProcessNativeMethods.CreateFileW(RedirectionPath,
ProcessNativeMethods.GENERIC_READ | ProcessNativeMethods.GENERIC_WRITE,
ProcessNativeMethods.FILE_SHARE_WRITE | ProcessNativeMethods.FILE_SHARE_READ,
lpSecurityAttributes,
dwCreationDisposition,
ProcessNativeMethods.FILE_ATTRIBUTE_NORMAL,
System.IntPtr.Zero);
if (hFileHandle == System.IntPtr.Zero)
{
int error = Marshal.GetLastWin32Error();
Win32Exception win32ex = new Win32Exception(error);
string message = StringUtil.Format(ProcessResources.InvalidStartProcess, win32ex.Message);
ErrorRecord er = new ErrorRecord(new InvalidOperationException(message), "InvalidOperationException", ErrorCategory.InvalidOperation, null);
ThrowTerminatingError(er);
}
SafeFileHandle sf = new SafeFileHandle(hFileHandle, true);
return sf;
}
internal static class ProcessManager
{
// Properties
public static bool IsNt
{
get
{
#if CORECLR
return true;
#else
return (Environment.OSVersion.Platform == PlatformID.Win32NT);
#endif
}
}
}
internal static class EnvironmentBlock
{
#if CORECLR
public static byte[] ToByteArray(IDictionary<string, string> sd, bool unicode)
#else
public static byte[] ToByteArray(StringDictionary sd, bool unicode)
#endif
{
string[] array = new string[sd.Count];
byte[] bytes = null;
sd.Keys.CopyTo(array, 0);
string[] strArray2 = new string[sd.Count];
sd.Values.CopyTo(strArray2, 0);
Array.Sort(array, strArray2, StringComparer.OrdinalIgnoreCase);
StringBuilder builder = new StringBuilder();
for (int i = 0; i < sd.Count; i++)
{
builder.Append(array[i]);//
builder.Append('=');
builder.Append(strArray2[i]);
builder.Append('\0');
}
builder.Append('\0');
if (unicode)
{
bytes = Encoding.Unicode.GetBytes(builder.ToString());
}
else
{
bytes = ClrFacade.GetDefaultEncoding().GetBytes(builder.ToString());
}
if (bytes.Length > 0xffff)
{
throw new InvalidOperationException("EnvironmentBlockTooLong");
}
return bytes;
}
}
private static StringBuilder BuildCommandLine(string executableFileName, string arguments)
{
StringBuilder builder = new StringBuilder();
string str = executableFileName.Trim();
bool flag = str.StartsWith("\"", StringComparison.Ordinal) && str.EndsWith("\"", StringComparison.Ordinal);
if (!flag)
{
builder.Append("\"");
}
builder.Append(str);
if (!flag)
{
builder.Append("\"");
}
if (!string.IsNullOrEmpty(arguments))
{
builder.Append(" ");
builder.Append(arguments);
}
return builder;
}
#endregion
}
#if !UNIX
/// <summary>
/// ProcessCollection is a helper class used by Start-Process -Wait cmdlet to monitor the
/// child processes created by the main process hosted by the Start-process cmdlet.
@@ -2618,7 +2599,7 @@ namespace Microsoft.PowerShell.Commands
/// <summary>
/// A variable-length array of process identifiers returned by this call.
/// Array elements 0 through NumberOfProcessIdsInList– 1
/// Array elements 0 through NumberOfProcessIdsInList minus 1
/// contain valid process identifiers.
/// </summary>
public IntPtr ProcessIdList;
@@ -2841,20 +2822,6 @@ namespace Microsoft.PowerShell.Commands
}
}
[SuppressUnmanagedCodeSecurity]
internal sealed class SafeThreadHandle : SafeHandleZeroOrMinusOneIsInvalid
{
// Methods
internal SafeThreadHandle()
: base(true)
{
}
protected override bool ReleaseHandle()
{
return SafeNativeMethods.CloseHandle(base.handle);
}
}
[SuppressUnmanagedCodeSecurity, HostProtection(SecurityAction.LinkDemand, MayLeakOnAbort = true)]
internal sealed class SafeJobHandle : SafeHandleZeroOrMinusOneIsInvalid
{
@@ -2869,7 +2836,7 @@ namespace Microsoft.PowerShell.Commands
return SafeNativeMethods.CloseHandle(base.handle);
}
}
#endif
#endregion
#region ProcessCommandException
@@ -1,6 +1,7 @@
/********************************************************************++
Copyright (c) Microsoft Corporation. All rights reserved.
--********************************************************************/
#if !UNIX // Not built on Unix
using System;
using System.Collections.Generic;
@@ -2419,3 +2420,4 @@ namespace Microsoft.PowerShell.Commands
#endregion NativeMethods
}
#endif // Not built on Unix
@@ -545,6 +545,17 @@ namespace Microsoft.PowerShell.CoreClr.Stubs
NoZone = -1,
}
/// <summary>
/// Stub for ProcessWindowStyle
/// </summary>
public enum ProcessWindowStyle
{
Normal,
Hidden,
Minimized,
Maximized
}
/// <summary>
/// Stub for MailAddress
/// </summary>
@@ -20,10 +20,17 @@ using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Text;
using System.Security.Principal;
using TypeTable = System.Management.Automation.Runspaces.TypeTable;
#if CORECLR
using System.Diagnostics;
using Microsoft.Win32.SafeHandles;
using Microsoft.PowerShell.CoreClr.Stubs;
#else
using System.Security.Principal;
using PSUtils = System.Management.Automation.PsUtils;
#endif
namespace System.Management.Automation
{
@@ -1588,4 +1595,215 @@ namespace System.Management.Automation.Internal
}
}
}
#if CORECLR && !UNIX
/// <summary>
/// Helper to start process using ShellExecuteEx. This is used only in PowerShell Core on Full Windows.
/// </summary>
internal class ShellExecuteHelper
{
/// <summary>
/// Start a process using ShellExecuteEx with default settings about WindowStyle and Verb.
/// </summary>
internal static Process Start(ProcessStartInfo startInfo)
{
return Start(startInfo, ProcessWindowStyle.Normal, string.Empty);
}
/// <summary>
/// Start a process using ShellExecuteEx
/// </summary>
/// <remarks>
/// Quoted from MSDN:
/// "Because ShellExecuteEx can delegate execution to Shell extensions (data sources, context menu handlers, verb implementations)
/// that are activated using Component Object Model (COM), COM should be initialized before ShellExecuteEx is called. Some Shell
/// extensions require the COM single-threaded apartment (STA) type. In that case, COM should be initialized as shown here:
/// CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)
/// There are instances where ShellExecuteEx does not use one of these types of Shell extension and those instances would not require
/// COM to be initialized at all. Nonetheless, it is good practice to always initalize COM before using this function."
///
/// TODO: In .NET Core, managed threads are all eagerly initialized with MTA mode, so to call 'ShellExecuteEx' from a STA thread, we
/// need to create a native thread using 'CreateThread' function and initialize COM with STA on that thread. Currently we are calling
/// ShellExecuteEx directly on MTA thread, and it works for things like openning a folder in File Explorer, openning a PDF/DOCX file,
/// openning URL in web browser and etc, but it's not guaranteed to work in all ShellExecution scenarios. Github issue #2969 is used
/// to track the "invoke-on-STA-thread" work.
/// </remarks>
internal static Process Start(ProcessStartInfo startInfo, ProcessWindowStyle windowStyle, string verb)
{
var shellExecuteInfo = new NativeMethods.ShellExecuteInfo();
shellExecuteInfo.fMask = NativeMethods.SEE_MASK_NOCLOSEPROCESS;
shellExecuteInfo.fMask |= NativeMethods.SEE_MASK_FLAG_NO_UI;
switch (windowStyle)
{
case ProcessWindowStyle.Hidden:
shellExecuteInfo.nShow = NativeMethods.SW_HIDE;
break;
case ProcessWindowStyle.Minimized:
shellExecuteInfo.nShow = NativeMethods.SW_SHOWMINIMIZED;
break;
case ProcessWindowStyle.Maximized:
shellExecuteInfo.nShow = NativeMethods.SW_SHOWMAXIMIZED;
break;
default:
shellExecuteInfo.nShow = NativeMethods.SW_SHOWNORMAL;
break;
}
try
{
if (startInfo.FileName.Length != 0)
shellExecuteInfo.lpFile = Marshal.StringToHGlobalUni(startInfo.FileName);
if (!string.IsNullOrEmpty(verb))
shellExecuteInfo.lpVerb = Marshal.StringToHGlobalUni(verb);
if (startInfo.Arguments.Length != 0)
shellExecuteInfo.lpParameters = Marshal.StringToHGlobalUni(startInfo.Arguments);
if (startInfo.WorkingDirectory.Length != 0)
shellExecuteInfo.lpDirectory = Marshal.StringToHGlobalUni(startInfo.WorkingDirectory);
shellExecuteInfo.fMask |= NativeMethods.SEE_MASK_FLAG_DDEWAIT;
if (!NativeMethods.ShellExecuteEx(shellExecuteInfo))
{
int errorCode = Marshal.GetLastWin32Error();
if (errorCode == 0)
{
switch ((long)shellExecuteInfo.hInstApp)
{
case NativeMethods.SE_ERR_FNF: errorCode = NativeMethods.ERROR_FILE_NOT_FOUND; break;
case NativeMethods.SE_ERR_PNF: errorCode = NativeMethods.ERROR_PATH_NOT_FOUND; break;
case NativeMethods.SE_ERR_ACCESSDENIED: errorCode = NativeMethods.ERROR_ACCESS_DENIED; break;
case NativeMethods.SE_ERR_OOM: errorCode = NativeMethods.ERROR_NOT_ENOUGH_MEMORY; break;
case NativeMethods.SE_ERR_DDEFAIL:
case NativeMethods.SE_ERR_DDEBUSY:
case NativeMethods.SE_ERR_DDETIMEOUT: errorCode = NativeMethods.ERROR_DDE_FAIL; break;
case NativeMethods.SE_ERR_SHARE: errorCode = NativeMethods.ERROR_SHARING_VIOLATION; break;
case NativeMethods.SE_ERR_NOASSOC: errorCode = NativeMethods.ERROR_NO_ASSOCIATION; break;
case NativeMethods.SE_ERR_DLLNOTFOUND: errorCode = NativeMethods.ERROR_DLL_NOT_FOUND; break;
default: errorCode = (int)shellExecuteInfo.hInstApp; break;
}
}
if(errorCode == NativeMethods.ERROR_BAD_EXE_FORMAT || errorCode == NativeMethods.ERROR_EXE_MACHINE_TYPE_MISMATCH)
{
throw new Win32Exception(errorCode, "InvalidApplication");
}
throw new Win32Exception(errorCode);
}
}
finally
{
if (shellExecuteInfo.lpFile != (IntPtr)0) Marshal.FreeHGlobal(shellExecuteInfo.lpFile);
if (shellExecuteInfo.lpVerb != (IntPtr)0) Marshal.FreeHGlobal(shellExecuteInfo.lpVerb);
if (shellExecuteInfo.lpParameters != (IntPtr)0) Marshal.FreeHGlobal(shellExecuteInfo.lpParameters);
if (shellExecuteInfo.lpDirectory != (IntPtr)0) Marshal.FreeHGlobal(shellExecuteInfo.lpDirectory);
}
Process processToReturn = null;
if (shellExecuteInfo.hProcess != IntPtr.Zero)
{
var handle = new SafeProcessHandle(shellExecuteInfo.hProcess, true);
try {
int processId = GetProcessIdFromHandle(handle);
processToReturn = Process.GetProcessById(processId);
} finally {
handle.Dispose();
}
}
return processToReturn;
}
private static int GetProcessIdFromHandle(SafeProcessHandle processHandle)
{
NativeMethods.NtProcessBasicInfo info = new NativeMethods.NtProcessBasicInfo();
int status = NativeMethods.NtQueryInformationProcess(processHandle, NativeMethods.NtQueryProcessBasicInfo, info, (int)Marshal.SizeOf(info), null);
if (status != 0) {
throw new InvalidOperationException("CantGetProcessId", new Win32Exception(status));
}
// We should change the signature of this function and ID property in process class.
return info.UniqueProcessId.ToInt32();
}
private static class NativeMethods
{
public const int SEE_MASK_NOCLOSEPROCESS = 0x00000040;
public const int SEE_MASK_FLAG_NO_UI = 0x00000400;
public const int SEE_MASK_FLAG_DDEWAIT = 0x00000100;
public const int SW_HIDE = 0;
public const int SW_SHOWMINIMIZED = 2;
public const int SW_SHOWMAXIMIZED = 3;
public const int SW_SHOWNORMAL = 1;
public const int SE_ERR_FNF = 2;
public const int SE_ERR_PNF = 3;
public const int SE_ERR_ACCESSDENIED = 5;
public const int SE_ERR_OOM = 8;
public const int SE_ERR_DLLNOTFOUND = 32;
public const int SE_ERR_SHARE = 26;
public const int SE_ERR_DDETIMEOUT = 28;
public const int SE_ERR_DDEFAIL = 29;
public const int SE_ERR_DDEBUSY = 30;
public const int SE_ERR_NOASSOC = 31;
public const int ERROR_FILE_NOT_FOUND = 2;
public const int ERROR_PATH_NOT_FOUND = 3;
public const int ERROR_ACCESS_DENIED = 5;
public const int ERROR_NOT_ENOUGH_MEMORY = 8;
public const int ERROR_SHARING_VIOLATION = 32;
public const int ERROR_OPERATION_ABORTED = 995;
public const int ERROR_NO_ASSOCIATION = 1155;
public const int ERROR_DLL_NOT_FOUND = 1157;
public const int ERROR_DDE_FAIL = 1156;
public const int ERROR_BAD_EXE_FORMAT = 193;
public const int ERROR_EXE_MACHINE_TYPE_MISMATCH = 216;
public const int NtQueryProcessBasicInfo = 0;
[StructLayout(LayoutKind.Sequential)]
internal class ShellExecuteInfo
{
public int cbSize = 0;
public int fMask = 0;
public IntPtr hwnd = (IntPtr)0;
public IntPtr lpVerb = (IntPtr)0;
public IntPtr lpFile = (IntPtr)0;
public IntPtr lpParameters = (IntPtr)0;
public IntPtr lpDirectory = (IntPtr)0;
public int nShow = 0;
public IntPtr hInstApp = (IntPtr)0;
public IntPtr lpIDList = (IntPtr)0;
public IntPtr lpClass = (IntPtr)0;
public IntPtr hkeyClass = (IntPtr)0;
public int dwHotKey = 0;
public IntPtr hIcon = (IntPtr)0;
public IntPtr hProcess = (IntPtr)0;
public ShellExecuteInfo()
{
cbSize = Marshal.SizeOf(this);
}
}
[StructLayout(LayoutKind.Sequential)]
internal class NtProcessBasicInfo {
public int ExitStatus = 0;
public IntPtr PebBaseAddress = (IntPtr)0;
public IntPtr AffinityMask = (IntPtr)0;
public int BasePriority = 0;
public IntPtr UniqueProcessId = (IntPtr)0;
public IntPtr InheritedFromUniqueProcessId = (IntPtr)0;
}
[DllImport("Shell32", CharSet=CharSet.Unicode, SetLastError=true)]
public static extern bool ShellExecuteEx(ShellExecuteInfo info);
[DllImport("Ntdll", CharSet=CharSet.Unicode)]
public static extern int NtQueryInformationProcess(SafeProcessHandle processHandle, int query, NtProcessBasicInfo info, int size, int[] returnedSize);
}
}
#endif
}
@@ -638,20 +638,17 @@ namespace Microsoft.PowerShell.Commands
browserProcess.StartInfo.Arguments = uriToLaunch.OriginalString;
browserProcess.Start();
#elif CORECLR
// On FullCLR, ProcessStartInfo.UseShellExecute is true by default. This means that the shell will be used when starting the process.
// On CoreCLR, UseShellExecute is not supported. To work around this, we check if there is a default browser in the system.
// If there is, we lunch it to open the HelpURI. If there isn't, we error out.
string webBrowserPath = GetDefaultWebBrowser();
if (webBrowserPath == null)
if (Platform.IsNanoServer || Platform.IsIoT)
{
// We cannot open the URL in browser on headless SKUs.
wrapCaughtException = false;
exception = PSTraceSource.NewInvalidOperationException(HelpErrors.CannotLaunchURI, uriToLaunch.OriginalString);
}
else
{
browserProcess.StartInfo = new ProcessStartInfo(webBrowserPath);
browserProcess.StartInfo.Arguments = "\"" + uriToLaunch.OriginalString + "\"";
browserProcess.Start();
// We can call ShellExecute directly on Full Windows.
browserProcess.StartInfo.FileName = uriToLaunch.OriginalString;
ShellExecuteHelper.Start(browserProcess.StartInfo);
}
#else
browserProcess.StartInfo.FileName = uriToLaunch.OriginalString;
@@ -676,46 +673,6 @@ namespace Microsoft.PowerShell.Commands
}
}
#if !UNIX
/// <summary>
/// Gets the path to the default browser by querying the Windows registry.
/// </summary>
/// <returns></returns>
private string GetDefaultWebBrowser()
{
// Check if there is a default browser in the system.
const string httpRegkey = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice";
object progId = Registry.GetValue(httpRegkey, "ProgId", null);
if (progId != null)
{
// Query the registry to find the web browser path.
using (RegistryKey browserRegKey = Registry.ClassesRoot.OpenSubKey(progId + "\\shell\\open\\command", false))
{
string browserPath = browserRegKey?.GetValue(null)?.ToString().Replace(/* remove the quotes */ "\"", "");
if (!string.IsNullOrEmpty(browserPath))
{
const string exeExtension = ".exe";
if (!browserPath.EndsWith(exeExtension, StringComparison.OrdinalIgnoreCase))
{
// Remove any extra chars in the path after ".exe".
int extIndex = browserPath.LastIndexOf(exeExtension, StringComparison.OrdinalIgnoreCase);
browserPath = extIndex > 0 ? browserPath.Substring(0, extIndex + exeExtension.Length) : string.Empty;
}
// Make sure the path to the default browser exists.
if (File.Exists(browserPath))
{
return browserPath;
}
}
}
}
// By default, return null.
return null;
}
#endif
#endregion
private void HelpSystem_OnProgress(object sender, HelpProgressInfo arg)
@@ -1330,7 +1330,19 @@ namespace Microsoft.PowerShell.Commands
invokeProcess.StartInfo.Arguments = path;
invokeProcess.Start();
#elif CORECLR
throw new PlatformNotSupportedException();
try
{
// Try Process.Start first. This works for executables even on headless SKUs.
invokeProcess.StartInfo.FileName = path;
invokeProcess.Start();
}
catch (Win32Exception)
{
// If it's headless SKUs, rethrow.
if (Platform.IsNanoServer || Platform.IsIoT) { throw; }
// If it's full Windows, then try ShellExecute.
ShellExecuteHelper.Start(invokeProcess.StartInfo);
}
#else
invokeProcess.StartInfo.FileName = path;
invokeProcess.Start();
@@ -1,86 +1,120 @@
Import-Module $PSScriptRoot\..\..\Common\Test.Helpers.psm1
Describe "Start-Process" -Tags @("CI","SLOW") {
$pingCommand = (Get-Command -CommandType Application ping)[0].Definition
$pingDirectory = Split-Path $pingCommand -Parent
$tempFile = Join-Path -Path $TestDrive -ChildPath PSTest
$assetsFile = Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath assets) -ChildPath SortTest.txt
if ($IsWindows) {
$pingParam = "-n 2 localhost"
}
elseif ($IsLinux -Or $IsOSX) {
$pingParam = "-c 2 localhost"
BeforeAll {
$isNanoServer = [System.Management.Automation.Platform]::IsNanoServer
$isIot = [System.Management.Automation.Platform]::IsIoT
$isFullWin = $IsWindows -and !$isNanoServer -and !$isIot
$pingCommand = (Get-Command -CommandType Application ping)[0].Definition
$pingDirectory = Split-Path $pingCommand -Parent
$tempFile = Join-Path -Path $TestDrive -ChildPath PSTest
$assetsFile = Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath assets) -ChildPath SortTest.txt
if ($IsWindows) {
$pingParam = "-n 2 localhost"
}
elseif ($IsLinux -Or $IsOSX) {
$pingParam = "-c 2 localhost"
}
}
# Note that ProcessName may still be `powershell` due to dotnet/corefx#5378
# This has been fixed on Linux, but not on OS X
It "Should process arguments without error" {
$process = Start-Process ping -ArgumentList $pingParam -PassThru -RedirectStandardOutput "$TESTDRIVE/output"
$process = Start-Process ping -ArgumentList $pingParam -PassThru -RedirectStandardOutput "$TESTDRIVE/output"
$process.Length | Should Be 1
$process.Id | Should BeGreaterThan 1
# $process.ProcessName | Should Be "ping"
$process.Length | Should Be 1
$process.Id | Should BeGreaterThan 1
# $process.ProcessName | Should Be "ping"
}
It "Should work correctly when used with full path name" {
$process = Start-Process $pingCommand -ArgumentList $pingParam -PassThru -RedirectStandardOutput "$TESTDRIVE/output"
$process = Start-Process $pingCommand -ArgumentList $pingParam -PassThru -RedirectStandardOutput "$TESTDRIVE/output"
$process.Length | Should Be 1
$process.Id | Should BeGreaterThan 1
# $process.ProcessName | Should Be "ping"
$process.Length | Should Be 1
$process.Id | Should BeGreaterThan 1
# $process.ProcessName | Should Be "ping"
}
It "Should invoke correct path when used with FilePath argument" {
$process = Start-Process -FilePath $pingCommand -ArgumentList $pingParam -PassThru -RedirectStandardOutput "$TESTDRIVE/output"
$process = Start-Process -FilePath $pingCommand -ArgumentList $pingParam -PassThru -RedirectStandardOutput "$TESTDRIVE/output"
$process.Length | Should Be 1
$process.Id | Should BeGreaterThan 1
# $process.ProcessName | Should Be "ping"
$process.Length | Should Be 1
$process.Id | Should BeGreaterThan 1
# $process.ProcessName | Should Be "ping"
}
It "Should wait for command completion if used with Wait argument" {
$process = Start-Process ping -ArgumentList $pingParam -Wait -PassThru -RedirectStandardOutput "$TESTDRIVE/output"
$process = Start-Process ping -ArgumentList $pingParam -Wait -PassThru -RedirectStandardOutput "$TESTDRIVE/output"
}
It "Should work correctly with WorkingDirectory argument" {
$process = Start-Process ping -WorkingDirectory $pingDirectory -ArgumentList $pingParam -PassThru -RedirectStandardOutput "$TESTDRIVE/output"
$process = Start-Process ping -WorkingDirectory $pingDirectory -ArgumentList $pingParam -PassThru -RedirectStandardOutput "$TESTDRIVE/output"
$process.Length | Should Be 1
$process.Id | Should BeGreaterThan 1
# $process.ProcessName | Should Be "ping"
$process.Length | Should Be 1
$process.Id | Should BeGreaterThan 1
# $process.ProcessName | Should Be "ping"
}
It "Should should handle stderr redirection without error" {
$process = Start-Process ping -ArgumentList $pingParam -PassThru -RedirectStandardError $tempFile -RedirectStandardOutput "$TESTDRIVE/output"
It "Should handle stderr redirection without error" {
$process = Start-Process ping -ArgumentList $pingParam -PassThru -RedirectStandardError $tempFile -RedirectStandardOutput "$TESTDRIVE/output"
$process.Length | Should Be 1
$process.Id | Should BeGreaterThan 1
# $process.ProcessName | Should Be "ping"
$process.Length | Should Be 1
$process.Id | Should BeGreaterThan 1
# $process.ProcessName | Should Be "ping"
}
It "Should should handle stdout redirection without error" {
$process = Start-Process ping -ArgumentList $pingParam -Wait -RedirectStandardOutput $tempFile
$dirEntry = get-childitem $tempFile
$dirEntry.Length | Should BeGreaterThan 0
It "Should handle stdout redirection without error" {
$process = Start-Process ping -ArgumentList $pingParam -Wait -RedirectStandardOutput $tempFile
$dirEntry = get-childitem $tempFile
$dirEntry.Length | Should BeGreaterThan 0
}
# Marking this test 'pending' to unblock daily builds. Filed issue : https://github.com/PowerShell/PowerShell/issues/2396
It "Should should handle stdin redirection without error" -Pending {
$process = Start-Process sort -Wait -RedirectStandardOutput $tempFile -RedirectStandardInput $assetsFile
$dirEntry = get-childitem $tempFile
$dirEntry.Length | Should BeGreaterThan 0
It "Should handle stdin redirection without error" -Pending {
$process = Start-Process sort -Wait -RedirectStandardOutput $tempFile -RedirectStandardInput $assetsFile
$dirEntry = get-childitem $tempFile
$dirEntry.Length | Should BeGreaterThan 0
}
It "Should give an error when Verb parameter is used" -Skip:(-not $IsCoreClr) {
try
{
Start-Process -Verb runas -FilePath $pingCommand -ArgumentList $pingParam
throw "No Exception!"
}
catch
{
$_.FullyQualifiedErrorId | Should be "NotSupportedException,Microsoft.PowerShell.Commands.StartProcessCommand"
$_.Exception.Message | Should match '-Verb'
}
## -Verb is supported in PowerShell core on Windows full desktop.
It "Should give an error when -Verb parameter is used" -Skip:$isFullWin {
{ Start-Process -Verb runas -FilePath $pingCommand } | ShouldBeErrorId "NotSupportedException,Microsoft.PowerShell.Commands.StartProcessCommand"
}
## -WindowStyle is supported in PowerShell core on Windows full desktop.
It "Should give an error when -WindowStyle parameter is used" -Skip:$isFullWin {
{ Start-Process -FilePath $pingCommand -WindowStyle Normal } | ShouldBeErrorId "NotSupportedException,Microsoft.PowerShell.Commands.StartProcessCommand"
}
It "Should give an error when both -NoNewWindow and -WindowStyle are specified" -Skip:(!$isFullWin) {
{ Start-Process -FilePath $pingCommand -NoNewWindow -WindowStyle Normal -ErrorAction Stop } | ShouldBeErrorId "InvalidOperationException,Microsoft.PowerShell.Commands.StartProcessCommand"
}
It "Should start cmd.exe with Verb 'open' and WindowStyle 'Minimized'" -Skip:(!$isFullWin) {
$fileToWrite = Join-Path $TestDrive "VerbTest.txt"
$process = Start-Process cmd.exe -ArgumentList "/c echo abc > $fileToWrite" -Verb open -WindowStyle Minimized -PassThru
$process.Name | Should Be "cmd"
$process.WaitForExit()
Test-Path $fileToWrite | Should Be $true
}
It "Should start notepad.exe with ShellExecute" -Skip:(!$isFullWin) {
$process = Start-Process notepad -PassThru -WindowStyle Normal
$process.Name | Should Be "notepad"
$process | Stop-Process
}
It "Should open the application that associates with extension '.txt'" -Skip:(!$isFullWin) {
$txtFile = Join-Path $TestDrive "TxtTest.txt"
New-Item $txtFile -ItemType File -Force
$process = Start-Process $txtFile -PassThru -WindowStyle Normal
$process.Name | Should Not BeNullOrEmpty
$process.Id | Should BeGreaterThan 1
$process | Stop-Process
}
Remove-Item -Path $tempFile -Force
}
@@ -47,14 +47,16 @@ Describe "Invoke-Item on non-Windows" -Tags "CI" {
Describe "Invoke-Item tests on Windows" -Tags "CI","RequireAdminOnWindows" {
BeforeAll {
if ($IsWindows) {
$isNanoServer = [System.Management.Automation.Platform]::IsNanoServer
$isIot = [System.Management.Automation.Platform]::IsIoT
$isFullWin = $IsWindows -and !$isNanoServer -and !$isIot
if ($isFullWin) {
$testfilename = "testfile.!!testext!!"
$testfilepath = Join-Path $TestDrive $testfilename
$renamedtestfilename = "renamedtestfile.!!testext!!"
$renamedtestfilepath = Join-Path $TestDrive $renamedtestfilename
remove-item $testfilepath -ErrorAction SilentlyContinue
remove-item $renamedtestfilepath -ErrorAction SilentlyContinue
new-item $testfilepath | Out-Null
cmd.exe /c assoc .!!testext!!=!!testext!!.FileType | Out-Null
cmd.exe /c ftype !!testext!!.FileType=cmd.exe /c rename $testfilepath $renamedtestfilename | Out-Null
}
@@ -62,14 +64,21 @@ Describe "Invoke-Item tests on Windows" -Tags "CI","RequireAdminOnWindows" {
AfterAll {
if ($IsWindows) {
remove-item $testfilepath -ErrorAction SilentlyContinue
remove-item $renamedtestfilepath -ErrorAction SilentlyContinue
cmd.exe /c assoc !!testext!!=
cmd.exe /c ftype !!testext!!.FileType=
}
}
It "Should invoke a file without error on Windows w/o .NET Core" -Skip:(-not $IsWindows -or ($IsWindows -and $IsCoreCLR)) {
BeforeEach {
New-Item $testfilepath -ItemType File | Out-Null
}
AfterEach {
Remove-Item $testfilepath -ErrorAction SilentlyContinue
Remove-Item $renamedtestfilepath -ErrorAction SilentlyContinue
}
It "Should invoke a file without error on Windows full SKUs" -Skip:(-not $isFullWin) {
invoke-item $testfilepath
# Waiting subprocess start and rename file
{
@@ -82,7 +91,8 @@ Describe "Invoke-Item tests on Windows" -Tags "CI","RequireAdminOnWindows" {
} | Should Not throw
}
It "Should throw 'not supported' on Windows with .NET Core" -Skip:(-not ($IsWindows -and $IsCoreCLR)) {
{ Invoke-Item $testfilepath } | Should Throw "Operation is not supported on this platform."
It "Should start a file without error on Windows full SKUs" -Skip:(-not $isFullWin) {
Start-Process $testfilepath -Wait
Test-Path $renamedtestfilepath | Should Be $true
}
}