-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Improving UseShellExecute implementation on Process.Start #23705
Changes from 1 commit
75f34a5
4620a97
5fcd9c7
1a22357
fc03dc4
3682f8a
3a5c6c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -57,6 +57,54 @@ internal DateTime StartTimeCore | |
} | ||
} | ||
|
||
/// <summary>Gets arguments.</summary> | ||
/// <param name="startInfo">The start info with which to start the process.</param> | ||
private string[] GetArgs(ProcessStartInfo startInfo) | ||
{ | ||
if (!startInfo.UseShellExecute) | ||
{ | ||
return ParseArgv(startInfo); | ||
} | ||
|
||
string shellArgs = string.IsNullOrEmpty(startInfo.Arguments) ? startInfo.FileName : startInfo.FileName + " " + startInfo.Arguments; | ||
return new string[2] { GetExecPath(), shellArgs }; | ||
} | ||
|
||
/// <summary>Gets execution path</summary> | ||
private string GetExecPath() | ||
{ | ||
string[] allowedProgramsToRun = { "xdg-open", "gnome-open", "kfmclient" }; | ||
foreach (var program in allowedProgramsToRun) | ||
{ | ||
string pathToProgram = GetPathToProgram(program); | ||
if (!string.IsNullOrEmpty(pathToProgram)) | ||
{ | ||
return Path.Combine(pathToProgram, program); | ||
} | ||
} | ||
return string.Empty; | ||
} | ||
|
||
/// <summary> | ||
/// Gets the path to the program | ||
/// </summary> | ||
/// <param name="program"></param> | ||
/// <returns></returns> | ||
private string GetPathToProgram(string program) | ||
{ | ||
string path = Environment.GetEnvironmentVariable("PATH"); | ||
string[] dirs = path.Split(":"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If for some reason PATH is not set in this shell, this will throw NRE. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it looks like Mono falls back to the current directory. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could eliminate all this code and just call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, I just noticed ResolvePath() is also calling path to get path. I'm going to extract that logic into a private method and reuse that private method both in ResolvePath() and in here. Thanks |
||
foreach (var dir in dirs) | ||
{ | ||
string[] files = Directory.GetFiles(dir, program); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We may want to handle at least some garbage in $PATH. Eg., try
{
files = Directory.GetFiles(dir, program);
}
catch (Exception e) when (ex is ArgumentException || ex is IOException || ex is UnauthorizedAccessException)
{} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually better still just use |
||
if (files.Length != 0) | ||
{ | ||
return dir; | ||
} | ||
} | ||
return string.Empty; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think |
||
} | ||
|
||
/// <summary> | ||
/// Gets the amount of time the associated process has spent utilizing the CPU. | ||
/// It is the sum of the <see cref='System.Diagnostics.Process.UserProcessorTime'/> and | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -232,18 +232,11 @@ private bool StartCore(ProcessStartInfo startInfo) | |
{ | ||
throw new InvalidOperationException(SR.CantRedirectStreams); | ||
} | ||
|
||
const string ShellPath = "/bin/sh"; | ||
|
||
filename = ShellPath; | ||
argv = new string[3] { ShellPath, "-c", startInfo.FileName + " " + startInfo.Arguments}; | ||
} | ||
else | ||
{ | ||
filename = ResolvePath(startInfo.FileName); | ||
argv = ParseArgv(startInfo); | ||
} | ||
|
||
filename = ResolvePath(startInfo.FileName); | ||
argv = ParseArgv(startInfo); | ||
|
||
string[] envp = CreateEnvp(startInfo); | ||
string cwd = !string.IsNullOrWhiteSpace(startInfo.WorkingDirectory) ? startInfo.WorkingDirectory : null; | ||
|
||
|
@@ -253,11 +246,33 @@ private bool StartCore(ProcessStartInfo startInfo) | |
// is used to fork/execve as executing managed code in a forked process is not safe (only | ||
// the calling thread will transfer, thread IDs aren't stable across the fork, etc.) | ||
int childPid, stdinFd, stdoutFd, stderrFd; | ||
Interop.Sys.ForkAndExecProcess( | ||
filename, argv, envp, cwd, | ||
startInfo.RedirectStandardInput, startInfo.RedirectStandardOutput, startInfo.RedirectStandardError, | ||
out childPid, | ||
out stdinFd, out stdoutFd, out stderrFd); | ||
|
||
try | ||
{ | ||
Interop.Sys.ForkAndExecProcess( | ||
filename, argv, envp, cwd, | ||
startInfo.RedirectStandardInput, startInfo.RedirectStandardOutput, startInfo.RedirectStandardError, | ||
out childPid, | ||
out stdinFd, out stdoutFd, out stderrFd); | ||
} | ||
catch (Win32Exception e) | ||
{ | ||
if (!startInfo.UseShellExecute) | ||
{ | ||
throw e; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. prefer |
||
} | ||
else | ||
{ | ||
filename = GetExecPath(); | ||
argv = GetArgs(startInfo); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this can be just assert (!startInfo.UseShellExecute) |
||
Interop.Sys.ForkAndExecProcess( | ||
filename, argv, envp, cwd, | ||
startInfo.RedirectStandardInput, startInfo.RedirectStandardOutput, startInfo.RedirectStandardError, | ||
out childPid, | ||
out stdinFd, out stdoutFd, out stderrFd); | ||
} | ||
} | ||
|
||
// Store the child's information into this Process object. | ||
Debug.Assert(childPid >= 0); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,8 +4,9 @@ | |
<BuildConfigurations> | ||
netstandard-Windows_NT; | ||
netstandard-Unix; | ||
netstandard-OSX; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think we can write the tests to work on different platforms (Linux and OSX) and we can pass different data to such tests according to the OS we are running on. We can use PlatformDetection to decide what set of data we can pass to the tests. doing that we don't have to add OSX configurations. |
||
uap-Windows_NT; | ||
uapaot-Windows_NT; | ||
</BuildConfigurations> | ||
</PropertyGroup> | ||
</Project> | ||
</Project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System.Collections.Generic; | ||
using System.ComponentModel; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Runtime.InteropServices; | ||
using System.Threading; | ||
using System.Security; | ||
using Xunit; | ||
using Xunit.NetCore.Extensions; | ||
|
||
namespace System.Diagnostics.Tests | ||
{ | ||
public partial class ProcessTests : ProcessTestBase | ||
{ | ||
[Fact] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please make tests that pop up windows (like shell exec ones) be [OuterLoop] to not be obnoxious. Note CI doesn't normally run outer loop, so if you want to use CI for these tests, you'll either have to request outer loop or change them to outer loop in your last commit. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added (and for now commented out) [OuterLoop] for these tests. On the final commit I will uncomment those lines. |
||
public void ProcessStart_UseShellExecuteTrue_OpenFile_ThrowsIfNoDefaultProgramInstalledSucceedsOtherwise() | ||
{ | ||
string fileToOpen = GetTestFilePath() + ".txt"; | ||
File.WriteAllText(fileToOpen, $"{nameof(ProcessStart_UseShellExecuteTrue_OpenFile_ThrowsIfNoDefaultProgramInstalledSucceedsOtherwise)}"); | ||
|
||
string[] allowedProgramsToRun = { "xdg-open", "gnome-open", "kfmclient" }; | ||
foreach (var program in allowedProgramsToRun) | ||
{ | ||
if (IsProgramInstalled(program)) | ||
{ | ||
var startInfo = new ProcessStartInfo { UseShellExecute = true, FileName = fileToOpen }; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tihnk it would be reasonable, especially as this is outer loop, to write to the console which of the three you're launching. That owuld allow you to look in the official run logs, and verify that it's actually doing something -- and also whether xdg-open exists on all the images or not. |
||
using (var px = Process.Start(startInfo)) | ||
{ | ||
if (px != null) | ||
{ | ||
Assert.Equal(program, px.ProcessName); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this test I mostly care to test which process we ended up using to open the input file. therefore I could remove the extra assertions that check if px exited and the exitcode value assertion as well |
||
px.Kill(); | ||
px.WaitForExit(); | ||
Assert.True(px.HasExited); | ||
Assert.Equal(137, px.ExitCode); // 137 means the process was killed | ||
} | ||
} | ||
return; | ||
} | ||
} | ||
|
||
Win32Exception e = Assert.Throws<Win32Exception>(() => Process.Start(new ProcessStartInfo { UseShellExecute = true, FileName = fileToOpen })); | ||
} | ||
|
||
[Fact] | ||
[OuterLoop("Returns failure exit code when default program, xdg-open, is installed")] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it should not fail on any of our machines. either it should succeed if xdg-open is not present, or we must require xdg-open on all our images. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the linux image (ubuntu 14.04) I cloned from DevTestLab doesnt have xdg-open installed by default. @danmosemsft based on this, what would you say should be the expected behavior in this case? do we change the images to have xdg-open installed from now on? or do I change how the API behaves so that it wont throw when xdg-open is not installed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's assume at least some of our helix (test ruN) machines have it. make the test write to console if it doesn't find it. It's outer loop so that's OK. After you've checked in, we'll see whether any of the machines actually have it. If non have it we might want to update hte images so at least some configuration is exercising the code. |
||
public void ProcessStart_UseShellExecuteTrue_OpenMissingFile_DefaultProgramInstalled_ReturnsFailureExitCode() | ||
{ | ||
string fileToOpen = Path.Combine(Environment.CurrentDirectory, "_no_such_file.TXT"); | ||
using (var p = Process.Start(new ProcessStartInfo { UseShellExecute = true, FileName = fileToOpen })) | ||
{ | ||
if (p != null) | ||
{ | ||
Assert.Equal("xdg-open", p.ProcessName); | ||
p.WaitForExit(); | ||
Assert.True(p.HasExited); | ||
Assert.Equal(2, p.ExitCode); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this test I wanted to show that when no file exists the xdg-open process will return the expected exitcode it uses when file doesnt exist. so the most important assertion here is Assert.Equal(2, p.ExitCode) and the rest could be removed if needed |
||
} | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Gets the path to the program | ||
/// </summary> | ||
/// <param name="program"></param> | ||
/// <returns></returns> | ||
private bool IsProgramInstalled(string program) | ||
{ | ||
string path = Environment.GetEnvironmentVariable("PATH"); | ||
string[] dirs = path.Split(':'); | ||
foreach (var dir in dirs) | ||
{ | ||
string[] files = Directory.GetFiles(dir, program); | ||
if (files.Length != 0) | ||
{ | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System.Collections.Generic; | ||
using System.ComponentModel; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Runtime.InteropServices; | ||
using System.Threading; | ||
using System.Security; | ||
using Xunit; | ||
using Xunit.NetCore.Extensions; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. need to check if I can remove unnecessary usings from here. also, there is room to cleanup the osx tests. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, you can always remove unnecessary usings. VS should do it for you in a couple clicks. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Surely. But I have VS installed on windows and VSCode on the other machine. When I'm using VS on windows, the "remove and sort" feature is not available for me on this file. I think it is because the intellisense for this file is not available, probably because in csproj it is not included for windows. VSCode also has limited functionality I think. It doesn't have the feature to remove and sort. I think there is limited intellisense functionality available in VSCode in general. I may be wrong, just my observation so far. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah right. I wouldn't bother with it then. we don't do it consistently and it's easy for us to do it periodically. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @maryamariyan It's definitely not obvious at first, but if you go into the "Configuration Manager" menu in VS, you can switch the build configuration for each project. You can select the OSX configuration for this project and then do refactorings. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not that you need to rush to do that or anything, just a tip for the future 😄 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks @mellinoe :) |
||
|
||
namespace System.Diagnostics.Tests | ||
{ | ||
public partial class ProcessTests : ProcessTestBase | ||
{ | ||
[Fact] | ||
[OuterLoop("Launches default application")] | ||
public void TestWithFilename_ShouldUseOpenWithDefaultApp() | ||
{ | ||
string file = Path.Combine(Environment.CurrentDirectory, "..", "..", "..", "..", "PATENTS.TXT"); | ||
using (var px = Process.Start("/usr/bin/open", file)) | ||
{ | ||
Assert.False(px.HasExited); | ||
px.WaitForExit(); | ||
Assert.True(px.HasExited); | ||
Assert.Equal(0, px.ExitCode); // Exit Code 0 from open means success | ||
} | ||
} | ||
|
||
[Fact] | ||
[OuterLoop("Launches default browser")] | ||
public void TestWithUrl_ShouldUseOpenWithDefaultApp() | ||
{ | ||
using (var px = Process.Start("/usr/bin/open", "http://www.google.com")) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why is /usr/bin/open relevant here? does xdg-open etc not exist on OSX? and if it does not, should we be looking for /usr/bin/open? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Youre right, I didn't mean literally, I meant without usr bin open. |
||
{ | ||
Assert.False(px.HasExited); | ||
px.WaitForExit(); | ||
Assert.True(px.HasExited); | ||
Assert.Equal(0, px.ExitCode); // Exit Code 0 from open means success | ||
} | ||
} | ||
|
||
[Fact] | ||
// TODO fix behavior to ThrowWin32Exception instead? | ||
public void ProcessStart_TryOpenFileThatDoesntExist_UseShellExecuteIsTrue_ThrowsWin32Exception() | ||
{ | ||
string file = Path.Combine(Environment.CurrentDirectory, "..", "..", "..", "..", "_no_such_file.TXT"); | ||
using (var p = Process.Start(new ProcessStartInfo { UseShellExecute = true, FileName = file })) | ||
{ | ||
Assert.True(p.WaitForExit(WaitInMS)); | ||
Assert.Equal(1, p.ExitCode); // Exit Code 1 from open means something went wrong | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -61,13 +61,27 @@ public void TestRootGetProcessById() | |
Assert.Equal(1, p.Id); | ||
} | ||
|
||
[Theory, InlineData(true), InlineData(false)] | ||
public void ProcessStart_TryExitCommandAsFileName_ThrowsWin32Exception(bool useShellExecute) | ||
{ | ||
Win32Exception e = Assert.Throws<Win32Exception>(() => Process.Start(new ProcessStartInfo { UseShellExecute = useShellExecute, FileName = "exit", Arguments = "42" })); | ||
} | ||
|
||
[Fact] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this test and the one below, do they match Windows behavior? If so they shoud lbe in ProcessTests.cs (ie., not Unix specific file). This will help make certain the behavior stays the same betwen Unix and windows. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and if they don't match Windows behavior, hopefully we thought that through, and a comment here explaining why we chose to diverge would be good |
||
public void TestUseShellExecute_Unix_Succeeds() | ||
public void ProcessStart_UseShellExecuteTrue_OpenNano_OpensNano() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this fails on osx. I need to investigate |
||
{ | ||
using (var p = Process.Start(new ProcessStartInfo { UseShellExecute = true, FileName = "exit", Arguments = "42" })) | ||
string appToOpen = "nano"; | ||
var startInfo = new ProcessStartInfo { UseShellExecute = true, FileName = appToOpen }; | ||
using (var px = Process.Start(startInfo)) | ||
{ | ||
Assert.True(p.WaitForExit(WaitInMS)); | ||
Assert.Equal(42, p.ExitCode); | ||
if (px != null) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if Process.Start returns null, this test will pass. is that desirable? |
||
{ | ||
Assert.Equal(appToOpen, px.ProcessName); | ||
px.Kill(); | ||
px.WaitForExit(); | ||
Assert.True(px.HasExited); | ||
Assert.Equal(137, px.ExitCode); | ||
} | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,8 @@ | |
<DefineConstants Condition="'$(TargetsWindows)' == 'true'">$(DefineConstants);TargetsWindows</DefineConstants> | ||
</PropertyGroup> | ||
<!-- Default configurations to help VS understand the configurations --> | ||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'netstandard-OSX-Debug|AnyCPU'" /> | ||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'netstandard-OSX-Release|AnyCPU'" /> | ||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'netstandard-Unix-Debug|AnyCPU'" /> | ||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'netstandard-Unix-Release|AnyCPU'" /> | ||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'netstandard-Windows_NT-Debug|AnyCPU'" /> | ||
|
@@ -32,6 +34,8 @@ | |
<Compile Condition="'$(TargetGroup)' != 'uap'" Include="ProcessTestBase.NonUap.cs" /> | ||
<Compile Condition="'$(TargetGroup)' == 'uap'" Include="ProcessTestBase.Uap.cs" /> | ||
<Compile Include="ProcessTests.cs" /> | ||
<Compile Include="ProcessTests.Linux.cs" Condition=" '$(TargetsLinux)' == 'true'" /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: inconsistent spacing around quotes |
||
<Compile Include="ProcessTests.OSX.cs" Condition=" '$(TargetsOSX)' == 'true'" /> | ||
<Compile Include="ProcessTests.Unix.cs" Condition="'$(TargetsWindows)' != 'true'" /> | ||
<Compile Include="ProcessThreadTests.cs" /> | ||
<Compile Include="ProcessWaitingTests.cs" /> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when will it be null?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this function returns null when program is not installed. GetPathToProgram(), now renamed to FindProgramInPath(), returns null when program was not found in path.