-
Notifications
You must be signed in to change notification settings - Fork 8.1k
Add support to ShellExecute in powershell core when it's running on Windows full SKUs #3281
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,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 | ||
| /// 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; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you tested this on a headless SKU?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| } | ||
| #else | ||
| invokeProcess.StartInfo.FileName = path; | ||
| invokeProcess.Start(); | ||
|
|
||
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's recorded in the description of this PR. This
Startmethod will be touched when fixing that issue and thisTODOcan be removed then.There was a problem hiding this comment.
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.