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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/********************************************************************++
Copyright (c) Microsoft Corporation. All rights reserved.
--********************************************************************/
#if !UNIX // Not built on Unix

using System;
using System.Collections.Generic;
Expand Down Expand Up @@ -2419,3 +2420,4 @@ public static extern bool QueryInformationJobObject(SafeHandle hJob, int JobObje
#endregion NativeMethods
}

#endif // Not built on Unix
11 changes: 11 additions & 0 deletions src/System.Management.Automation/CoreCLR/CorePsStub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,17 @@ public enum SecurityZone
NoZone = -1,
}

/// <summary>
/// Stub for ProcessWindowStyle
/// </summary>
public enum ProcessWindowStyle
{
Normal,
Hidden,
Minimized,
Maximized
}

/// <summary>
/// Stub for MailAddress
/// </summary>
Expand Down
220 changes: 219 additions & 1 deletion src/System.Management.Automation/engine/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -1588,4 +1595,215 @@ public static void SetTestHook(string property, bool value)
}
}
}

#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
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you have an issue number for this TODO?

Copy link
Member Author

Choose a reason for hiding this comment

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

#2969 will be used to track the "invoke-on-STA-thread" work.

Yes, it's recorded in the description of this PR. This Start method will be touched when fixing that issue and this TODO can be removed then.

Copy link
Member Author

Choose a reason for hiding this comment

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

I will add the issue number in the comment here as well.

/// 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;
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you please write a comment on what is happening right here? Please correct me if I am wrong. In the finally block you are freeing memory for lpfile, lpVerb, lpParameters, and lpDirectory. Then you use the pointer to the process to fetch the process using its id. My question is: Do we need to free anymore memory from shellExecuteInfo once the process is returned?

Copy link
Member Author

Choose a reason for hiding this comment

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

No more resource needs to be freed. This code mainly comes from .NET code here

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok. Thanks.

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
}
53 changes: 5 additions & 48 deletions src/System.Management.Automation/help/HelpCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -676,46 +673,6 @@ private void LaunchOnlineHelp(Uri uriToLaunch)
}
}

#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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

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

Have you tested this on a headless SKU?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes I did. It's able to start executable files, like .exe file, on NanoServer.

Copy link
Contributor

Choose a reason for hiding this comment

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

Awesome. Thanks!

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);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a possibility that this call will throw? If it does, how are you handling it?

Copy link
Member Author

@daxian-dbw daxian-dbw Mar 14, 2017

Choose a reason for hiding this comment

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

If it throws, then the exception just propagates and powershell will handle it. It's like what would happen when invokeProcess.Start() throws.

}
#else
invokeProcess.StartInfo.FileName = path;
invokeProcess.Start();
Expand Down
Loading