tgstation-server 6.19.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
PosixProcessFeatures.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Globalization;
4using System.IO;
5using System.Linq;
6using System.Text;
7using System.Threading;
8using System.Threading.Tasks;
9
10using Microsoft.Extensions.Hosting;
11using Microsoft.Extensions.Logging;
12using Mono.Unix;
13using Mono.Unix.Native;
14
18
20{
23 {
27 const short SelfOomAdjust = 1;
28
33
37 readonly Lazy<IProcessExecutor> lazyLoadedProcessExecutor;
38
43
47 readonly ILogger<PosixProcessFeatures> logger;
48
53
60 public PosixProcessFeatures(Lazy<IProcessExecutor> lazyLoadedProcessExecutor, IIOManager ioManager, ILogger<PosixProcessFeatures> logger)
61 {
62 this.lazyLoadedProcessExecutor = lazyLoadedProcessExecutor ?? throw new ArgumentNullException(nameof(lazyLoadedProcessExecutor));
63 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
64 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
65 }
66
68 public void ResumeProcess(global::System.Diagnostics.Process process)
69 {
70 var result = Syscall.kill(process.Id, Signum.SIGCONT);
71 if (result != 0)
72 throw new UnixIOException(Stdlib.GetLastError());
73 }
74
76 public void SuspendProcess(global::System.Diagnostics.Process process)
77 {
78 var result = Syscall.kill(process.Id, Signum.SIGSTOP);
79 if (result != 0)
80 throw new UnixIOException(Stdlib.GetLastError());
81 }
82
84 public string GetExecutingUsername(global::System.Diagnostics.Process process)
85 => throw new NotSupportedException();
86
88 public async ValueTask CreateDump(global::System.Diagnostics.Process process, string outputFile, bool minidump, CancellationToken cancellationToken)
89 {
90 ArgumentNullException.ThrowIfNull(process);
91 ArgumentNullException.ThrowIfNull(outputFile);
92
93 string? gcorePath = null;
94 foreach (var path in GetPotentialGCorePaths())
95 if (await ioManager.FileExists(path, cancellationToken))
96 {
97 gcorePath = path;
98 break;
99 }
100
101 if (gcorePath == null)
102 throw new JobException(ErrorCode.MissingGCore);
103
104 int pid;
105 try
106 {
107 if (process.HasExited)
108 throw new JobException(ErrorCode.GameServerOffline);
109
110 pid = process.Id;
111 }
112 catch (InvalidOperationException ex)
113 {
114 throw new JobException(ErrorCode.GameServerOffline, ex);
115 }
116
117 string? output;
118 int exitCode;
119 await using (var gcoreProc = await lazyLoadedProcessExecutor.Value.LaunchProcess(
120 gcorePath,
121 Environment.CurrentDirectory,
122 $"{(!minidump ? "-a " : String.Empty)}-o {outputFile} {process.Id}",
123 cancellationToken,
124 readStandardHandles: true,
125 noShellExecute: true))
126 {
127 using (cancellationToken.Register(() => gcoreProc.Terminate()))
128 exitCode = (await gcoreProc.Lifetime).Value;
129
130 output = await gcoreProc.GetCombinedOutput(cancellationToken);
131 logger.LogDebug("gcore output:{newline}{output}", Environment.NewLine, output);
132 }
133
134 if (exitCode != 0)
135 throw new JobException(
136 ErrorCode.GCoreFailure,
137 new JobException(
138 $"Exit Code: {exitCode}{Environment.NewLine}Output:{Environment.NewLine}{output}"));
139
140 // gcore outputs name.pid so remove the pid part
141 var generatedGCoreFile = $"{outputFile}.{pid}";
142 await ioManager.MoveFile(generatedGCoreFile, outputFile, cancellationToken);
143 }
144
146 public async ValueTask<int> HandleProcessStart(global::System.Diagnostics.Process process, CancellationToken cancellationToken)
147 {
148 ArgumentNullException.ThrowIfNull(process);
149 var pid = process.Id;
150 try
151 {
152 // make sure all processes we spawn are killed _before_ us
153 await AdjustOutOfMemoryScore(pid, ChildProcessOomAdjust, cancellationToken);
154 }
155 catch (Exception ex) when (ex is not OperationCanceledException)
156 {
157 logger.LogWarning(ex, "Failed to adjust OOM killer score for pid {pid}!", pid);
158 }
159
160 return pid;
161 }
162
164 public async Task StartAsync(CancellationToken cancellationToken)
165 {
166 // let this all throw
167 string originalString;
168 {
169 // can't use ReadAllBytes here, /proc files have 0 length so the buffer is initialized to empty
170 // https://stackoverflow.com/questions/12237712/how-can-i-show-the-size-of-files-in-proc-it-should-not-be-size-zero
171 await using var fileStream = ioManager.CreateAsyncReadStream(
172 "/proc/self/oom_score_adj", true, true);
173 using var reader = new StreamReader(fileStream, Encoding.UTF8, leaveOpen: true);
174 originalString = await reader.ReadToEndAsync(cancellationToken);
175 }
176
177 var trimmedString = originalString.Trim();
178
179 logger.LogTrace("Original oom_score_adj is \"{original}\"", trimmedString);
180
181 var originalOomAdjust = Int16.Parse(trimmedString, CultureInfo.InvariantCulture);
182 baselineOomAdjust = Math.Clamp(originalOomAdjust, (short)-1000, (short)1000);
183
184 if (baselineOomAdjust == 1000)
185 if (originalOomAdjust != baselineOomAdjust)
186 logger.LogWarning("oom_score_adj is at it's limit of 1000 (Clamped from {original}). TGS cannot guarantee the kill order of its parent/child processes!", originalOomAdjust);
187 else
188 logger.LogWarning("oom_score_adj is at it's limit of 1000. TGS cannot guarantee the kill order of its parent/child processes!");
189
190 try
191 {
192 // we do not want to be killed before the host watchdog
193 await AdjustOutOfMemoryScore(null, SelfOomAdjust, cancellationToken);
194 }
195 catch (Exception ex) when (ex is not OperationCanceledException)
196 {
197 logger.LogWarning(ex, "Could not increase oom_score_adj!");
198 }
199 }
200
202 public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
203
211 ValueTask AdjustOutOfMemoryScore(int? pid, short adjustment, CancellationToken cancellationToken)
212 {
213 var adjustedValue = Math.Clamp(baselineOomAdjust + adjustment, -1000, 1000);
214
215 var pidStr = pid.HasValue
216 ? pid.Value.ToString(CultureInfo.InvariantCulture)
217 : "self";
218 logger.LogTrace(
219 "Setting oom_score_adj of {pid} to {adjustment}...", pidStr, adjustedValue);
221 $"/proc/{pidStr}/oom_score_adj",
222 Encoding.UTF8.GetBytes(adjustedValue.ToString(CultureInfo.InvariantCulture)),
223 cancellationToken);
224 }
225
230 IEnumerable<string> GetPotentialGCorePaths()
231 {
232 var enviromentPath = Environment.GetEnvironmentVariable("PATH");
233 IEnumerable<string> enumerator;
234 if (enviromentPath == null)
235 enumerator = Enumerable.Empty<string>();
236 else
237 {
238 var paths = enviromentPath.Split(';');
239 enumerator = paths
240 .Select(x => x.Split(':'))
241 .SelectMany(x => x);
242 }
243
244 var exeName = "gcore";
245
246 enumerator = enumerator
247 .Concat(new List<string>(2)
248 {
249 "/usr/bin",
250 "/usr/share/bin",
251 "/bin",
252 });
253
254 enumerator = enumerator.Select(x => ioManager.ConcatPath(x, exeName));
255
256 return enumerator;
257 }
258 }
259}
Operation exceptions thrown from the context of a Models.Job.
short baselineOomAdjust
The original value of oom_score_adj as read from the /proc/ filesystem. Inherited from parent process...
void SuspendProcess(global::System.Diagnostics.Process process)
Suspend a given process .
readonly IIOManager ioManager
The IIOManager for the PosixProcessFeatures.
readonly Lazy< IProcessExecutor > lazyLoadedProcessExecutor
Lazy<T> loaded IProcessExecutor.
async ValueTask< int > HandleProcessStart(global::System.Diagnostics.Process process, CancellationToken cancellationToken)
Run events on starting a process.A ValueTask<TResult> resulting in the process ID.
IEnumerable< string > GetPotentialGCorePaths()
Gets potential paths to the gcore executable.
async ValueTask CreateDump(global::System.Diagnostics.Process process, string outputFile, bool minidump, CancellationToken cancellationToken)
Create a dump file for a given process .A ValueTask representing the running operation.
void ResumeProcess(global::System.Diagnostics.Process process)
Resume a given suspended global::System.Diagnostics.Process.
const short ChildProcessOomAdjust
Difference from baselineOomAdjust to set the oom_score_adj of child processes to. 1 higher than ourse...
PosixProcessFeatures(Lazy< IProcessExecutor > lazyLoadedProcessExecutor, IIOManager ioManager, ILogger< PosixProcessFeatures > logger)
Initializes a new instance of the PosixProcessFeatures class.
const short SelfOomAdjust
Difference from baselineOomAdjust to set our own oom_score_adj to. 1 higher host watchdog.
ValueTask AdjustOutOfMemoryScore(int? pid, short adjustment, CancellationToken cancellationToken)
Set oom_score_adj for a given pid .
async Task StartAsync(CancellationToken cancellationToken)
readonly ILogger< PosixProcessFeatures > logger
The ILogger<TCategoryName> for the PosixProcessFeatures.
string GetExecutingUsername(global::System.Diagnostics.Process process)
Get the name of the user executing a given process .The name of the user executing process .
Task StopAsync(CancellationToken cancellationToken)
Interface for using filesystems.
Definition IIOManager.cs:14
Stream CreateAsyncReadStream(string path, bool sequential, bool shareWrite)
Creates an asynchronous FileStream for sequential reading.
string ConcatPath(params string[] paths)
Combines an array of strings into a path.
Task MoveFile(string source, string destination, CancellationToken cancellationToken)
Moves a file at source to destination .
ValueTask WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken)
Writes some contents to a file at path overwriting previous content.
Task< bool > FileExists(string path, CancellationToken cancellationToken)
Check that the file at path exists.
Abstraction for suspending and resuming processes.
ErrorCode
Types of Response.ErrorMessageResponses that the API may return.
Definition ErrorCode.cs:12