joelc
joelc

Reputation: 2751

Process.Start() differences between Windows and Linux

I'm experiencing some differences in Process.Start() in a .NET Core 2.2 project. You can find the full source in this gist: https://gist.github.com/jchristn/5a2a301baedeed787a2e57cd528e46d6

I have a method for starting processes:

private static void ExecuteShell(
    string filename, 
    string args, 
    bool useShellExecute,
    bool redirectStdOut,
    bool redirectStdErr,
    out int returnCode, 
    out string consoleOutput)
{
    returnCode = 0;
    consoleOutput = null;

    if (String.IsNullOrEmpty(filename)) throw new ArgumentNullException(nameof(filename));

    // fileName, i.e. "cmd.exe"
    // args, i.e.     "/c dir /w"

    Process process = new Process();
    process.StartInfo.FileName = filename;
    process.StartInfo.Arguments = args;
    process.StartInfo.UseShellExecute = useShellExecute;
    process.StartInfo.RedirectStandardOutput = redirectStdOut;
    process.StartInfo.RedirectStandardError = redirectStdErr;
    process.Start();
    if (process.StartInfo.RedirectStandardOutput) consoleOutput = process.StandardOutput.ReadToEnd();
    process.WaitForExit();
    returnCode = process.ExitCode;
}

And the caller looks like this:

while (true)
{
    try
    {
        Console.WriteLine("");
        Console.WriteLine("Example: cmd.exe /c dir /w");
        Console.Write("Command: ");
        string userInput = Console.ReadLine();
        if (String.IsNullOrEmpty(userInput)) break;

        string[] parts = userInput.Split(new char[] { ' ' }, 2);

        string filename = parts[0];
        string arg = null;
        if (parts.Length > 1) arg = parts[1];

        bool useShellExecute = InputBoolean("  Use shell execute  : ", false);
        bool redirectStdOut =  InputBoolean("  Redirect stdout    : ", false);
        bool redirectStdErr =  InputBoolean("  Redirect stderr    : ", false);

        int returnCode;
        string consoleOutput;

        ExecuteShell(
            filename,
            arg,
            useShellExecute,
            redirectStdOut,
            redirectStdErr,
            out returnCode, out consoleOutput);

        Console.WriteLine("Return code    : " + returnCode);
        Console.WriteLine("Console output : " + consoleOutput);
    }
    catch (Exception e)
    {
        Console.WriteLine("Exception: " + Environment.NewLine + SerializeJson(e, true));
    }
}

The simplest way to reproduce the larger issue I'm having is this. Assume there is a file on the file system and I want to type testfile > testfile2, i.e. to pipe to another file.

On Windows, if I use cmd.exe /c type testfile > testfile2 it works great (with the three Boolean values set to false).

i.e.

C:\Code\ExecuteShell\ExecuteShell\bin\Debug\netcoreapp2.2>dotnet ExecuteShell.dll

Example: cmd.exe /c dir /w
Command: cmd.exe /c type testfile > testfile2
  Use shell execute  :  [y/N]?
  Redirect stdout    :  [y/N]?
  Redirect stderr    :  [y/N]?
Return code    : 0
Console output :

Example: cmd.exe /c dir /w
Command:
C:\Code\ExecuteShell\ExecuteShell\bin\Debug\netcoreapp2.2>dir
 Volume in drive C is OS
 Volume Serial Number is 72E2-466A

 Directory of C:\Code\ExecuteShell\ExecuteShell\bin\Debug\netcoreapp2.2

... portions removed ...
04/09/2020  05:41 PM                15 testfile
04/09/2020  05:41 PM                15 testfile2

When I try this on Ubuntu 14.04 it fails.

~/code/ExecuteShell/ExecuteShell/bin/Debug/netcoreapp2.2/publish$ dotnet ExecuteShell.dll

Example: cmd.exe /c dir /w
Command: cat testfile > testfile2
  Use shell execute  :  [y/N]?
  Redirect stdout    :  [y/N]?
  Redirect stderr    :  [y/N]?
Hello, world!

cat: >: No such file or directory
cat: testfile2: No such file or directory
Return code    : 1
Console output :

If I try it with useShellExecute set to true, I get this strange xdg-open issue:

~/code/ExecuteShell/ExecuteShell/bin/Debug/netcoreapp2.2/publish$ dotnet ExecuteShell.dll

Example: cmd.exe /c dir /w
Command: cat testfile > testfile2
  Use shell execute  :  [y/N]? y
  Redirect stdout    :  [y/N]?
  Redirect stderr    :  [y/N]?
xdg-open: unexpected argument 'testfile'
Try 'xdg-open --help' for more information.
Return code    : 1
Console output :

Any idea how to make this work?

Upvotes: 1

Views: 4129

Answers (2)

Ryan
Ryan

Reputation: 2010

Process is a fairly opaque and convoluted abstraction and unfortunately it has lots of undocumented pitfalls, mostly because it attempts to emulate Windows-like behaviour on Unix. The only way to figure out exactly what it's doing is to read the latest and greatest .NET source code.

ProcessStartInfo.UseShellExecute behaves weird on Unix-like systems:

https://github.com/dotnet/runtime/blob/cc585f6d611b4d0cef2b19c4a1ca1e5ec435e80c/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs#L408-L446

It does the following in order:

  1. Attempts to resolve absolute executable path. If the path is "rooted" (starts with /), it uses it as is. If the path is a URI, and that URI describes a local path on the system, it uses the local path as the path. If the filename is relative, it resolves it into an absolute path by first searching the working directory, and then by searching the current PATH. Finally, it checks the file at the path has the x (Execute) bit set, and if it doesn't, throws it away.

  2. If the absolute path was resolved, and the file appears to be executable, it attempts to execute it directly.

  3. If the file couldn't be executed directly, attempt to find the default program that can be used to open the file. This is operating system dependent. On Linux, it searches for any of "xdg-open", "gnome-open", "kfmclient" in the path. On FreeBSD, it tries to use "/usr/local/bin/open". On OSX, it tries to use "/usr/bin/open".

  4. Finally it runs the default program which takes the original filename as the first argument and the rest of the arguments after that.

So, this is why you get that xdg-open error. It's attempting to open the file with a Window Manager to simulate how Windows operates. For this reason UseShellExecute = false is almost always preferable on Unix.

As for the arguments issue, it causes problems because .NET is splitting the string into an argv array using its own little state machine tokenizer:

https://github.com/dotnet/runtime/blob/cc585f6d611b4d0cef2b19c4a1ca1e5ec435e80c/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs#L853-L945

This notably ignores single quotes ('), only recognizing double quotes ("). Again, it's an annoying abstraction over what's actually passed to the process, the argv array.

It's much better to ignore ProcessStartInfo.Arguments entirely and instead add individual arguments with ProcessStartInfo.ArgumentList.Add(), which will cause them to be translated directly into the process argv array.

Upvotes: 1

bk2204
bk2204

Reputation: 76409

In this case, you're confusing the use of the word “shell” in its Unix context (a command line interpreter) with its Windows and C# use:

The word "shell" in this context (UseShellExecute) refers to a graphical shell (similar to the Windows shell) rather than command shells (for example, bash or sh) and lets users launch graphical applications or open documents.

So useShellExecute actually means that you can give the program a document of some type and expect it to be opened by a suitable program. That's what xdg-open does, so that's probably why C# invokes it.

In your case, the command you want to run is sh -c 'cat testfile > testfile2'. That's the equivalent of your cmd invocation. However, your code won't work if you do that now because you split on whitespace. So you'll end up with the arguments sh, -c, 'cat, testfile, >, and testfile2'. Unlike cmd, which is responsible for its own argument processing, sh does not concatenate its commands with whitespace, and this won't work.

If you were passing this as an array of arguments, you'd want your arguments to be sh, -c, and cat testfile > testfile2; that is, the entire string you want to pass to the shell should be one complete argument.

Upvotes: 4

Related Questions