Working with processes using C#

As software developers, working with processes is something that we frequently need to do.

Whether it’s checking if a specific process is running, stopping a process, or creating a new process, it is useful to have a reference point for how to accomplish each of these tasks.

In the sections below, I’m going to cover how to achieve all of the above using C# by providing a set of reusable methods which can be packaged together into a helper class.

Prerequisites

In order to run the code samples in the following sub-sections, you’ll need access to an IDE or code editor such as Visual Studio or Visual Studio Code.

You’ll also need to have a basic understanding of C# in order to follow along.

Logging

When you’re working with processes, it can be beneficial to log the results of process-related operations.

In the sample code contained in this article, I use a logging framework called Serilog. Serilog is a simple, yet powerful logging framework which is becoming increasingly popular in the .NET community.

The simplest way of setting up Serilog is to use the static Log class. The code below will configure Serilog to log output to the Console.

Log.Logger = new LoggerConfiguration()
   .MinimumLevel.Information()
   .WriteTo.Console()
   .CreateLogger();

Note that setting the MinimumLevel to Information means that anything logged at the Information level, plus more important/critical log levels such as Warning and Error will all be logged.

Writing a log message is as simple as the following line of code.

Log.Information("Hi, Jonathan!");

Parameters can be passed in a similar way to other logging frameworks by using curly braces e.g. {0}, {1} but can also be named for better searchability, as per the code below.

string firstName = "Jonathan";
 
Log.Warning("Wait, {name}!", firstName);

Note that the text you use within the curly braces can be whatever you want it to be. The text doesn’t need to match the names of the parameters that are passed in as they are ‘positional’.

Serilog has a concept of ‘sinks’ which act as an abstraction of the formats which Serilog can log to, such as the Console. As a result, you’ll need to install both the main Serilog NuGet package as well as the Serilog.Sinks.Console NuGet package in order to run the sample code.

Many other log sinks are available for Serilog, such as the File sink and the Elasticsearch sink.

Now, let’s move on to working with processes.

Create a process

First of all, how do we create a new process and start it? Let’s look at how this is accomplished using C#, specifically the creation of a hidden process.

/// <summary>
/// Creates a process that runs in the background with no UI.
/// </summary>
/// <param name="processFilePath">The file path for the process to start</param>
/// <param name="processArguments">The command-line arguments to use</param>
/// <returns>The created hidden process</returns>
public static Process CreateHiddenProcess(string processFilePath, string processArguments)
{
    Log.Information("Creating hidden process '{0}' with arguments '{1}'", processName, processArguments);
 
    var process                   = new Process();
    process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
    process.StartInfo.FileName    = processFilePath;
    process.StartInfo.Arguments   = processArguments;
 
    Log.Information("Hidden process '{0}' with arguments '{1}' created", processName, processArguments);
 
    return process;
}

The CreateHiddenProcess method takes the name/file-path of the process to create and the arguments to pass to the process when it starts. Typically when you create a hidden process you’ll be starting a command-line program that requires arguments to be passed to it e.g. ping or msiexec.

Setting the WindowStyle to Hidden instructs the program you are launching to not show a window if the program normally launches a user interface.

After creating the hidden process we need to call the Start method on the Process object to actually start the process.

Process process = ProcessHelper.CreateHiddenProcess("notepad"@"C:\Windows\System32\drivers\etc\hosts");
process.Start();

The above code creates an invisible ‘Notepad’ process and starts it. The path to the system ‘hosts’ file is passed as an argument to the Notepad program which causes Notepad to load the file.

A variation on creating a hidden process is the creation of a background process that redirects the output from a command-line program such that we can capture it from our code and log it.

/// <summary>
/// Creates a process that runs in the background and redirects command line output.
/// </summary>
/// <param name="processFilePath">The file path for the process to start</param>
/// <param name="processArguments">The command line arguments to use</param>
/// <returns>The created background process</returns>
public static Process CreateBackgroundProcess(string processFilePath, string processArguments)
{
    Log.Information("Creating background process '{0}' with arguments '{1}'", processFilePath, processArguments);
 
    var process                              = new Process();
    process.EnableRaisingEvents              = true;
    process.StartInfo.UseShellExecute        = false;
    process.StartInfo.RedirectStandardOutput = true;
    process.StartInfo.FileName               = processFilePath;
    process.StartInfo.CreateNoWindow         = true;
    process.StartInfo.Arguments              = processArguments;
 
    Log.Information("Background process '{0}' with arguments '{1}' created", processFilePath, processArguments);
 
    return process;
}

Setting the EnableRaisingEvents and RedirectStandardOutput properties to true are the two key differences from the previous method which allow output from a command-line program to be captured by our code.

Setting the UseShellExecute property to false means that the process will be launched by our program directly as opposed to being launched separately by the Operating System.

Setting the CreateNoWindow property to true means that the process will be started without creating a new window to contain it.

var process = ProcessHelper.CreateBackgroundProcess("bcp""AdventureWorksDW2017.dbo.DimProduct out C:\Data\ProductData.txt -S SPECTRE-007 -T");
process.OutputDataReceived += (s, e) => Console.WriteLine(e.Data);
process.Start();

The above code creates a background process and wires up the OutputDataReceived method to write the output from the command-line program to the console. The process is then started.

Another process property to be aware of is the WorkingDirectory property (set via StartInfo) which sets the directory that a process will be started in. This can be important when the program you are launching assumes a certain working directory e.g. its install folder.

Check if a process is running

How about checking if a process is currently running? The code below demonstrates how to achieve this.

/// <summary>
/// Checks if a process is currently running.
/// </summary>
/// <param name="processName">The name of the process to check</param>
/// <returns>True if the process is running, otherwise false</returns>
public static bool ProcessIsRunning(string processName)
{
    Log.Information("Checking if any processes named '{0}' exist", processName);
    
    Process[] processes = Process.GetProcessesByName(processName);
 
    int processesCount = processes.Count();
 
    if (processesCount > 0)
    {
        Log.Information("Found {0} existing process(es) named '{1}'", processesCount, processName);
 
        int currentProcessId = Process.GetCurrentProcess().Id;
 
        foreach (Process process in processes)
        {
            if (process.Id == currentProcessId)
            {
                Log.Information("Ignoring existing process named '{0}' which matches current process ID of: {1}", processName, currentProcessId);
            }
            else
            {
                Log.Information("Found existing process named '{0}'", processName);
                return true;
            }
        }
    }
    else Log.Information("No existing processes named '{0}' exist", processName);
 
    return false;
}

The ProcessIsRunning method accepts the name of the process to check as a parameter.

The GetProcessesByName method on the Process class is used to get an array of Process objects which match the specified process name.

The code then iterates through the processes and returns true if there is a matching process that isn’t the current process.

Stop a process

Sometimes we need to stop a process in its tracks. Below is an example of how to do this.

/// <summary>
/// Kills all instances of a specified process.
/// </summary>
/// <param name="processName">The name of the process to kill</param>
/// <param name="timeout">The number of milliseconds to wait for the process to stop</param>
public static void KillProcess(string processName, int timeout = 30000)
{
    Log.Information("Checking if any processes named '{0}' exist", processName);
 
    Process[] processes = Process.GetProcessesByName(processName);
 
    int processesCount = processes.Count();
 
    if (processesCount > 0)
    {
        Log.Information("Found {0} existing process(es) named '{1}'", processesCount, processName);
 
        int currentProcessId = Process.GetCurrentProcess().Id;
 
        foreach (Process process in processes)
        {
            if (process.Id == currentProcessId)
            {
                Log.Information("Ignoring existing process named '{0}' which matches current process ID of: {1}", processName, currentProcessId);
            }
            else
            {
                Log.Information("Killing existing process named '{0}'", processName);
                process.Kill();
                process.WaitForExit(timeout);
 
                if (process.HasExited) Log.Information("Killed existing process named '{0}'", processName);
                else Log.Warning("Waited for 30 seconds, but couldn't kill process named '{0}'", processName);
            }
        }
    }
    else Log.Information("No existing processes named '{0}' exist", processName);
}

The KillProcess method accepts the name of the process to stop as a parameter, as well as a timeout parameter that configures how long the method will wait for a process to stop before giving up.

After getting an array of Process objects, the code iterates through the array and ignores the current process, as per the ProcessIsRunning method.

For all other processes matching the specified process name, the Kill method of the Process object is used to stop the process. The WaitForExit method is then used to ensure that the process has stopped successfully.

Before returning to the caller, the HasExited property is inspected and an appropriate log message is recorded.

Summary

In this article, we’ve seen how to accomplish some common process-related operations using C#.

Creating wrapper methods to hide the low-level details of interacting with processes can help to make our code much cleaner and avoid code duplication.

If you would like to see a working example of the code from this article in action, please take a look at the accompanying GitHub repository to access the full sample project.

Comments

This site uses Akismet to reduce spam. Learn how your comment data is processed.