File: src\Shared\ProcessSignaler.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
 
/// <summary>
/// Provides best-effort process signaling for graceful shutdown and forceful termination.
/// </summary>
internal static partial class ProcessSignaler
{
    public static void RequestGracefulShutdown(int pid, DateTimeOffset? expectedStartTime, ILogger logger)
    {
        using var process = TryGetRunningProcess(pid, expectedStartTime, logger);
        if (process is null)
        {
            return; // Process is not running or does not match the expected start time
        }
 
        RequestGracefulShutdown(pid, logger);
    }
 
    public static void RequestGracefulShutdownWithRuntimeStartTime(int pid, DateTimeOffset expectedStartTime, TimeSpan tolerance, ILogger logger)
    {
        using var process = TryGetRunningProcessWithRuntimeStartTime(pid, expectedStartTime, tolerance, logger);
        if (process is null)
        {
            return; // Process is not running or does not match the expected start time
        }
 
        RequestGracefulShutdown(pid, logger);
    }
 
    private static void RequestGracefulShutdown(int pid, ILogger logger)
    {
        logger.LogDebug("Requesting graceful shutdown of process {Pid}...", pid);
 
        if (OperatingSystem.IsWindows())
        {
            logger.LogDebug("Windows graceful process shutdown is handled by caller-specific process tree signaling.");
        }
        else
        {
            RequestGracefulShutdownUnix(pid, logger);
        }
    }
 
    public static void ForceKill(int pid, DateTimeOffset? expectedStartTime, ILogger logger, bool killEntireProcessTree = false)
    {
        using var process = TryGetRunningProcess(pid, expectedStartTime, logger);
        if (process is { })
        {
            logger.LogDebug("Killing process {Pid} (entireProcessTree={EntireProcessTree})...", pid, killEntireProcessTree);
            try
            {
                process.Kill(entireProcessTree: killEntireProcessTree);
            }
            catch (InvalidOperationException)
            {
                // Process already exited.
            }
        }
    }
 
    public static Process? TryGetRunningProcess(int pid, DateTimeOffset? expectedStartTime, ILogger logger)
    {
        Process? process = null;
        try
        {
            process = Process.GetProcessById(pid);
            if (process.HasExited)
            {
                process.Dispose();
                return null;
            }
 
            if (expectedStartTime is not null)
            {
                var actualStartTimeUnixMilliseconds = ProcessStartTimeHelper.TryGetProcessStartTimeUnixMilliseconds(pid);
                if (actualStartTimeUnixMilliseconds is null)
                {
                    logger.LogDebug("Could not inspect process {Pid} start time. Treating it as not running.", pid);
                    process.Dispose();
                    return null;
                }
 
                var expectedStartTimeUnixMilliseconds = expectedStartTime.Value.ToUnixTimeMilliseconds();
                if (!ProcessStartTimeHelper.AreCloseMilliseconds(expectedStartTimeUnixMilliseconds, actualStartTimeUnixMilliseconds.Value))
                {
                    logger.LogDebug("Process {Pid} start time {ProcessStartTimeMs}ms does not match expected start time {ExpectedStartTimeMs}ms", pid, actualStartTimeUnixMilliseconds, expectedStartTimeUnixMilliseconds);
                    process.Dispose();
                    return null; // Do not return processes that do not match the expected start time
                }
 
                if (process.HasExited)
                {
                    process.Dispose();
                    return null;
                }
            }
 
            return process;
        }
        catch (ArgumentException)
        {
            // Process doesn't exist - already terminated.
            process?.Dispose();
            return null;
        }
        catch (InvalidOperationException)
        {
            // Process has already exited.
            process?.Dispose();
            return null;
        }
        catch (Win32Exception ex)
        {
            // Process inspection can race with process exit. On macOS, StartTime can throw:
            //   Win32Exception (3): Unable to retrieve the specified information about the process or thread. It may have exited or may be privileged.
            // If we cannot inspect the process enough to prove it is the expected target, do
            // not signal or kill it.
            logger.LogDebug(ex, "Could not inspect process {Pid}. Treating it as not running.", pid);
            process?.Dispose();
            return null;
        }
    }
 
    public static Process? TryGetRunningProcessWithRuntimeStartTime(int pid, DateTimeOffset expectedStartTime, TimeSpan tolerance, ILogger logger)
    {
        Process? process = null;
        try
        {
            process = Process.GetProcessById(pid);
            if (process.HasExited)
            {
                process.Dispose();
                return null;
            }
 
            var expectedStartTimeUnix = expectedStartTime.ToUnixTimeSeconds();
            if (!ProcessStartTimeHelper.IsProcessRunningWithRuntimeStartTime(pid, expectedStartTimeUnix, tolerance))
            {
                logger.LogDebug("Process {Pid} legacy start time does not match expected start time {ExpectedStartTime}.", pid, expectedStartTime);
                process.Dispose();
                return null;
            }
 
            if (process.HasExited)
            {
                process.Dispose();
                return null;
            }
 
            return process;
        }
        catch (ArgumentException)
        {
            // Process doesn't exist - already terminated.
            process?.Dispose();
            return null;
        }
        catch (InvalidOperationException)
        {
            // Process has already exited.
            process?.Dispose();
            return null;
        }
        catch (Win32Exception ex)
        {
            // Process inspection can race with process exit. On macOS, StartTime can throw:
            //   Win32Exception (3): Unable to retrieve the specified information about the process or thread. It may have exited or may be privileged.
            // If we cannot inspect the process enough to prove it is the expected target, do
            // not signal or kill it.
            logger.LogDebug(ex, "Could not inspect process {Pid}. Treating it as not running.", pid);
            process?.Dispose();
            return null;
        }
    }
 
    private const int SigTerm = 15;
 
    private static void RequestGracefulShutdownUnix(int pid, ILogger logger)
    {
        var result = kill(pid, SigTerm);
        if (result != 0)
        {
            int errno = Marshal.GetLastSystemError();
            // Best effort.
            logger.LogWarning("Could not gracefully stop Aspire application host process {Pid}; the error code from signal send operation was {ErrorCode}", pid, errno);
        }
    }
 
    // "libc" here is a moniker for standard C library, which .NET maps to system C library on Unix-like systems.
    // See https://developers.redhat.com/blog/2019/03/25/using-net-pinvoke-for-linux-system-functions
    [LibraryImport("libc", SetLastError = true, EntryPoint = "kill")]
    private static partial int kill(int pid, int sig);
}