From 0f135e1bb2e641539614b08e8eee9444403f5393 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 7 Mar 2017 17:32:23 -0800 Subject: [PATCH 1/3] Add ShellExecute support to powershell core on windows full desktop --- .../commands/management/Process.cs | 501 ++++++++---------- .../commands/management/Service.cs | 2 + .../CoreCLR/CorePsStub.cs | 11 + .../engine/Utils.cs | 219 +++++++- .../help/HelpCommands.cs | 53 +- .../namespaces/FileSystemProvider.cs | 14 +- 6 files changed, 483 insertions(+), 317 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Management/commands/management/Process.cs b/src/Microsoft.PowerShell.Commands.Management/commands/management/Process.cs index 5ccf67c46cf..27d7c12d885 100644 --- a/src/Microsoft.PowerShell.Commands.Management/commands/management/Process.cs +++ b/src/Microsoft.PowerShell.Commands.Management/commands/management/Process.cs @@ -1542,6 +1542,18 @@ public sealed class StartProcessCommand : PSCmdlet, IDisposable private ManualResetEvent _waithandle = null; private bool _isDefaultSetParameterSpecified = false; + private bool _useShellExecute; + private readonly bool _isOnFullWinSku; + + /// + /// Constructor + /// + public StartProcessCommand() + { + _isOnFullWinSku = Platform.IsWindows && !Platform.IsNanoServer && !Platform.IsIoT; + _useShellExecute = _isOnFullWinSku; + } + #region Parameters /// @@ -1692,13 +1704,9 @@ public string RedirectStandardOutput [ValidateNotNullOrEmpty] public string Verb { get; set; } -#if !CORECLR /// /// Window style of the process window /// - /// - /// The 'WindowStyle' is not supported in CoreCLR - /// [Parameter] [ValidateNotNullOrEmpty] public ProcessWindowStyle WindowStyle @@ -1712,7 +1720,6 @@ public ProcessWindowStyle WindowStyle } private ProcessWindowStyle _windowstyle = ProcessWindowStyle.Normal; private bool _windowstyleSpecified = false; -#endif /// /// wait for th eprocess to terminate @@ -1735,29 +1742,49 @@ public SwitchParameter UseNewEnvironment } private SwitchParameter _UseNewEnvironment; - private StreamWriter _outputWriter; - private StreamWriter _errorWriter; - #endregion #region overrides /// - /// + /// BeginProcessing /// 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 @@ protected override void BeginProcessing() { if (_isDefaultSetParameterSpecified) { + _useShellExecute = false; startInfo.UseShellExecute = false; } @@ -1819,16 +1847,7 @@ protected override void BeginProcessing() 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 @@ protected override void BeginProcessing() { 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 @@ protected override void BeginProcessing() } } } -#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 @@ protected override void BeginProcessing() { 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(); - } + // 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 + 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 @@ protected override void StopProcessing() } } + #endregion + + #region IDisposable Overrides + + /// + /// Dispose WaitHandle used to honor -Wait parameter + /// + 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 + /// /// When Process exits the wait handle is set. /// - /// - /// private void myProcess_Exited(object sender, System.EventArgs e) { if (_waithandle != null) @@ -2037,7 +2046,11 @@ private void myProcess_Exited(object sender, System.EventArgs e) } } - #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 @@ private void LoadEnvironmentVariable(ProcessStartInfo startinfo, IDictionary Env } } + 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 @@ private void WriteToStandardInput(Process p) } 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 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; + } + /// + /// This method will be used on all windows platforms, both full desktop and headless SKUs. + /// private Process StartWithCreateProcess(ProcessStartInfo startinfo) { ProcessNativeMethods.STARTUPINFO lpStartupInfo = new ProcessNativeMethods.STARTUPINFO(); @@ -2220,11 +2352,8 @@ private Process StartWithCreateProcess(ProcessStartInfo startinfo) //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 @@ private Process StartWithCreateProcess(ProcessStartInfo startinfo) 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 @@ private Process StartWithCreateProcess(ProcessStartInfo startinfo) { 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 @@ private Process StartWithCreateProcess(ProcessStartInfo startinfo) } } + /// + /// This method will be used only on Windows full desktop. + /// 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); - ErrorRecord er = new ErrorRecord(new InvalidOperationException(message), "InvalidOperationException", ErrorCategory.InvalidOperation, null); - ThrowTerminatingError(er); - } - 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 - - /// - /// Dispose WaitHandle used to honor -Wait parameter - /// - 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); + string message = StringUtil.Format(ProcessResources.InvalidStartProcess, ex.Message); ErrorRecord er = new ErrorRecord(new InvalidOperationException(message), "InvalidOperationException", ErrorCategory.InvalidOperation, null); ThrowTerminatingError(er); } - SafeFileHandle sf = new SafeFileHandle(hFileHandle, true); - return sf; + return result; } - - 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 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 /// /// 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 @@ internal struct JOBOBJECT_BASIC_PROCESS_ID_LIST /// /// A variable-length array of process identifiers returned by this call. - /// Array elements 0 through NumberOfProcessIdsInList– 1 + /// Array elements 0 through NumberOfProcessIdsInList� 1 /// contain valid process identifiers. /// public IntPtr ProcessIdList; @@ -2841,20 +2822,6 @@ private void Dispose(bool disposing) } } - [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 @@ protected override bool ReleaseHandle() return SafeNativeMethods.CloseHandle(base.handle); } } - +#endif #endregion #region ProcessCommandException diff --git a/src/Microsoft.PowerShell.Commands.Management/commands/management/Service.cs b/src/Microsoft.PowerShell.Commands.Management/commands/management/Service.cs index 9374d61c814..5fa68328835 100644 --- a/src/Microsoft.PowerShell.Commands.Management/commands/management/Service.cs +++ b/src/Microsoft.PowerShell.Commands.Management/commands/management/Service.cs @@ -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 @@ public static extern bool QueryInformationJobObject(SafeHandle hJob, int JobObje #endregion NativeMethods } +#endif // Not built on Unix diff --git a/src/System.Management.Automation/CoreCLR/CorePsStub.cs b/src/System.Management.Automation/CoreCLR/CorePsStub.cs index b4bc1c57a23..8c4bc8ba893 100644 --- a/src/System.Management.Automation/CoreCLR/CorePsStub.cs +++ b/src/System.Management.Automation/CoreCLR/CorePsStub.cs @@ -545,6 +545,17 @@ public enum SecurityZone NoZone = -1, } + /// + /// Stub for ProcessWindowStyle + /// + public enum ProcessWindowStyle + { + Normal, + Hidden, + Minimized, + Maximized + } + /// /// Stub for MailAddress /// diff --git a/src/System.Management.Automation/engine/Utils.cs b/src/System.Management.Automation/engine/Utils.cs index e701738d446..ae99f0f022f 100644 --- a/src/System.Management.Automation/engine/Utils.cs +++ b/src/System.Management.Automation/engine/Utils.cs @@ -20,10 +20,17 @@ 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,214 @@ public static void SetTestHook(string property, bool value) } } } + +#if CORECLR && !UNIX + /// + /// Helper to start process using ShellExecuteEx. This is used only in PowerShell Core on Full Windows. + /// + internal class ShellExecuteHelper + { + /// + /// Start a process using ShellExecuteEx with default settings about WindowStyle and Verb. + /// + internal static Process Start(ProcessStartInfo startInfo) + { + return Start(startInfo, ProcessWindowStyle.Normal, string.Empty); + } + + /// + /// Start a process using ShellExecuteEx + /// + /// + /// 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. + /// + 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 } diff --git a/src/System.Management.Automation/help/HelpCommands.cs b/src/System.Management.Automation/help/HelpCommands.cs index 3ed342367e7..857dd5af5d2 100644 --- a/src/System.Management.Automation/help/HelpCommands.cs +++ b/src/System.Management.Automation/help/HelpCommands.cs @@ -638,20 +638,17 @@ private void LaunchOnlineHelp(Uri uriToLaunch) 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 @@ private void LaunchOnlineHelp(Uri uriToLaunch) } } -#if !UNIX - /// - /// Gets the path to the default browser by querying the Windows registry. - /// - /// - 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) diff --git a/src/System.Management.Automation/namespaces/FileSystemProvider.cs b/src/System.Management.Automation/namespaces/FileSystemProvider.cs index e02d8551cc2..706893c5945 100644 --- a/src/System.Management.Automation/namespaces/FileSystemProvider.cs +++ b/src/System.Management.Automation/namespaces/FileSystemProvider.cs @@ -1330,7 +1330,19 @@ protected override void InvokeDefaultAction(string path) 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(); From 5e2beb8e5b166eeb68a0d4381bd77f08571489a1 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 7 Mar 2017 22:56:40 -0800 Subject: [PATCH 2/3] Add tests for Start-Process and update existing related tests --- .../Start-Process.Tests.ps1 | 134 +++++++++++------- .../Invoke-Item.Tests.ps1 | 28 ++-- 2 files changed, 103 insertions(+), 59 deletions(-) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/Start-Process.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/Start-Process.Tests.ps1 index 1b743e74cef..4864a5c8d37 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Management/Start-Process.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Management/Start-Process.Tests.ps1 @@ -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'" { + $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 } diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Invoke-Item.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Invoke-Item.Tests.ps1 index ec7124a1b56..ce25b0d34df 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Invoke-Item.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Invoke-Item.Tests.ps1 @@ -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 } } From c86a287726fe366895723dfcc7739f93038615ea Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 14 Mar 2017 09:50:51 -0700 Subject: [PATCH 3/3] Address review comments --- .../commands/management/Process.cs | 2 +- src/System.Management.Automation/engine/Utils.cs | 3 ++- .../Microsoft.PowerShell.Management/Start-Process.Tests.ps1 | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Management/commands/management/Process.cs b/src/Microsoft.PowerShell.Commands.Management/commands/management/Process.cs index 27d7c12d885..d0264581fc5 100644 --- a/src/Microsoft.PowerShell.Commands.Management/commands/management/Process.cs +++ b/src/Microsoft.PowerShell.Commands.Management/commands/management/Process.cs @@ -2599,7 +2599,7 @@ internal struct JOBOBJECT_BASIC_PROCESS_ID_LIST /// /// 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. /// public IntPtr ProcessIdList; diff --git a/src/System.Management.Automation/engine/Utils.cs b/src/System.Management.Automation/engine/Utils.cs index ae99f0f022f..f426f539412 100644 --- a/src/System.Management.Automation/engine/Utils.cs +++ b/src/System.Management.Automation/engine/Utils.cs @@ -1625,7 +1625,8 @@ internal static Process Start(ProcessStartInfo startInfo) /// 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. + /// 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. /// internal static Process Start(ProcessStartInfo startInfo, ProcessWindowStyle windowStyle, string verb) { diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/Start-Process.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/Start-Process.Tests.ps1 index 4864a5c8d37..7ed13ff0e5f 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Management/Start-Process.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Management/Start-Process.Tests.ps1 @@ -107,7 +107,7 @@ Describe "Start-Process" -Tags @("CI","SLOW") { $process | Stop-Process } - It "Should open the application that associates with extension '.txt'" { + 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