The .NET Process class on Linux

The .NET Process class on Linux

In this article, we’ll look at .NET’s Process class. We’ll go over the basics of how and when to use it, then cover differences in usage between Windows and Linux, and point out a few caveats. This article covers behavior in .NET Core 3.0.

The basics

The Process class represents an instance of a running process. You can use it to start new processes using Process.Start or get running processes via the static GetProcessById,GetProcesses,GetProcessesByName methods.

When starting a new Process, all information to start the process is set on a ProcessStartInfo instance (PSI). PSI has properties like FileName and Arguments to set the program to execute and its arguments. UseShellExecute allows you to open documents. RedirectStandard{Input/Output/Error} allows you to write/read the standard I/O streams. Environment/EnvironmentVariables and WorkingDirectory allow you to control the environment variables and working directory.

.NET Core 2.1 (/netstandard 2.1) added an ArgumentList property. The Arguments property is a string and requires the user to use Windows command-line escaping rules (e.g., using double-quotes to delimit arguments). The ArgumentsList property is a Collection that holds separate arguments. Process.Start will take care of passing those to the underlying platform. It’s recommended to use ArgumentList over Arguments when targeting .NET Core 2.1+/netstandard2.1+.

The following example shows launching the echo application, with a single argument hello world, and then waiting for the process to terminate.

using var process = Process.Start(
    new ProcessStartInfo
    {
        FileName = "echo",
        ArgumentList = { "hello world" }
    });
process.WaitForExit();

Everything you need to grow your career.

With your free Red Hat Developer program membership, unlock our library of cheat sheets and ebooks on next-generation application development.

SIGN UP

Not supported on Linux

The following properties of ProcessStartInfo aren’t supported on Linux and throw PlatformNotSupportedException: PasswordInClearText, Domain, LoadUserProfile, Password.

Retrieving processes from a remote machine using the machineName overload GetProcessById,GetProcesses,GetProcessesByName is also not supported.

On the Process, it isn’t supported to set working set limits (MinWorkingSet, MaxWorkingSet). On a ProcessThread (obtained via Process.Threads) the PriorityLevel/ProcessorAffinity cannot be set.

Killing processes

Processes can be stopped by calling Process.Kill. On Linux, this is implemented by sending the SIGKILL signal, which tells the kernel to terminate the application immediately. It’s not possible to send a SIGTERM signal, which requests the application to gracefully terminate.

Since .NET Core 3.0, Process.Kill no longer throws Win32Exception/InvalidOperationException if the process is terminating or was already terminated. If you are targeting earlier .NET Core versions (or .NET Framework), you should add a try/catch block to handle these exceptions.

.NET Core 3.0 adds an overload to the Process.Kill method that accepts a bool entireProcessTree. When set to true, descendants of the process will also be killed.

UseShellExecute

The ProcessStartInfo.UseShellExecute property can be used to open documents. Shell refers to the graphical shell of the user, and not a command-line shell like bash. Setting this to true means behave as if the user double-clicks the file. When ProcessStartInfo.FileName refers to an executable, it will be executed. When it refers to a document, it will be opened using the default program. For example an .ods file will open with LibreOffice Calc. You can set FileName to an http-uri (like https://redhatloves.net) to open up a browser and show a website.

Unix shell scripts are considered real executables by the operating system (OS). This means it is not required to set UseShellExecute. This approach is unlike Windows .bat files, which need the Windows shell to find the interpreter.

On Windows, UseShellExecute allows alternative actions on a document (like printing) by setting the ProcessStartInfo.Verb. On other OSes, this property is ignored.

FileName resolution

When setting a relative filename on Process.FileName, the file will be resolved. The resolution steps depend on UseShellExecute being set or not. The resolution on non-Windows platforms is implemented to behave similarly to Windows.

When UseShellExecute is set to false:

  • Find the file in the native application directory.
  • Find the file in the process’s working directory.
  • Find the file on PATH.

The native application directory is the dotnet installation directory when executing dotnet. When using a native apphost, it’s the apphost directory.

When UseShellExecute is set to true:

  • Find an executable file in the ProcessStartInfo.WorkingDirectory. If that is not set, the process working directory is used.
  • Find an executable file on PATH.
  • Use the shell open program and pass it the relative path.

It’s important to know that in both cases directories are searched, which may be unsafe (executable directory, working directory). You may want to implement your own resolution and always set FileName to an absolute path.

Redirected streams

When UseShellExecute is set to false, you can redirect standard input, output, and error using ProcessStartInfo.RedirectStandard{Input/Output/Error}. Corresponding Encoding properties (like StandardOutputEncoding) allow you to set the encoding for the streams.

Unless you’re running or starting an interactive application (like launching vi), you should redirect the streams and handle them.

Note that asynchronous methods like Process.StandardOutput.ReadAsync and Process.BeginOutputReadLine use the ThreadPool to do asynchronous reads, which means they block a ThreadPool thread when waiting for application output. This approach can lead to ThreadPool starvation if you have many Processes that don’t output much. This is an issue on Windows, too.

If you are using Begin{Output/Error}ReadLine and call WaitForExit, that method will wait until all standard output/error was read (and corresponding {Output/Error}DataReceived events are emitted). This approach can cause the call to block for a process that has exited when there are descendants that are keeping the redirected streams open.

ProcessName

On Linux, when executing a shell script, Process.ProcessName holds the name of the script. Similarly, Process.GetProcessesByName will match script names. This capability is useful to identify processes regardless of whether they are native executables, scripts, or scripts wrapping native executables.

Process exit

Processes hold up some resources in the kernel. On Windows, this information is reference counted, which allows multiple users to keep the information alive using a Process Handle. On Unix, there is a single owner of this information. First, it is the parent process, and when the parent dies, it is the init (pid 1) process (or a process that assumed this responsibility using PR_SET_CHILD_SUBREAPER). The owning process is the process responsible for cleaning up the kernel resources (aka reaping the child). .NET Core reaps child processes as soon as they terminate.

When the resources are cleaned up, the information about the process can no longer be retrieved. Properties that return runtime information throw InvalidOperationException at that point. On Windows, you can retrieve StartTime, {Privileged,Total,User}ProcessorTime after the process exited. On Linux, these properties throw InvalidOperationException also.

On Linux, the Process.ExitCode is valid only for direct children. For other processes, it returns 0 or throws InvalidOperationException depending on the state of the Process.

If you are running in a container, often there is no init process. This means that no one is reaping orphaned children. Such child processes will keep consuming kernel resources, and .NET Core will never consider them exited. This issue occurs when an application has descendants that out-live their parents. If you are in this case, you should add an init process to your container. When using docker/podman run, you can add one using the --init flag.

Conclusion

In this article, we explained the behavior of .NET’s Process class on Linux. We covered basic use, non-supported behavior, differences with Windows, and other things to be aware of.

Share