The Role of a Windows Process

In simple terms, a process is a running program. Formally speaking, a process is an operating system-level concept used to describe a set of resources (such as external code libraries and the primary thread) and the necessary memory allocation used by a running application. For each .NET Core Application loaded into memory, the OS creates a separate and isolated process for use during its lifetime.

Using this approach to application isolation, given that the failure of one process cannot be directly accessed by another process, unless you use specific tools such as System.IO.Pipes or the MemoryMappedFile class.

Every Windows process is assigned a unique process identifier (PID) and may be independently loaded and unloaded by the OS as necessary.

The Role of Threads

Every windows process contains an initial "thread" that functions as the entry point for the application. First, a thread is a path of execution within a process. Formally speaking, the first thread created by a process's entry point is termed the primary thread. Any .NET Core program marks its entry point with the Main() method or a file containing top-level statement.

Processes that contain a single primary thread of execution are intrinsically thread-safe given that there is only one thread that can access the data in the application at a given time.

The operating systems that are supported by .NET Core make it possible for the primary thread to spawn additional secondary threads (also termed worker threads) using a handful of API functions such as CreateThread().

Each thread is given the ability to write to TLS (Thread Local Storage) and is provided with a separate call stack.

Interacting with Processes Using .NET Core

The System.Diagnostics namespace defines several types that allow you to programmatically interact with processes and various diagnostics-related types such as the system event log and performance counter.

System.Diagnostics Namespace

  1. Process : This class provides access to local and remove processes and allows you to programmatically start and stop processes.
  2. ProcessModule : This type represents a module (*.dll or *.exe) that is loaded into a process. This type can represent any module - COM-based, .NET-based, or traditional C-based binaries.
  3. ProcessModuleCreation : This provides a strongly typed collection of ProcessModule objects.
  4. ProcessStartInfo : This specifies a set of values used when starting a process via the Process.Start() method.
  5. ProcessThread : This type represents a thread within a given process. Be aware that ProcessThread is a type used to diagnose a process is thread set and is not used.
  6. ProcessThreadCollection : This provides a strongly typed collection of ProcessThread objects.

System.Diagnostics.Process Class's Properties

  1. ExitTime : This property gets the timestamp associated with the process that has terminated (DateTime).
  2. Id : This property gets the PID for the associated process.
  3. MachineName : This property gets the name of the computer the associated process is running on.
  4. ProcessName : This property gets the name of the process.
  5. StartTime : This prorperty gets the time that the associated process was started (DateTime(
  6. Handle : This property returns the handle (represented by an IntPtr) associated to the process by the OS. This can be useful when building .NET applications that need to communicate with unmanaged code.
  7. MainWindowTitle : This gets the caption of the main windows of the process (if the process does not have a main window, you receive an empty string).
  8. Modules : This property provides access to the strongly typed ProcessModuleCollection type, which represents the set of modules (*.dll or *.exe) loaded within the current process.
  9. Responding : This property gets a value indicating whether the user interface of the process is responding to user input (or is currently "hung").
  10. Threads : This property gets the set of threads that are running in the associated process (represented via a collection of ProcessThread objects)

Methods

  1. CloseMainWindow() : This method closes a process that has a user interface by sending a close message to its main window.
  2. GetCurrentProcess() : This static method returns a new Process object that represents the currently active process.
  3. GetProcesses() : This static method returns an array of new Process objects running on a given machine.
  4. Kill() : This method immediately stops the associated process.
  5. Start() : This method starts a process.

Enumerating Running Processes

// Get all the processes on the local machine, ordered by PID
var runningProcs = from proc in Process.GetProcesses(".") orderby proc.Id select proc;
foreach(var p in runningProcs)
 {
     Console.WriteLine(p.Id+" "+p.ProcessName);
 }

Investigating a Specific Process

The static Process.GetProcessById() method allows you to obtain a single Process object via the associated PID.

Process theProc = null;
try
 {
     theProc = Process.GetProcessById(30592);
     Console.WriteLine(theProc.ProcessName);
 }catch(Exception e)
 {
     Console.WriteLine(e.Message);
 }

Investigating a Process's Thread Set

ProcessThreadCollection theThreads = theProc.Threads;
foreach(ProcessThread pt in theThreads)
 {
     Console.WriteLine(pt.Id+" "+pt.StartTime.ToShortTimeString()+" "+pt.PriorityLevel);
 }

The ProcessThread type has additional members of interest beyond Id,StartTime, and PriorityLevel.

Members of ProcessThread Type

  1. CurrentPriority : Gets the current priority of the thread.
  2. Id : Gets the unique identifier of the thread.
  3. IdealProcessor : Sets the preferred processor for this thread to run on.
  4. PriorityLevel : Gets or sets the priority level of the thread.
  5. ProcessorAffinity : Sets the processors on which the associated thread can run.
  6. StartAddress : Gets the memory address of the function that the OS called that started this thread.
  7. StartTime : Gets the time that the OS started the thread.
  8. ThreadState : Gets the current state of this method.
  9. TotalProcessorTime : Gets the total amount of time that this thread has spent using the processor.
  10. WaitReason : Gets the reason that the threat is waiting.

ProcessThread type is not the entity used to create, suspend, or kill threads under the .NET Core platform. Rather, ProcessThread is a vehicle used to obtain diagnostic information for the active Windows threads within a running process.

Investigating a Process's Module Set

When talking about processes, a module is a general term used to describe a given *.dll (or the *.exe itself) that is hosted by a specific process.

ProcessModuleCollection theMods = theProc.Modules;
foreach(ProcessModule pm in theMods)
 {
     Console.WriteLine(pm.ModuleName); // kernel32.dll,ntdll.dll,user32.dll,..
 }
 
Starting and Stopping Processes Automatically
Process theProc = null;
try
 {
     theProc = Process.Start(@"C:\Program Files (x86)\msedge.exe", "dotnetguard.blog");
 }catch(Exception e)
 {
     Console.WriteLine(e.Message);
 }
 
 
// killall of the msedge.exe
try
 {
     foreach(var p in Process.GetProcessesByName("MsEdge"))
     {
         p.Kill(true);
     }
 }
catch{}

Controlling Process Startup Using the ProcessStartInfo Class

The Process.Start() method also allows you to pass in a System.Diagnostics.ProcessStartInfo type to specify additional bits of information regarding how a given process should come to life.

Process theProc = null;
try
 {
     ProcessStartInfo startInfo = new ProcessStartInfo("MsEdge", "dotnetguard.blog");
     startInfo.UseShellExecute = true;
     theProc = Process.Start(startInfo);
 }catch(Exception e)
 {
     Console.WriteLine(e.Message);
 }

In .NET Core, the UseShellExecute property default to false, while in prior versions of .NET, the UseShellExecute property default to true. This is the reason that the previous version of Process.Start(), shown here, no longer works without using ProcessStartInfo and setting the UseShellExecute property to true:

Process.Start("msedge");

Leveraging OS Verbs with ProcessStartInfo

In addition to using the OS shortcuts to launch applications, you can also take advantage of file associations with ProcessStartInfo. On Windows, if you right-click a Word document, there are options to editor print the document.

ProcessStartInfo pi = new ProcessStartInfo(@"..\TestPage.docx");
foreach (var verb in pi.Verbs)
 {
     Console.WriteLine(verb);
 }
pi.WindowStyle = ProcessWindowStyle.Maximized;
pi.Verb = "Edit";
pi.UseShellExecute = true;
Process.Start(pi);

Understanding .NET Application Domains

Under the .NET and .NET Core platforms, executables are not hosted directly within a window process as is the case in traditional unmanaged applications. Rather, .NET and .NET Core executables are hosted by a logical partition within a process called as an application domain.

Process = Windows Process (.exe)

AppDomain = Logical Partition Inside Process (only ONE in .NET Core)

LoadContext = Assembly isolation within the AppDomain

Process -> AppDomain -> LoadContext

Building -> Apartment -> Room

The System.AppDomain Class

The AppDomain class is largely deprecated with .NET Core. While most of the remaining support is designed to make migrating from .NET 4.X to .NET Core easier.

Interacting with the Default Application Domain

Your application has access to the default application domain using the static AppDomain.CurrentDomain property. After you have this access point, you can use the methods and properties of AppDomain to perform some runtime diagnostics.

// Get Access to the AppDomain for the Current Thread.
AppDomain defaultAD = AppDomain.CurrentDomain;
Console.WriteLine("Name of the domain : "+defaultAD.FriendlyName);
Console.WriteLine("ID of domain in this process : "+defaultAD.Id);
Console.WriteLine("Is this the default domain : "+defaultAD.IsDefaultAppDomain());
Console.WriteLine("Base directory of this domain : "+defaultAD.BaseDirectory);
Console.WriteLine("Application Base : "+defaultAD.SetupInformation.ApplicationBase);
Console.WriteLine("Target Framework : "+defaultAD.SetupInformation.TargetFrameworkName);

Enumerating Loaded Assemblies

It's also possible to discover all the loaded .NET Core assemblies within a given application domain using the instance-level GetAssemblies() method. This method will return to you an array of Assembly objects.

// Get Access to the AppDomain for the Current Thread.
AppDomain defaultAD = AppDomain.CurrentDomain;
Assembly[] aS = defaultAD.GetAssemblies();
foreach (Assembly a in aS)
 {
     Console.WriteLine(a.GetName().Name+" "+a.GetName().Version);
 }

Assembly Isolation with Application Load Contexts

AppDomains are logical partitions used to host .NET Core assemblies. Additionally, an application domain may be further subdivided into numerous load context boundaries. Conceptually, a load context creates a scope for loading, resolving, and potentially unloading a set of assemblies.

Add -> New Project -> Add .NET Core Class Library.

class Car
 {
     public string PetName { get; set; }
     public string Make { get; set; }
     public int Speed { get; set; }
 }

Add -> Project Reference

static void LoadAdditionalAssembliesDifferentContext()
 {
     var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ClassLibrary1.dll");
     AssemblyLoadContext lc1 = new AssemblyLoadContext("NewContext1", false);
     var cl1 = lc1.LoadFromAssemblyPath(path);
     var c1 = cl1.CreateInstance("ClassLibrary1.Car");
    var path2 = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ClassLibrary1.dll");
     AssemblyLoadContext lc2 = new AssemblyLoadContext("NewContext2", false);
     var cl2 = lc2.LoadFromAssemblyPath(path);
     var c2 = cl2.CreateInstance("ClassLibrary1.Car");
}

Note: You might be wondering why you created a reference for an assembly that will be loaded dynamically. This is to make sure that when the project builds, the ClassLibrarry1 assembly builds as well and is in the same directory as the DefaultAppDomainApp. there is no need to reference an assembly that you will load dynamically.

// All false
Console.WriteLine(cl1.Equals(cl2));
Console.WriteLine(cl1 == cl2);
Console.WriteLine(c1.Equals(c2));
Console.WriteLine(c1 == c2);

If we want to LoadAdditionalAssemblies same context:

var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ClassLibrary1.dll");
AssemblyLoadContext lc1 = new AssemblyLoadContext(null, false);
var cl1 = lc1.LoadFromAssemblyPath(path);
var c1 = cl1.CreateInstance("ClassLibrary1.Car");
var cl2 = lc1.LoadFromAssemblyPath(path);
var c2 = cl2.CreateInstance("ClassLibrary1.Car");
         
Console.WriteLine(cl1.Equals(cl2)); // true
Console.WriteLine(cl1 == cl2);  // true
Console.WriteLine(c1.Equals(c2)); // false
Console.WriteLine(c1 == c2); // false