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();
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.
Last updated: March 29, 2023