Make ShellExecuteEx use STA (#4362)

This commit is contained in:
Steve Lee 2017-08-01 11:27:40 -07:00 committed by Dongbo Wang
parent b7b3b69a74
commit e38f0e7149

View file

@ -23,13 +23,8 @@ using System.Text;
using TypeTable = System.Management.Automation.Runspaces.TypeTable;
#if CORECLR
using System.Diagnostics;
using Microsoft.Win32.SafeHandles;
#else
using System.Security.Principal;
using PSUtils = System.Management.Automation.PsUtils;
#endif
namespace System.Management.Automation
{
@ -242,43 +237,9 @@ namespace System.Management.Automation
internal static string GetApplicationBase(string shellId)
{
#if CORECLR
// Use the location of SMA.dll as the application base.
Assembly assembly = typeof(PSObject).GetTypeInfo().Assembly;
return Path.GetDirectoryName(assembly.Location);
#else
// This code path applies to Windows FullCLR inbox deployments. All CoreCLR
// implementations should use the location of SMA.dll since it must reside in PSHOME.
//
// try to get the path from the registry first
string result = GetApplicationBaseFromRegistry(shellId);
if (result != null)
{
return result;
}
// The default keys aren't installed, so try and use the entry assembly to
// get the application base. This works for managed apps like minishells...
Assembly assem = Assembly.GetEntryAssembly();
if (assem != null)
{
// For minishells, we just return the executable path.
return Path.GetDirectoryName(assem.Location);
}
// For unmanaged host apps, look for the SMA dll, if it's not GAC'ed then
// use it's location as the application base...
assem = typeof(PSObject).GetTypeInfo().Assembly;
string gacRootPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Microsoft.Net\\assembly");
if (!assem.Location.StartsWith(gacRootPath, StringComparison.OrdinalIgnoreCase))
{
// For other hosts.
return Path.GetDirectoryName(assem.Location);
}
// otherwise, just give up...
return "";
#endif
}
private static string[] s_productFolderDirectories;
@ -324,11 +285,7 @@ namespace System.Management.Automation
// TODO: #1184 will resolve this work-around
// Side-by-side versions of PowerShell use modules from their application base, not
// the system installation path.
#if CORECLR
progFileDir = Path.Combine(appBase, "Modules");
#else
progFileDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "WindowsPowerShell", "Modules");
#endif
if (!string.IsNullOrEmpty(progFileDir))
{
@ -522,64 +479,6 @@ namespace System.Management.Automation
return AllowedEditionValues.Contains(editionValue, StringComparer.OrdinalIgnoreCase);
}
#if !CORECLR
/// <summary>
/// Checks whether current monad session supports NetFrameworkVersion specified
/// by checkVersion. The specified version is treated as the the minimum required
/// version of .NET framework.
/// </summary>
/// <param name="checkVersion">Version to check</param>
/// <param name="higherThanKnownHighestVersion">true if version to check is higher than the known highest version</param>
/// <returns>true if supported, false otherwise</returns>
internal static bool IsNetFrameworkVersionSupported(Version checkVersion, out bool higherThanKnownHighestVersion)
{
higherThanKnownHighestVersion = false;
bool isSupported = false;
if (checkVersion == null)
{
return false;
}
// Construct a temporary version number with build number and revision number set to 0.
// This is done so as to re-use the version specifications in PSUtils.FrameworkRegistryInstallation
Version tempVersion = new Version(checkVersion.Major, checkVersion.Minor, 0, 0);
// Win8: 840038 - For any version above the highest known .NET version (4.5 for Windows 8), we can't make a call as to
// whether the requirement is satisfied or not because we can't detect that version of .NET.
// We end up erring on the side of app compat by letting it through.
// We will write a message in the Verbose output saying that we cannot detect the specified version of the .NET Framework.
if (checkVersion > PsUtils.FrameworkRegistryInstallation.KnownHighestNetFrameworkVersion)
{
isSupported = true;
higherThanKnownHighestVersion = true;
}
// For a script to have a valid .NET version, the specified version or atleast one of its compatible versions must be installed on the machine.
else if (PSUtils.FrameworkRegistryInstallation.CompatibleNetFrameworkVersions.ContainsKey(tempVersion))
{
if (PSUtils.FrameworkRegistryInstallation.IsFrameworkInstalled(tempVersion.Major, tempVersion.Minor, 0))
{
// If the specified version is installed on the machine, then we return true.
isSupported = true;
}
else
{
// If any of the compatible versions are installed on the machine, then we return true.
HashSet<Version> compatibleVersions = PSUtils.FrameworkRegistryInstallation.CompatibleNetFrameworkVersions[tempVersion];
foreach (Version compatibleVersion in compatibleVersions)
{
if (PSUtils.FrameworkRegistryInstallation.IsFrameworkInstalled(compatibleVersion.Major, compatibleVersion.Minor, 0))
{
isSupported = true;
break;
}
}
}
}
return isSupported;
}
#endif
#endregion
/// <summary>
@ -590,11 +489,7 @@ namespace System.Management.Automation
/// <summary>
/// This is used to construct the profile path.
/// </summary>
#if CORECLR
internal static string ProductNameForDirectory = Platform.IsInbox ? "WindowsPowerShell" : "PowerShell";
#else
internal const string ProductNameForDirectory = "WindowsPowerShell";
#endif
/// <summary>
/// The subdirectory of module paths
@ -1450,7 +1345,7 @@ namespace System.Management.Automation
internal static readonly UTF8Encoding utf8NoBom =
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
#if !CORECLR // TODO:CORECLR - WindowsIdentity.Impersonate() is not available. Use WindowsIdentity.RunImplemented to replace it.
#if !CORECLR // TODO:CORECLR - WindowsIdentity.Impersonate() is not available. Use WindowsIdentity.RunImpersonated to replace it.
/// <summary>
/// Queues a CLR worker thread with impersonation of provided Windows identity.
/// </summary>
@ -1600,12 +1495,21 @@ namespace System.Management.Automation.Internal
}
}
#if CORECLR && !UNIX
#if !UNIX
/// <summary>
/// Helper to start process using ShellExecuteEx. This is used only in PowerShell Core on Full Windows.
/// </summary>
internal class ShellExecuteHelper
{
private NativeMethods.ShellExecuteInfo _executeInfo;
private int _errorCode;
private bool _succeeded;
/// <summary>
/// Constructor for ShellExecuteHelper
/// </summary>
private ShellExecuteHelper(NativeMethods.ShellExecuteInfo executeInfo) { _executeInfo = executeInfo; }
/// <summary>
/// Start a process using ShellExecuteEx with default settings about WindowStyle and Verb.
/// </summary>
@ -1617,21 +1521,6 @@ namespace System.Management.Automation.Internal
/// <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();
@ -1666,34 +1555,17 @@ namespace System.Management.Automation.Internal
shellExecuteInfo.lpDirectory = Marshal.StringToHGlobalUni(startInfo.WorkingDirectory);
shellExecuteInfo.fMask |= NativeMethods.SEE_MASK_FLAG_DDEWAIT;
if (!NativeMethods.ShellExecuteEx(shellExecuteInfo))
ShellExecuteHelper helper = new ShellExecuteHelper(shellExecuteInfo);
if (!helper.ExecuteOnSTAThread())
{
int errorCode = Marshal.GetLastWin32Error();
if (errorCode == 0)
if(helper.ErrorCode == NativeMethods.ERROR_BAD_EXE_FORMAT || helper.ErrorCode == NativeMethods.ERROR_EXE_MACHINE_TYPE_MISMATCH)
{
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;
}
throw new Win32Exception(helper.ErrorCode, "InvalidApplication");
}
if(errorCode == NativeMethods.ERROR_BAD_EXE_FORMAT || errorCode == NativeMethods.ERROR_EXE_MACHINE_TYPE_MISMATCH)
else
{
throw new Win32Exception(errorCode, "InvalidApplication");
throw new Win32Exception(helper.ErrorCode);
}
throw new Win32Exception(errorCode);
}
}
finally
@ -1719,6 +1591,56 @@ namespace System.Management.Automation.Internal
return processToReturn;
}
private void ShellExecuteFunction()
{
if (!(_succeeded = NativeMethods.ShellExecuteEx(_executeInfo)))
{
_errorCode = Marshal.GetLastWin32Error();
if (_errorCode == 0)
{
switch ((long)_executeInfo.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)_executeInfo.hInstApp; break;
}
}
}
}
private bool ExecuteOnSTAThread()
{
if (Thread.CurrentThread.GetApartmentState() != System.Threading.ApartmentState.STA)
{
ThreadStart threadStart = new ThreadStart(this.ShellExecuteFunction);
Thread thread = new Thread(threadStart);
thread.SetApartmentState(System.Threading.ApartmentState.STA);
thread.Start();
thread.Join();
}
else
{
ShellExecuteFunction();
}
return _succeeded;
}
private int ErrorCode
{
get
{
return _errorCode;
}
}
private static int GetProcessIdFromHandle(SafeProcessHandle processHandle)
{
NativeMethods.NtProcessBasicInfo info = new NativeMethods.NtProcessBasicInfo();