diff --git a/src/System.Management.Automation/namespaces/FileSystemProvider.cs b/src/System.Management.Automation/namespaces/FileSystemProvider.cs index 123936bead8..4d1efe401b8 100644 --- a/src/System.Management.Automation/namespaces/FileSystemProvider.cs +++ b/src/System.Management.Automation/namespaces/FileSystemProvider.cs @@ -1312,6 +1312,14 @@ private FileSystemInfo GetFileSystemItem(string path, ref bool isContainer, bool /// protected override void InvokeDefaultAction(string path) { +#if UNIX + // Error code 13 -- Permission denied + const int NOT_EXECUTABLE = 13; +#else + // Error code 193 -- BAD_EXE_FORMAT (not a valid Win32 application) + const int NOT_EXECUTABLE = 193; +#endif + if (String.IsNullOrEmpty(path)) { throw PSTraceSource.NewArgumentException("path"); @@ -1325,21 +1333,34 @@ protected override void InvokeDefaultAction(string path) if (ShouldProcess(resource, action)) { - System.Diagnostics.Process invokeProcess = new System.Diagnostics.Process(); + var invokeProcess = new System.Diagnostics.Process(); + invokeProcess.StartInfo.FileName = path; + bool invokeDefaultProgram = false; - try + if (Directory.Exists(path) && !Platform.IsNanoServer && !Platform.IsIoT) { - // 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(); + // Path points to a directory and it's not NanoServer or IoT, so we can opne the file explorer + invokeDefaultProgram = true; } -#if UNIX - catch (Win32Exception ex) when (ex.NativeErrorCode == 13) + else + { + try + { + // Try Process.Start first. This works for executables on Win/Unix platforms + invokeProcess.Start(); + } + catch (Win32Exception ex) when (ex.NativeErrorCode == NOT_EXECUTABLE) + { + // The file is possibly not an executable. If it's headless SKUs, rethrow. + if (Platform.IsNanoServer || Platform.IsIoT) { throw; } + // Otherwise, try invoking the default program that handles this file. + invokeDefaultProgram = true; + } + } + + if (invokeDefaultProgram) { - // Error code 13 -- Permission denied. - // The file is possibly not an executable, so we try invoking the default program that handles this file. +#if UNIX const string quoteFormat = "\"{0}\""; invokeProcess.StartInfo.FileName = Platform.IsLinux ? "xdg-open" : /* OS X */ "open"; if (NativeCommandParameterBinder.NeedQuotes(path)) @@ -1348,25 +1369,10 @@ protected override void InvokeDefaultAction(string 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 - 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. - } + ShellExecuteHelper.Start(invokeProcess.StartInfo); #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 52a4198685a..8405dc53034 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Invoke-Item.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Invoke-Item.Tests.ps1 @@ -28,21 +28,23 @@ Describe "Invoke-Item basic tests" -Tags "CI" { ## 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 { + It "Should invoke text file '' without error on Mac" -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 -c Invoke-Item '$TestFile'" ` - -RedirectStandardError $redirectErr ` - -PassThru - $proc.WaitForExit(3000) > $null - if (!$proc.HasExited) { - try { $proc.Kill() } catch { } + $expectedTitle = Split-Path $TestFile -Leaf + $beforeCount = [int]('tell application "TextEdit" to count of windows' | osascript) + Invoke-Item -Path $TestFile + $startTime = Get-Date + $title = [String]::Empty + while (((Get-Date) - $startTime).TotalSeconds -lt 10 -and ($title -ne $expectedTitle)) + { + Start-Sleep -Milliseconds 100 + $title = 'tell application "TextEdit" to get name of front window' | osascript } - ## 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 + $afterCount = [int]('tell application "TextEdit" to count of windows' | osascript) + $afterCount | Should Be ($beforeCount + 1) + $title | Should Be $expectedTitle + "tell application ""TextEdit"" to close window ""$expectedTitle""" | osascript } } @@ -59,6 +61,100 @@ Describe "Invoke-Item basic tests" -Tags "CI" { } Get-Content $redirectFile -Raw | Should Match "usage: ping" } + + Context "Invoke a folder" { + BeforeAll { + $supportedEnvironment = $true + if ($IsLinux) + { + $appFolder = "$HOME/.local/share/applications" + if (Test-Path $appFolder) + { + $mimeDefault = xdg-mime query default inode/directory + Remove-Item $HOME/InvokeItemTest.Success -Force -ErrorAction SilentlyContinue + Set-Content -Path "$appFolder/InvokeItemTest.desktop" -Force -Value @" +[Desktop Entry] +Version=1.0 +Name=InvokeItemTest +Comment=Validate Invoke-Item for directory +Exec=/bin/sh -c 'echo %u > ~/InvokeItemTest.Success' +Icon=utilities-terminal +Terminal=true +Type=Application +Categories=Application; +"@ + xdg-mime default InvokeItemTest.desktop inode/directory + } + else + { + $supportedEnvironment = $false + } + } + } + + AfterAll { + if ($IsLinux -and $supportedEnvironment) + { + xdg-mime default $mimeDefault inode/directory + Remove-Item $appFolder/InvokeItemTest.desktop -Force -ErrorAction SilentlyContinue + Remove-Item $HOME/InvokeItemTest.Success -Force -ErrorAction SilentlyContinue + } + } + + It "Should invoke a folder without error" -Skip:(!$supportedEnvironment) { + if ($IsWindows) + { + $shell = New-Object -ComObject "Shell.Application" + $windows = $shell.Windows() + + $before = $windows.Count + Invoke-Item -Path $PSHOME + $startTime = Get-Date + # may take time for explorer to open window + while (((Get-Date) - $startTime).TotalSeconds -lt 10 -and ($windows.Count -eq $before)) + { + Start-Sleep -Milliseconds 100 + } + $after = $windows.Count + + $before + 1 | Should Be $after + $item = $windows.Item($after - 1) + $item.LocationURL | Should Match ($PSHOME -replace '\\', '/') + ## close the windows explorer + $item.Quit() + } + elseif ($IsLinux) + { + # validate on Unix by reassociating default app for directories + Invoke-Item -Path $PSHOME + $startTime = Get-Date + # may take time for handler to start + while (((Get-Date) - $startTime).TotalSeconds -lt 10 -and (-not (Test-Path "$HOME/InvokeItemTest.Success"))) + { + Start-Sleep -Milliseconds 100 + } + Get-Content $HOME/InvokeItemTest.Success | Should Be $PSHOME + } + else + { + # validate on MacOS by using AppleScript + $beforeCount = [int]('tell application "Finder" to count of windows' | osascript) + Invoke-Item -Path $PSHOME + $startTime = Get-Date + $expectedTitle = Split-Path $PSHOME -Leaf + $title = [String]::Empty + while (((Get-Date) - $startTime).TotalSeconds -lt 10 -and ($title -ne $expectedTitle)) + { + Start-Sleep -Milliseconds 100 + $title = 'tell application "Finder" to get name of front window' | osascript + } + $afterCount = [int]('tell application "Finder" to count of windows' | osascript) + $afterCount | Should Be ($beforeCount + 1) + $title | Should Be $expectedTitle + 'tell application "Finder" to close front window' | osascript + } + } + } } Describe "Invoke-Item tests on Windows" -Tags "CI","RequireAdminOnWindows" {