Starting and Debugging Processes Programmatically in C# with EnvDTE

Posted in software by Christopher R. Wirz on Mon Dec 18 2017



EnvDTE is an assembly-wrapped COM library containing the objects and members for Visual Studio core automation. This library is probably one of the most under-utilized - considering its capabilities. One of its best features is the ability to attach a debugger to a process - meaning event-driven debugging is a lot easier.

Note: Your C# project must reference C:\Program Files (x86)\Common Files\Microsoft Shared\MSEnv\PublicAssemblies\envdte.dll
When you add an assembly reference to EnvDTE.dll, you must also set the Embed Interop Types property of the assembly to false.

First, let's start with a way to start or get a running process.


using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Linq;

static System.Diagnostics.Process LoadIfNotRunning(string processName, string exe = null, string args = null, bool hidden = false)
{
	var processes = System.Diagnostics.Process.GetProcessesByName(processName);
	if (processes.Any()) { return processes.First(); }
	if (exe == null)
	{
		exe = processName;
	}
	System.Diagnostics.Process p = null;
	if (!exe.ToLowerInvariant().EndsWith(".exe"))
	{
		exe += ".exe";
	}
	if (!exe.Contains(@":\"))
	{
		exe = Path.Combine(Environment.CurrentDirectory, exe);
	}

	try
	{
		var pi = new ProcessStartInfo(
			exe,
			args);
		if (hidden)
		{
			pi.UseShellExecute = false;
			pi.CreateNoWindow = true;
			pi.WindowStyle = ProcessWindowStyle.Hidden;
		}
		p = System.Diagnostics.Process.Start(pi);
		#if DEBUG
		Console.WriteLine("Starting " + p.ProcessName + "...");
		#endif
	} catch { }
	return p;
}

The above code will return a process if it exists, and will run the executable if it does not. Now, we need a way to attach the debugger. For this, we'll use the EnvDTE library.


using EnvDTE;

/// <summary>
///     Attaches Visual Studio to the specified process.
/// </summary>
/// <param name="process">The process.</param>
/// <param name="maxTries">The number of tries to get the process.</param>
public static void Attach(this System.Diagnostics.Process process, int maxTries = 5)
{
	// Reference visual studio core
	DTE dte = null;
	int version = 9;
	while (version < 17 && dte == null)
	{
		try
		{
			version++;
			dte = (DTE)Marshal.GetActiveObject(String.Format("VisualStudio.DTE.{0}.0", version));
		}
		catch (COMException)
		{
			#if DEBUG
			Console.WriteLine(String.Format("Visual studio {0} not found.", version));
			#endif
		}
	}

	if (dte == null)
	{
		Console.WriteLine("No debugger found, nothing attached...");
		return;
	}

	// Try loop - visual studio may not respond the first time.
	// We also don't want it to stall the main thread
	new System.Threading.Thread(() =>
	{
		while (maxTries-- > 0)
		{
			try
			{
				Processes processes = dte.Debugger.LocalProcesses;
				foreach (EnvDTE.Process proc in processes)
				{
					try
					{
						if (proc.Name.Contains(process.ProcessName))
						{
							proc.Attach();
#if DEBUG
							Console.WriteLine(String.Format("Attatched to process {0} successfully.", process.ProcessName));
#endif
							return;
						}
					}
					catch { }
				}
			}
			catch { }
			// Wait for debugger and application and debugger to find application
			System.Threading.Thread.Sleep(1500);
		}
	}).Start();
}

Okay, now it's time to put it all together in a console application that will just start "MyProgram".


public static class Program
{
	static void Main(string[] args)
	{
		List<System.Diagnostics.Process> processes = new List<System.Diagnostics.Process>();
		var myProgram = LoadIfNotRunning("MyProgram", exe: @"C:\Program Files (x86)\MyProgram\bin\MyProgram.exe");
		processes.Add(myProgram);
		#if DEBUG
		Attach(myProgram);
		#endif

		Console.WriteLine("Press any key to shut down...");
		Console.ReadKey();
		foreach (var p in processes)
		{
			try
			{
				Console.WriteLine("Killing " + p.ProcessName);
				p.Kill();
			}
			catch (Exception ex)
			{
				try
				{
					Console.WriteLine(ex.Message);
					Console.WriteLine("Closing " + p.ProcessName);
					p.Close();
				}
				catch (Exception ex2)
				{
					Console.WriteLine(ex2.Message);
				}
			}
		}
		Console.WriteLine("Goodbye!");
		Console.ReadKey();
	}
}

And that's it! "MyProgram" should be attached to the debugger when you debug run the console application.