diff --git a/src/System.Management.Automation/engine/NativeCommandParameterBinder.cs b/src/System.Management.Automation/engine/NativeCommandParameterBinder.cs index 3794f41c43..c00f62823e 100644 --- a/src/System.Management.Automation/engine/NativeCommandParameterBinder.cs +++ b/src/System.Management.Automation/engine/NativeCommandParameterBinder.cs @@ -306,7 +306,7 @@ namespace System.Management.Automation /// Check to see if the string contains spaces and therefore must be quoted. /// /// The string to check for spaces - private bool NeedQuotes(string stringToCheck) + internal static bool NeedQuotes(string stringToCheck) { bool needQuotes = false, followingBackslash = false; int quoteCount = 0; diff --git a/src/System.Management.Automation/namespaces/FileSystemProvider.cs b/src/System.Management.Automation/namespaces/FileSystemProvider.cs index d56977ade1..d5dde6ff71 100644 --- a/src/System.Management.Automation/namespaces/FileSystemProvider.cs +++ b/src/System.Management.Automation/namespaces/FileSystemProvider.cs @@ -1327,27 +1327,45 @@ namespace Microsoft.PowerShell.Commands { System.Diagnostics.Process invokeProcess = new System.Diagnostics.Process(); -#if UNIX - invokeProcess.StartInfo.FileName = Platform.IsLinux ? "xdg-open" : /* OS X */ "open"; - invokeProcess.StartInfo.Arguments = path; - invokeProcess.Start(); -#elif CORECLR try { - // Try Process.Start first. This works for executables even on headless SKUs. + // Try Process.Start first. + // - In FullCLR, this is all we need to do. + // - In CoreCLR, this works for executables on Win/Unix platforms invokeProcess.StartInfo.FileName = path; invokeProcess.Start(); } - catch (Win32Exception) +#if UNIX + catch (Win32Exception ex) when (ex.NativeErrorCode == 13) { + // Error code 13 -- Permission denied. + // The file is possibly not an executable, so we try invoking the default program that handles this file. + const string quoteFormat = "\"{0}\""; + invokeProcess.StartInfo.FileName = Platform.IsLinux ? "xdg-open" : /* OS X */ "open"; + if (NativeCommandParameterBinder.NeedQuotes(path)) + { + path = string.Format(CultureInfo.InvariantCulture, quoteFormat, path); + } + invokeProcess.StartInfo.Arguments = path; + invokeProcess.Start(); + } +#elif CORECLR + catch (Win32Exception ex) when (ex.NativeErrorCode == 193) + { + // Error code 193 -- BAD_EXE_FORMAT (not a valid Win32 application). // 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(); + finally + { + // Nothing to do in FullCLR. + // This empty 'finally' block is just to match the 'try' block above so that the code can be organized + // in a clean way without too many if/def's. + // Empty finally block will be ignored in release build, so there is no performance concern. + } #endif } } // InvokeDefaultAction 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 ce25b0d34d..9954f5d94a 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Invoke-Item.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Invoke-Item.Tests.ps1 @@ -1,47 +1,63 @@ using namespace System.Diagnostics -Describe "Invoke-Item on non-Windows" -Tags "CI" { - - function NewProcessStartInfo([string]$CommandLine, [switch]$RedirectStdIn) - { - return [ProcessStartInfo]@{ - FileName = $powershell - Arguments = $CommandLine - RedirectStandardInput = $RedirectStdIn - RedirectStandardOutput = $true - RedirectStandardError = $true - UseShellExecute = $false - } - } - - function RunPowerShell([ProcessStartInfo]$debugfn) - { - $process = [Process]::Start($debugfn) - return $process - } - - function EnsureChildHasExited([Process]$process, [int]$WaitTimeInMS = 15000) - { - $process.WaitForExit($WaitTimeInMS) - - if (!$process.HasExited) - { - $process.HasExited | Should Be $true - $process.Kill() - } - } - +Describe "Invoke-Item basic tests" -Tags "CI" { BeforeAll { - $powershell = Join-Path -Path $PsHome -ChildPath "powershell" - Setup -File testfile.txt -Content "Hello World" - $testfile = Join-Path $TestDrive testfile.txt + $powershell = Join-Path $PSHOME -ChildPath powershell + + $testFile1 = Join-Path -Path $TestDrive -ChildPath "text1.txt" + New-Item -Path $testFile1 -ItemType File -Force > $null + + $testFolder = Join-Path -Path $TestDrive -ChildPath "My Folder" + New-Item -Path $testFolder -ItemType Directory -Force > $null + $testFile2 = Join-Path -Path $testFolder -ChildPath "text2.txt" + New-Item -Path $testFile2 -ItemType File -Force > $null + + $textFileTestCases = @( + @{ TestFile = $testFile1 }, + @{ TestFile = $testFile2 }) } - It "Should invoke a text file without error on non-Windows" -Skip:($IsWindows) { - $debugfn = NewProcessStartInfo "-noprofile ""``Invoke-Item $testfile`n" -RedirectStdIn - $process = RunPowerShell $debugfn - EnsureChildHasExited $process - $process.ExitCode | Should Be 0 + Context "Invoke a text file on Unix" { + BeforeEach { + $redirectErr = Join-Path -Path $TestDrive -ChildPath "error.txt" + } + + AfterEach { + Remove-Item -Path $redirectErr -Force -ErrorAction SilentlyContinue + } + + ## Run this test only on OSX because redirecting stderr of 'xdg-open' results in weird behavior in our Linux CI, + ## causing this test to fail or the build to hang. + It "Should invoke text file '' without error" -Skip:(!$IsOSX) -TestCases $textFileTestCases { + param($TestFile) + + ## Redirect stderr to a file. So if 'open' failed to open the text file, an error + ## message from 'open' would be written to the redirection file. + $proc = Start-Process -FilePath $powershell -ArgumentList "-noprofile Invoke-Item '$TestFile'" ` + -RedirectStandardError $redirectErr ` + -PassThru + $proc.WaitForExit(3000) > $null + if (!$proc.HasExited) { + try { $proc.Kill() } catch { } + } + ## If the text file was successfully opened, the redirection file should be empty since no error + ## message was written to it. + Get-Content $redirectErr -Raw | Should BeNullOrEmpty + } + } + + It "Should invoke an executable file without error" { + $executable = Get-Command "ping" -CommandType Application | ForEach-Object Source + $redirectFile = Join-Path -Path $TestDrive -ChildPath "redirect2.txt" + + if ($IsWindows) { + ## 'ping.exe' on Windows writes out usage to stdout. + & $powershell "-noprofile" "Invoke-Item '$executable'" > $redirectFile + } else { + ## 'ping' on Unix write out usage to stderr + & $powershell "-noprofile" "Invoke-Item '$executable'" 2> $redirectFile + } + Get-Content $redirectFile -Raw | Should Match "usage: ping" } }