Support Invoke-Item -Path <folder> (#4262)
On CoreFx, UseShellExecute for Process start is false by default to be cross platform compatible. In the case of a folder, Process.Start() returns Access Denied as it's not an executable. On Windows, we can use the ShellExecute path to have explorer open the folder. On Unix, we use `xdg-open` and `open` for Linux and macOS respectively.
This commit is contained in:
parent
cf7b9f3de1
commit
03d4e9120b
|
@ -1312,6 +1312,14 @@ namespace Microsoft.PowerShell.Commands
|
|||
/// </exception>
|
||||
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 @@ namespace Microsoft.PowerShell.Commands
|
|||
|
||||
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
|
||||
{
|
||||
// Error code 13 -- Permission denied.
|
||||
// The file is possibly not an executable, so we try invoking the default program that handles this file.
|
||||
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)
|
||||
{
|
||||
#if UNIX
|
||||
const string quoteFormat = "\"{0}\"";
|
||||
invokeProcess.StartInfo.FileName = Platform.IsLinux ? "xdg-open" : /* OS X */ "open";
|
||||
if (NativeCommandParameterBinder.NeedQuotes(path))
|
||||
|
@ -1348,25 +1369,10 @@ namespace Microsoft.PowerShell.Commands
|
|||
}
|
||||
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
|
||||
|
||||
|
|
|
@ -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 '<TestFile>' without error" -Skip:(!$IsOSX) -TestCases $textFileTestCases {
|
||||
It "Should invoke text file '<TestFile>' 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" {
|
||||
|
|
Loading…
Reference in a new issue