From 65baccc69637819d61df170da338aa2ceb7ed731 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 12 Mar 2019 15:30:28 -0700 Subject: [PATCH 1/3] Make 'Start-Job' throw terminating error when PowerShell is being hosted --- .../hostifaces/PowerShellProcessInstance.cs | 37 ++++++------------- .../remoting/commands/PSRemotingCmdlet.cs | 2 +- .../engine/remoting/commands/StartJob.cs | 36 ++++++++++++++++++ .../resources/RemotingErrorIdStrings.resx | 4 ++ 4 files changed, 52 insertions(+), 27 deletions(-) diff --git a/src/System.Management.Automation/engine/hostifaces/PowerShellProcessInstance.cs b/src/System.Management.Automation/engine/hostifaces/PowerShellProcessInstance.cs index e7ace3aad53..efa82711938 100644 --- a/src/System.Management.Automation/engine/hostifaces/PowerShellProcessInstance.cs +++ b/src/System.Management.Automation/engine/hostifaces/PowerShellProcessInstance.cs @@ -14,17 +14,18 @@ namespace System.Management.Automation.Runspaces /// public sealed class PowerShellProcessInstance : IDisposable { - #region Private Members + #region Fields private readonly ProcessStartInfo _startInfo; - private static readonly string s_PSExePath; private RunspacePool _runspacePool; private readonly object _syncObject = new object(); private bool _started; private bool _isDisposed; private bool _processExited; - #endregion Private Members + internal static readonly string PwshExePath; + + #endregion Fields #region Constructors @@ -33,11 +34,9 @@ public sealed class PowerShellProcessInstance : IDisposable static PowerShellProcessInstance() { #if UNIX - s_PSExePath = Path.Combine(Utils.DefaultPowerShellAppBase, - "pwsh"); + PwshExePath = Path.Combine(Utils.DefaultPowerShellAppBase, "pwsh"); #else - s_PSExePath = Path.Combine(Utils.DefaultPowerShellAppBase, - "pwsh.exe"); + PwshExePath = Path.Combine(Utils.DefaultPowerShellAppBase, "pwsh.exe"); #endif } @@ -49,16 +48,16 @@ static PowerShellProcessInstance() /// public PowerShellProcessInstance(Version powerShellVersion, PSCredential credential, ScriptBlock initializationScript, bool useWow64) { - string psWow64Path = s_PSExePath; + string psWow64Path = PwshExePath; if (useWow64) { string procArch = Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE"); - if ((!string.IsNullOrEmpty(procArch)) && (procArch.Equals("amd64", StringComparison.OrdinalIgnoreCase) || - procArch.Equals("ia64", StringComparison.OrdinalIgnoreCase))) + if (!string.IsNullOrEmpty(procArch) && (procArch.Equals("amd64", StringComparison.OrdinalIgnoreCase) || + procArch.Equals("ia64", StringComparison.OrdinalIgnoreCase))) { - psWow64Path = s_PSExePath.ToLowerInvariant().Replace("\\system32\\", "\\syswow64\\"); + psWow64Path = PwshExePath.ToLowerInvariant().Replace("\\system32\\", "\\syswow64\\"); if (!File.Exists(psWow64Path)) { @@ -71,21 +70,7 @@ public PowerShellProcessInstance(Version powerShellVersion, PSCredential credent } } -#if CORECLR string processArguments = " -s -NoLogo -NoProfile"; -#else - // Adding Version parameter to powershell - // Version parameter needs to go before all other parameters because the native layer looks for Version or - // PSConsoleFile parameters before parsing other parameters. - // The other parameters get parsed in the managed layer. - Version tempVersion = powerShellVersion ?? PSVersionInfo.PSVersion; - string processArguments = string.Format(CultureInfo.InvariantCulture, - "-Version {0}", new Version(tempVersion.Major, tempVersion.Minor)); - - processArguments = string.Format(CultureInfo.InvariantCulture, - "{0} -s -NoLogo -NoProfile", processArguments); - -#endif if (initializationScript != null) { @@ -103,7 +88,7 @@ public PowerShellProcessInstance(Version powerShellVersion, PSCredential credent // to 'false' in our use, we can ignore the 'WindowStyle' setting in the initialization below. _startInfo = new ProcessStartInfo { - FileName = useWow64 ? psWow64Path : s_PSExePath, + FileName = useWow64 ? psWow64Path : PwshExePath, Arguments = processArguments, UseShellExecute = false, RedirectStandardInput = true, diff --git a/src/System.Management.Automation/engine/remoting/commands/PSRemotingCmdlet.cs b/src/System.Management.Automation/engine/remoting/commands/PSRemotingCmdlet.cs index 63661d45c6f..d27be984575 100644 --- a/src/System.Management.Automation/engine/remoting/commands/PSRemotingCmdlet.cs +++ b/src/System.Management.Automation/engine/remoting/commands/PSRemotingCmdlet.cs @@ -781,7 +781,7 @@ public virtual Hashtable[] SSHConnection /// [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = InvokeCommandCommand.SSHHostParameterSet)] - public string Subsystem { get; set; } + public virtual string Subsystem { get; set; } #endregion diff --git a/src/System.Management.Automation/engine/remoting/commands/StartJob.cs b/src/System.Management.Automation/engine/remoting/commands/StartJob.cs index 7e8c160f9d5..b6156efb059 100644 --- a/src/System.Management.Automation/engine/remoting/commands/StartJob.cs +++ b/src/System.Management.Automation/engine/remoting/commands/StartJob.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.IO; using System.Management.Automation; using System.Management.Automation.Internal; using System.Management.Automation.Remoting; @@ -197,6 +198,22 @@ public override string KeyFilePath get { return null; } } + /// + /// Suppress HostName. + /// + public override string[] HostName + { + get { return null; } + } + + /// + /// Suppress Subsystem. + /// + public override string Subsystem + { + get { return null; } + } + #endregion /// @@ -548,6 +565,25 @@ public override object[] ArgumentList /// protected override void BeginProcessing() { + if (!File.Exists(PowerShellProcessInstance.PwshExePath)) + { + // The pwsh executable file is not found under $PSHOME. + // This means that PowerShell is currently being hosted in another application, + // and 'Start-Job' is not supported by design in that scenario. + string message = StringUtil.Format( + RemotingErrorIdStrings.IPCPwshExecutableNotFound, + PowerShellProcessInstance.PwshExePath); + + var exception = new PSNotSupportedException(message); + var errorRecord = new ErrorRecord( + exception, + "IPCPwshExecutableNotFound", + ErrorCategory.NotInstalled, + PowerShellProcessInstance.PwshExePath); + + ThrowTerminatingError(errorRecord); + } + CommandDiscovery.AutoloadModulesWithJobSourceAdapters(this.Context, this.CommandOrigin); if (ParameterSetName == DefinitionNameParameterSet) diff --git a/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx b/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx index e100c8babc3..daad2f6ad3b 100644 --- a/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx +++ b/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx @@ -787,6 +787,10 @@ Do you want to continue? The specified authentication mechanism "{0}" is not supported. Only "{1}" is supported for this operation. + + The pwsh executable cannot be found at "{0}". +Note that 'Start-Job' is not supported by design in scenarios where PowerShell is being hosted in other applications. Instead, usage of the 'ThreadJob' module is recommended in such scenarios. + The "{0}" executable file was not found. Verify that the WOW64 feature is installed. From 3ce785d8920403586f3c1b5374501d7d2422e6f2 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 12 Mar 2019 16:02:59 -0700 Subject: [PATCH 2/3] Add xUnit test --- test/xUnit/csharp/test_PowerShellAPI.cs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 test/xUnit/csharp/test_PowerShellAPI.cs diff --git a/test/xUnit/csharp/test_PowerShellAPI.cs b/test/xUnit/csharp/test_PowerShellAPI.cs new file mode 100644 index 00000000000..d147225aadf --- /dev/null +++ b/test/xUnit/csharp/test_PowerShellAPI.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Management.Automation; +using Xunit; + +namespace PSTests.Sequential +{ + // Not static because a test requires non-const variables + public class PowerShellHostingScenario + { + // Test that it does not throw an exception + [Fact] + public void TestStartJobThrowTerminatingException() + { + using (var ps = PowerShell.Create()) + { + ps.AddCommand("Start-Job").AddParameter("ScriptBlock", ScriptBlock.Create("1+1")); + Exception ex = Assert.Throws(() => ps.Invoke()); + Assert.IsType(ex.InnerException); + } + } + } +} From e08f1239b87c1d8f65e90c42a332a7d23ce9cf3d Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 13 Mar 2019 10:25:37 -0700 Subject: [PATCH 3/3] [Feature] Fix Codacy issue --- test/xUnit/csharp/test_PowerShellAPI.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/xUnit/csharp/test_PowerShellAPI.cs b/test/xUnit/csharp/test_PowerShellAPI.cs index d147225aadf..7a54f9008e0 100644 --- a/test/xUnit/csharp/test_PowerShellAPI.cs +++ b/test/xUnit/csharp/test_PowerShellAPI.cs @@ -8,17 +8,18 @@ namespace PSTests.Sequential { // Not static because a test requires non-const variables - public class PowerShellHostingScenario + public static class PowerShellHostingScenario { // Test that it does not throw an exception [Fact] - public void TestStartJobThrowTerminatingException() + public static void TestStartJobThrowTerminatingException() { using (var ps = PowerShell.Create()) { ps.AddCommand("Start-Job").AddParameter("ScriptBlock", ScriptBlock.Create("1+1")); - Exception ex = Assert.Throws(() => ps.Invoke()); + var ex = Assert.Throws(() => ps.Invoke()); Assert.IsType(ex.InnerException); + Assert.Equal("IPCPwshExecutableNotFound,Microsoft.PowerShell.Commands.StartJobCommand", ex.ErrorRecord.FullyQualifiedErrorId); } } }