-
Notifications
You must be signed in to change notification settings - Fork 895
Expand file tree
/
Copy pathProcessSignaler.cs
More file actions
134 lines (120 loc) · 4.86 KB
/
ProcessSignaler.cs
File metadata and controls
134 lines (120 loc) · 4.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// 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
}
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 processStartTime = process.StartTime;
if (!AreClose(expectedStartTime, processStartTime))
{
logger.LogDebug("Process {Pid} start time {ProcessStartTime} does not match expected start time {ExpectedStartTime}", pid, processStartTime, expectedStartTime);
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;
}
}
private static bool AreClose(DateTimeOffset? expectedStartTime, DateTime processStartTime, TimeSpan? tolerance = default)
{
if (expectedStartTime is null)
{
return true;
}
tolerance ??= TimeSpan.FromSeconds(1);
return ((DateTimeOffset)expectedStartTime - new DateTimeOffset(processStartTime)).Duration() <= tolerance;
}
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);
}