tgstation-server 6.12.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
71 static IEnumerable<string> GetPotentialGCorePaths()
72 {
73 var enviromentPath = Environment.GetEnvironmentVariable("PATH");
74 IEnumerable<string> enumerator;
75 if (enviromentPath == null)
76 enumerator = Enumerable.Empty<string>();
77 else
78 {
79 var paths = enviromentPath.Split(';');
80 enumerator = paths
81 .Select(x => x.Split(':'))
82 .SelectMany(x => x);
83 }
84
85 var exeName = "gcore";
86
87 enumerator = enumerator
88 .Concat(new List<string>(2)
89 {
90 "/usr/bin",
91 "/usr/share/bin",
92 "/bin",
93 });
94
95 enumerator = enumerator.Select(x => Path.Combine(x, exeName));
96
97 return enumerator;
98 }
99
101 public void ResumeProcess(global::System.Diagnostics.Process process)
102 {
103 var result = Syscall.kill(process.Id, Signum.SIGCONT);
104 if (result != 0)
105 throw new UnixIOException(Stdlib.GetLastError());
106 }
107
109 public void SuspendProcess(global::System.Diagnostics.Process process)
110 {
111 var result = Syscall.kill(process.Id, Signum.SIGSTOP);
112 if (result != 0)
113 throw new UnixIOException(Stdlib.GetLastError());
114 }
115
117 public string GetExecutingUsername(global::System.Diagnostics.Process process)
118 => throw new NotSupportedException();
119
121 public async ValueTask CreateDump(global::System.Diagnostics.Process process, string outputFile, bool minidump, CancellationToken cancellationToken)
122 {
123 ArgumentNullException.ThrowIfNull(process);
124 ArgumentNullException.ThrowIfNull(outputFile);
125
126 string? gcorePath = null;
127 foreach (var path in GetPotentialGCorePaths())
128 if (await ioManager.FileExists(path, cancellationToken))
129 {
130 gcorePath = path;
131 break;
132 }
133
134 if (gcorePath == null)
135 throw new JobException(ErrorCode.MissingGCore);
136
137 int pid;
138 try
139 {
140 if (process.HasExited)
141 throw new JobException(ErrorCode.GameServerOffline);
142
143 pid = process.Id;
144 }
145 catch (InvalidOperationException ex)
146 {
147 throw new JobException(ErrorCode.GameServerOffline, ex);
148 }
149
150 string? output;
151 int exitCode;
152 await using (var gcoreProc = await lazyLoadedProcessExecutor.Value.LaunchProcess(
153 gcorePath,
154 Environment.CurrentDirectory,
155 $"{(!minidump ? "-a " : String.Empty)}-o {outputFile} {process.Id}",
156 cancellationToken,
157 readStandardHandles: true,
158 noShellExecute: true))
159 {
160 using (cancellationToken.Register(() => gcoreProc.Terminate()))
161 exitCode = (await gcoreProc.Lifetime).Value;
162
163 output = await gcoreProc.GetCombinedOutput(cancellationToken);
164 logger.LogDebug("gcore output:{newline}{output}", Environment.NewLine, output);
165 }
166
167 if (exitCode != 0)
168 throw new JobException(
169 ErrorCode.GCoreFailure,
170 new JobException(
171 $"Exit Code: {exitCode}{Environment.NewLine}Output:{Environment.NewLine}{output}"));
172
173 // gcore outputs name.pid so remove the pid part
174 var generatedGCoreFile = $"{outputFile}.{pid}";
175 await ioManager.MoveFile(generatedGCoreFile, outputFile, cancellationToken);
176 }
177
179 public async ValueTask<int> HandleProcessStart(global::System.Diagnostics.Process process, CancellationToken cancellationToken)
180 {
181 ArgumentNullException.ThrowIfNull(process);
182 var pid = process.Id;
183 try
184 {
185 // make sure all processes we spawn are killed _before_ us
186 await AdjustOutOfMemoryScore(pid, ChildProcessOomAdjust, cancellationToken);
187 }
188 catch (Exception ex) when (ex is not OperationCanceledException)
189 {
190 logger.LogWarning(ex, "Failed to adjust OOM killer score for pid {pid}!", pid);
191 }
192
193 return pid;
194 }
195
197 public async Task StartAsync(CancellationToken cancellationToken)
198 {
199 // let this all throw
200 string originalString;
201 {
202 // can't use ReadAllBytes here, /proc files have 0 length so the buffer is initialized to empty
203 // https://stackoverflow.com/questions/12237712/how-can-i-show-the-size-of-files-in-proc-it-should-not-be-size-zero
204 await using var fileStream = ioManager.CreateAsyncSequentialReadStream(
205 "/proc/self/oom_score_adj");
206 using var reader = new StreamReader(fileStream, Encoding.UTF8, leaveOpen: true);
207 originalString = await reader.ReadToEndAsync(cancellationToken);
208 }
209
210 var trimmedString = originalString.Trim();
211
212 logger.LogTrace("Original oom_score_adj is \"{original}\"", trimmedString);
213
214 var originalOomAdjust = Int16.Parse(trimmedString, CultureInfo.InvariantCulture);
215 baselineOomAdjust = Math.Clamp(originalOomAdjust, (short)-1000, (short)1000);
216
217 if (baselineOomAdjust == 1000)
218 if (originalOomAdjust != baselineOomAdjust)
219 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);
220 else
221 logger.LogWarning("oom_score_adj is at it's limit of 1000. TGS cannot guarantee the kill order of its parent/child processes!");
222
223 try
224 {
225 // we do not want to be killed before the host watchdog
226 await AdjustOutOfMemoryScore(null, SelfOomAdjust, cancellationToken);
227 }
228 catch (Exception ex) when (ex is not OperationCanceledException)
229 {
230 logger.LogWarning(ex, "Could not increase oom_score_adj!");
231 }
232 }
233
235 public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
236
244 ValueTask AdjustOutOfMemoryScore(int? pid, short adjustment, CancellationToken cancellationToken)
245 {
246 var adjustedValue = Math.Clamp(baselineOomAdjust + adjustment, -1000, 1000);
247
248 var pidStr = pid.HasValue
249 ? pid.Value.ToString(CultureInfo.InvariantCulture)
250 : "self";
251 logger.LogTrace(
252 "Setting oom_score_adj of {pid} to {adjustment}...", pidStr, adjustedValue);
254 $"/proc/{pidStr}/oom_score_adj",
255 Encoding.UTF8.GetBytes(adjustedValue.ToString(CultureInfo.InvariantCulture)),
256 cancellationToken);
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.
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 .
static IEnumerable< string > GetPotentialGCorePaths()
Gets potential paths to the gcore executable.
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:13
FileStream CreateAsyncSequentialReadStream(string path)
Creates an asynchronous FileStream for sequential reading.
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