Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Improving UseShellExecute implementation on Process.Start #23705

Merged
merged 7 commits into from
Sep 12, 2017

Conversation

maryamariyan
Copy link
Member

@maryamariyan maryamariyan commented Aug 31, 2017

Fixes both #19956 and #22299

@maryamariyan maryamariyan changed the title Temp unix fix Attempt to improve UseShellExecute implementation on Process.Start Aug 31, 2017

private string GetPathToProgram(string program)
{
var path = Environment.GetEnvironmentVariable("PATH");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please review the corefx guidelines for "var" usage - these don't fit.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops I didnt realize you guys will get notified of my PR just yet :) I need to cleanup all this now

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you create a PR, folks will notice fairly quickly even if they aren't assigned it. So generally you throw up a PR either when you think you're done, or when you need feedback on what you have so far 😄

Assert.Equal(42, p.ExitCode);
}
}
//[Fact]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you commented out tests -- are you looking for feedback on the approach, before you write tests?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have the failing tests commented out for now, since I'm toggling between different os as I make fixes (win, osx and linux). I will keep them commented out until all get fixes.

actually some of them are failing because I don't know if it is ok to change current behavior of Process.Start just yet or not. I'll explain in more detail in a bit when I have a good summary to share.

using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do'nt think you need all those. I believe VS has a refactoring to remove unnecessary using statements.

var lookupLocation = path.Split(":");
foreach (var loc in lookupLocation)
{
string[] dirs = Directory.GetFiles(loc, program);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of dirs maybe files ? and instead of loc maybe dir

@maryamariyan maryamariyan changed the title Attempt to improve UseShellExecute implementation on Process.Start [WIP] Improving UseShellExecute implementation on Process.Start Aug 31, 2017
@maryamariyan maryamariyan changed the title [WIP] Improving UseShellExecute implementation on Process.Start [WIP] Don't Review Yet. Improving UseShellExecute implementation on Process.Start Aug 31, 2017
@maryamariyan maryamariyan force-pushed the temp-unix-fix branch 2 times, most recently from d88796a to dc2d88f Compare September 1, 2017 05:07
[Fact]
public void UseShellExecuteIsFalse_ThrowsWithOrWithoutMyChanges()
{
// The test below fails on linux with or without my changes
Copy link
Member Author

@maryamariyan maryamariyan Sep 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observation 1: the commented out test below fails on linux with or without my changes.
Notice that UseShellExecute is false.

Question: is this ok to fail?

[Fact]
public void UseShellExecuteIsFalse_ThrowsWithOrWithoutMyChanges()
{
// The test below fails on osx with or without my changes
Copy link
Member Author

@maryamariyan maryamariyan Sep 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observation 2: the commented out test below fails on osx with or without my changes.
Notice that UseShellExecute is false.

Question: is this ok to fail?

using (var p = Process.Start(new ProcessStartInfo { UseShellExecute = true, FileName = "exit", Arguments = "42" }))
{
Assert.True(p.WaitForExit(WaitInMS));
Assert.Equal(42, p.ExitCode); // Assert.Equal Failure with after adding my changes: Expected: 42, Actual: 1
Copy link
Member Author

@maryamariyan maryamariyan Sep 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observation 3: this Assert.Equals fails on osx only after I submit my changes.
Notice that UseShellExecute is true.

Question: do I need to fix this to keep the old behavior?

using (var p = Process.Start(new ProcessStartInfo { UseShellExecute = true, FileName = "exit", Arguments = "42" }))
{
Assert.True(p.WaitForExit(WaitInMS));
// Assert.Equal(42, p.ExitCode); // Assert.Equal Failure with after adding my changes: Expected: 42, Actual: 1
Copy link
Member Author

@maryamariyan maryamariyan Sep 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observation 4: this Assert.Equals fails on linux only after I submit my changes.
Notice that UseShellExecute is true.

Question: do I need to fix this to keep the old behavior?

Copy link

@TSlivede TSlivede Sep 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this should work. If it did work, UseShellExecute would have completely different effects on Linux and Windows. (see my comment)
PS: This also applies to your other Questions from four hours ago.

[OuterLoop("Launches default application")]
public void NewXdg_TestWithFilename_ShouldUseOpenWithDefaultApp()
{
string fileToOpen = Path.Combine(Environment.CurrentDirectory, "..", "..", "..", "..", "PATENTS.TXT");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to have a txt file in the tests\ directory and have this in your test csproj

        <Content Include="foo.txt">
          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
        </Content>

@maryamariyan maryamariyan force-pushed the temp-unix-fix branch 4 times, most recently from f4c01d8 to 30b1771 Compare September 8, 2017 05:05
Making sure we are coherent with mono implementation
Added unit tests per linux and osx
{
if (px != null)
{
Assert.Equal(program, px.ProcessName);
Copy link
Member Author

Choose a reason for hiding this comment

The 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

Assert.Equal("xdg-open", p.ProcessName);
p.WaitForExit();
Assert.True(p.HasExited);
Assert.Equal(2, p.ExitCode);
Copy link
Member Author

Choose a reason for hiding this comment

The 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

using System.Threading;
using System.Security;
using Xunit;
using Xunit.NetCore.Extensions;
Copy link
Member Author

@maryamariyan maryamariyan Sep 8, 2017

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

@maryamariyan maryamariyan Sep 10, 2017

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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 😄

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @mellinoe :)

[Fact]
public void TestUseShellExecute_Unix_Succeeds()
public void ProcessStart_UseShellExecuteTrue_OpenNano_OpensNano()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this fails on osx. I need to investigate

private string GetPathToProgram(string program)
{
string path = Environment.GetEnvironmentVariable("PATH");
string[] dirs = path.Split(":");
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like Mono falls back to the current directory.
https://github.com/mono/mono/blob/master/mono/eglib/gpath.c#L236

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could eliminate all this code and just call ResolvePath() if you don't mind a slightly different resolution algorithm. I think that's fine.

Copy link
Member Author

Choose a reason for hiding this comment

The 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

@maryamariyan
Copy link
Member Author

@danmosemsft my plan is to let the build run once with all [OuterLoop] commented out.
I think this should fail on OSX tests that pop up browsers I think.
Once the build is finished, then I'll update with another commit to uncomment all the [OuterLoop] lines.

if (!startInfo.UseShellExecute)
{
// Could not find the file
Debug.Assert(string.IsNullOrEmpty(filename));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If filename was null/empty (and USE=false), then it would go into this block. so why assert? Debug.Assert should only be to catch mistakes by us (you). In this case it seems like it's for bad user input, and we should throw somewhere.

Copy link
Member Author

@maryamariyan maryamariyan Sep 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assert is against filename.

  • filename is retrieved from ResolvePath() on line 250. This happens when startInfo.FileName is a URL.

I added a comment on the summary of this method to explain this.
Also, it's not expected to reach this line in any way when filename is not null. In that case we would have thrown already on line 264

out childPid,
out stdinFd, out stdoutFd, out stderrFd);

Debug.Assert(result == 0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If result != 0 does that mean you made a mistake in your code? If not, Debug.Assert is probably not the right choice. You should either ignore it (if appropriate) or throw, or some other effect visible in retail.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is not expected to reach here and result != 0 so I think the assert is proper here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok fine

Win32Exception e = Assert.Throws<Win32Exception>(() => Process.Start(new ProcessStartInfo { UseShellExecute = false, FileName = "exit", Arguments = "42" }));
}

[Fact]
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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

px.Kill();
px.WaitForExit();
Assert.True(px.HasExited);
Assert.Equal(137, px.ExitCode); // 137 means the process was killed
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

137 = Interop.Sys.Signals.SIGKILL + 128 btw... I think the shell adds 128 to signals as the exit code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, BASH man page The return value of a simple command is its exit status, or 128+n if the command is terminated by signal n.

// [OuterLoop("Opens program")]
public void ProcessStart_OpenFileOnLinux_UsesSpecifiedProgram(string programToOpenWith)
{

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: line

}
else
{
Console.WriteLine($"Program specified to open file with {programToOpenWith} is not installed on this machine.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could possibly log Environment.GetenvironmentVariable("PATH") if that might help.

using (var px = Process.Start(programToOpenWith, fileToOpen))
{
Console.WriteLine($"in OSX, {nameof(programToOpenWith)} is {programToOpenWith}, while {nameof(px.ProcessName)} is {px.ProcessName}.");
// Assert.Equal(programToOpenWith, px.ProcessName); // on OSX, process name is dotnet for some reason
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you were going to open an issue? if so you could put the URL of hte issue in the comment

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

created #23972 to track this.


[Fact, PlatformSpecific(TestPlatforms.OSX)]
// [OuterLoop("Opens program")]
public void ProcessStart_UseShellExecuteTrue_TryOpenFileThatDoesntExist_ReturnsExitCode1()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is the Linux version of this test? also, why is this OSX specific? If it matches Windows behavior, it should be a test for all platforms?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is another test similar to this for Linux, but since the return code is different for linux I split it to two different tests. (the exit code is coming from xdg-open and open).
I'll add a comment to explain why I split them to two tests

Copy link
Member

@danmoseley danmoseley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM aside from comments. Thanks!

{
using (var px = Process.Start(new ProcessStartInfo { UseShellExecute = true, FileName = Path.GetTempPath() }))
{
Assert.Null(px); // Not sure why px returned is null. but the call does not throw and opens folder successfully.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test for #23969

@joperezr
Copy link
Member

Looking at the two test failures seems like you need to make some tests platform specific. netcoreapp-nano and uap-RS2 are throwing: System.PlatformNotSupportedException : UseShellExecute is not supported on this platform.

Copy link
Member

@joperezr joperezr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than the comments by the rest of the folks, this LGTM once CI is green.

@maryamariyan
Copy link
Member Author

@dotnet-bot test outerloop please

@maryamariyan maryamariyan merged commit e670fd3 into dotnet:master Sep 12, 2017
@maryamariyan maryamariyan deleted the temp-unix-fix branch September 12, 2017 18:40
@karelz karelz added this to the 2.1.0 milestone Oct 11, 2017
@wfurt
Copy link
Member

wfurt commented Oct 17, 2017

It seems like this change will break backward compatibility.
/bin/sh basically exists on any unix. New helpers may or may not.
The other problem is that shell normally handles pipes, shell variable expansions and (dangerous) embedded subcommands (like "echo cat /etc/os-release")
Running shell will also typically process .profile and possibly .bashrc (or /etc/bashrc)
There may be additional variables set or PATH variable can be modified.

Secondary note is that "use shell" has established meaning on Unix.
I think we are breaking that.
Process.Start(new ProcessStartInfo { UseShellExecute = true, FileName ="ls"})
used to work but it does not any more.
@janvorli, @stephentoub ot @tmds may have some more thoughts.

@TSlivede
Copy link

TSlivede commented Oct 17, 2017

@wfurt I agree, that "shellExecute" sounds like command-line-shell (e.g. /bin/sh). So yes, "UseShellExecute" might be an unlucky name for the given purpose.
The reason for this name is the Win32-API function ShellExecute - "shell" typically doesn't mean command-line-shell within the Windows-API.

That said, using /bin/sh for "UseShellExecute" should never have been implemented, because that's not what "UseShellExecute" is about, See https://github.com/dotnet/corefx/issues/19956#issuecomment-325704971 and the following comments.

Process.Start(new ProcessStartInfo { UseShellExecute = true, FileName ="ls"})
used to work but it does not any more.

I didn't notice that before but yes, this case should be fixed if it doesn't work currently. (Because Windows .Net does also respect the Path-Environment-Variable when using "UseShellExecute") It should not be fixed by using /bin/sh but by searching FileName in the PATH-Environment-Variable before passing it to the helper application. Additionally, we might want to ensure that if FileName is an executable, it is executed without "helper application", because ShellExecute on windows allows executing of application-binaries and some of the "helper applications" seem to not really support that.

@danmoseley
Copy link
Member

We attempted to match Mono behavior. It's a good point that the original 'sh' behavior we had would be useful for some folks and maybe we should have a way to choose that. Also we can discuss whether UseShellExecute=true on Unix should use sh or xdg-open. In the Windows world, Shell Execute means more or less "here's a file please give it to the correct program". Maybe we need ExecuteWithShell=true|false also. @wfurt will open an issue to discuss this...

@stephentoub
Copy link
Member

because that's not what "UseShellExecute" is about

Says who?

@wfurt
Copy link
Member

wfurt commented Oct 17, 2017

I created #24704 with some more examples of 2.0 behavior. Who ever cares about Process.Start() with shell should jump on that thread.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.