Kill a one-file Python process in C#

on

A recent project of mine involved starting and killing processes on Windows. Some of these were scripts written in Python compiled to one .exe file. We ran into some serious problems with killing these processes. I will post a rant here explaining what was happening and how we got around this. If you are interested in following this up, the source code is available on GitHub.

The Python part

For the purpose of this blog post I will create a script that prints out how many seconds passed since the script was started. I am using Python 2.7 (as this is what we used in the project I worked on). The code looks like this:

import time
for i in range(1000):
  print ("{} seconds passed.".format(i))
  time.sleep(1)

Compile a script like this into one .exe file. You can use the pyinstaller to do this:

pyinstaller.exe --onefile myScript.py

If you do not have pyinstaller installed, you can install it by running:

pip install pyinstaller

The output .exe file will be in the dist/myScript.exe. You can try running it, it should look like this:

Exampe of output from myScript.exe

The “basic” C# part

In C# I will create two methods, one for starting a process and another one for killing a process. Do note that the whole blog post discusses actually killing the process with the .Kill() approach. You can argue that it would be better to use something like .CloseMainWindow(), as it allows the process to end in a “natural” way. This is outside of the scope of this blog post, here I am focusing mainly on the kill itself.

The interface for the start and kill methods is provided here:

public interface IProcessManagementService
{
  /// <summary>
  /// Method starts a system process runnig the file provided (should be an .exe file).
  /// </summary>
  /// <param name="filename">file to be run</param>
  /// <returns>system ID assigned by the system</returns>
  int StartNewProcess(string filename);

  /// <summary>
  /// Method kills a system process running
  /// </summary>
  /// <param name="systemId">system ID of the process to be killed</param>
  void KillProcess(int systemId);
}

We will set that the .exe file is started from the shell (UseShellExecute = true):

public class ProcessManagementService : IProcessManagementService
{
  public void KillProcess(int systemId)
  {
    Process p = Process.GetProcessById(systemId);
    if(p != null)
    {
      p.Kill();
    }
  }

  public int StartNewProcess(string filename)
  {
    int sysId = -1;
    var p = new Process();
    p.StartInfo.FileName = filename;
    p.StartInfo.UseShellExecute = true;
    if(p.Start())
    {
      sysId = p.Id;
    }
    return sysId;
  }
}

And when you try the Start method, it works like a charm. When you try the Kill method, it works without errors… but the window with the script running is still there and the script is still alive and kicking. And will be for the next 1000 seconds or so. But we just killed it, didn’t we? What is happening here, what is causing this problem?

Debugging with Task Manager

Try using these methods with the task manager running. You will note an interesting detail:

Task manager screenshot after the process has started

We see three processes here. One is the Console Window Host, as we did set the Use Shell Script to be true. But you will note two instances of myScript.exe. Even better – after we run the Kill method, we get the following:

Task manager screenshot after the KillProcess method has ran

We see the script doing quite well in the background, and in the task manager we see that two processes out of three have been killed. But the one we actually wanted to kill, the script itself is still there. You can imagine I (as someone who is not a Python programmer) was quite frustrated by this. And then I started googling for different solutions and I got amazed by different ideas people with this problem had.

So – what IS the problem?

When you compile to an .exe file with the -f (or –onefile) option and run the .exe, what actually happens is the following:

  1. A loader program is run
  2. The loader program decompresses your script to a temporary directory
  3. Your program is run as a different process from there
  4. The loader is kept alive so it can clean up the temporary directory if the program exits or crashes

In normal circumstances this is just fine, the program works well and the user does not really care about the fact that actually two processes exist. Until you need to kill them. The problem is that once you run the code:

if(p.Start())
{
  sysId = p.Id;
}

in the sysId variable you actually get the ID of the loader process. And there is no (easy) way to get to the process which is running your program.

The solution

The tree that actually gets created is the following:

  • Loader
    • Console window
    • Script

The system process ID we get from the Process object is the ID of the Loader. However, the .Kill() method does not automatically recursively kill all children. Whether this should happen, or whether this could be an option in the Process class is being discussed here: https://github.com/dotnet/corefx/issues/26234. Also, it is important to note that the Process class does not contain a property to get the process parent or process children.

The solution we used was to build a recursive method that finds all children, kills them, and then kills the main process. The solution was proposed in this StackOverflow thread: https://stackoverflow.com/questions/5901679/kill-process-tree-programmatically-in-c-sharp.

The solution proposed in the Stack Overflow answer uses ManagementObjects and assumes adding a reference to the System.Management namespace. However, this does not exist in ASP.NET Core by default (as it is specialized for Windows OS, and .NET Core wants to go beyond working only on Windows). However, the System Management namespace is available through the NuGet Package Manager:

System.Management available through NuGet Package Manager

As our solution needed to work only on Windows, this was good enough for our purposes. In order to solve this problem on different operating systems (or even better, universally), you should use other approaches (or other tools for a similar approach).

The code itself is fairly simple:

public void KillProcess(int systemId)
{
  KillProcessAndChildren(systemId);
}

/// <summary>
/// Kill a process, and all of its children, grandchildren, etc.
/// </summary>
/// <param name="pid">Process ID.</param>
private static void KillProcessAndChildren(int pid)
{
  // Cannot close 'system idle process'.
  if (pid == 0)
  {
    return;
  }
  ManagementObjectSearcher searcher = new ManagementObjectSearcher
    ("Select * From Win32_Process Where ParentProcessID=" + pid);
  ManagementObjectCollection moc = searcher.Get();
  foreach (ManagementObject mo in moc)
  {
    KillProcessAndChildren(Convert.ToInt32(mo["ProcessID"]));
  }
  try
  {
    Process proc = Process.GetProcessById(pid);
    proc.Kill();
  }
  catch (ArgumentException)
  {
    // Process already exited.
  }
  catch (Win32Exception)
  {
    // Access denied
  }
}

Note that the actual .Kill() method is inside a try-catch block and two exceptions are caught. The ArgumentException will be thrown if, for some reason, the process already exited. The Win32Exception will be thrown in case of an “Access denied” error. This is happening because the process is being killed by something else or is exiting on its own and does not allow the kill operation.

Conclusion

Thanks a lot to Contango, his answer in StackOverflow really helped me a lot with this. Personally, I would like to see the enhancement of the Process class in ASP.NET Core. I will probably be building my own at one point (and it will probably be a new blog post at some point!), but for now I am very happy that I was first able to define what was the problem that was occurring and a way to solve it.

 

Leave a Reply

Your email address will not be published. Required fields are marked *

You are currently offline