tgstation-server 6.11.3
The /tg/station 13 server suite
Loading...
Searching...
No Matches
PosixProcessFeatures.cs
Go to the documentation of this file.
1using System;
2using System.Globalization;
3using System.IO;
4using System.Text;
5using System.Threading;
6using System.Threading.Tasks;
7
8using Microsoft.Extensions.Hosting;
9using Microsoft.Extensions.Logging;
10using Mono.Unix;
11using Mono.Unix.Native;
12
16
18{
21 {
25 const short SelfOomAdjust = 1;
26
31
35 readonly Lazy<IProcessExecutor> lazyLoadedProcessExecutor;
36
41
45 readonly ILogger<PosixProcessFeatures> logger;
46
51
58 public PosixProcessFeatures(Lazy<IProcessExecutor> lazyLoadedProcessExecutor, IIOManager ioManager, ILogger<PosixProcessFeatures> logger)
59 {
60 this.lazyLoadedProcessExecutor = lazyLoadedProcessExecutor ?? throw new ArgumentNullException(nameof(lazyLoadedProcessExecutor));
61 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
62 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
63 }
64
66 public void ResumeProcess(global::System.Diagnostics.Process process)
67 {
68 var result = Syscall.kill(process.Id, Signum.SIGCONT);
69 if (result != 0)
70 throw new UnixIOException(Stdlib.GetLastError());
71 }
72
74 public void SuspendProcess(global::System.Diagnostics.Process process)
75 {
76 var result = Syscall.kill(process.Id, Signum.SIGSTOP);
77 if (result != 0)
78 throw new UnixIOException(Stdlib.GetLastError());
79 }
80
82 public string GetExecutingUsername(global::System.Diagnostics.Process process)
83 => throw new NotSupportedException();
84
86 public async ValueTask CreateDump(global::System.Diagnostics.Process process, string outputFile, bool minidump, CancellationToken cancellationToken)
87 {
88 ArgumentNullException.ThrowIfNull(process);
89 ArgumentNullException.ThrowIfNull(outputFile);
90
91 const string GCorePath = "/usr/bin/gcore";
92 if (!await ioManager.FileExists(GCorePath, cancellationToken))
93 throw new JobException(ErrorCode.MissingGCore);
94
95 int pid;
96 try
97 {
98 if (process.HasExited)
99 throw new JobException(ErrorCode.GameServerOffline);
100
101 pid = process.Id;
102 }
103 catch (InvalidOperationException ex)
104 {
105 throw new JobException(ErrorCode.GameServerOffline, ex);
106 }
107
108 string? output;
109 int exitCode;
110 await using (var gcoreProc = await lazyLoadedProcessExecutor.Value.LaunchProcess(
111 GCorePath,
112 Environment.CurrentDirectory,
113 $"{(!minidump ? "-a " : String.Empty)}-o {outputFile} {process.Id}",
114 cancellationToken,
115 readStandardHandles: true,
116 noShellExecute: true))
117 {
118 using (cancellationToken.Register(() => gcoreProc.Terminate()))
119 exitCode = (await gcoreProc.Lifetime).Value;
120
121 output = await gcoreProc.GetCombinedOutput(cancellationToken);
122 logger.LogDebug("gcore output:{newline}{output}", Environment.NewLine, output);
123 }
124
125 if (exitCode != 0)
126 throw new JobException(
127 ErrorCode.GCoreFailure,
128 new JobException(
129 $"Exit Code: {exitCode}{Environment.NewLine}Output:{Environment.NewLine}{output}"));
130
131 // gcore outputs name.pid so remove the pid part
132 var generatedGCoreFile = $"{outputFile}.{pid}";
133 await ioManager.MoveFile(generatedGCoreFile, outputFile, cancellationToken);
134 }
135
137 public async ValueTask<int> HandleProcessStart(global::System.Diagnostics.Process process, CancellationToken cancellationToken)
138 {
139 ArgumentNullException.ThrowIfNull(process);
140 var pid = process.Id;
141 try
142 {
143 // make sure all processes we spawn are killed _before_ us
144 await AdjustOutOfMemoryScore(pid, ChildProcessOomAdjust, cancellationToken);
145 }
146 catch (Exception ex) when (ex is not OperationCanceledException)
147 {
148 logger.LogWarning(ex, "Failed to adjust OOM killer score for pid {pid}!", pid);
149 }
150
151 return pid;
152 }
153
155 public async Task StartAsync(CancellationToken cancellationToken)
156 {
157 // let this all throw
158 string originalString;
159 {
160 // can't use ReadAllBytes here, /proc files have 0 length so the buffer is initialized to empty
161 // https://stackoverflow.com/questions/12237712/how-can-i-show-the-size-of-files-in-proc-it-should-not-be-size-zero
162 await using var fileStream = ioManager.CreateAsyncSequentialReadStream(
163 "/proc/self/oom_score_adj");
164 using var reader = new StreamReader(fileStream, Encoding.UTF8, leaveOpen: true);
165 originalString = await reader.ReadToEndAsync(cancellationToken);
166 }
167
168 var trimmedString = originalString.Trim();
169
170 logger.LogTrace("Original oom_score_adj is \"{original}\"", trimmedString);
171
172 var originalOomAdjust = Int16.Parse(trimmedString, CultureInfo.InvariantCulture);
173 baselineOomAdjust = Math.Clamp(originalOomAdjust, (short)-1000, (short)1000);
174
175 if (baselineOomAdjust == 1000)
176 if (originalOomAdjust != baselineOomAdjust)
177 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);
178 else
179 logger.LogWarning("oom_score_adj is at it's limit of 1000. TGS cannot guarantee the kill order of its parent/child processes!");
180
181 try
182 {
183 // we do not want to be killed before the host watchdog
184 await AdjustOutOfMemoryScore(null, SelfOomAdjust, cancellationToken);
185 }
186 catch (Exception ex) when (ex is not OperationCanceledException)
187 {
188 logger.LogWarning(ex, "Could not increase oom_score_adj!");
189 }
190 }
191
193 public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
194
202 ValueTask AdjustOutOfMemoryScore(int? pid, short adjustment, CancellationToken cancellationToken)
203 {
204 var adjustedValue = Math.Clamp(baselineOomAdjust + adjustment, -1000, 1000);
205
206 var pidStr = pid.HasValue
207 ? pid.Value.ToString(CultureInfo.InvariantCulture)
208 : "self";
209 logger.LogTrace(
210 "Setting oom_score_adj of {pid} to {adjustment}...", pidStr, adjustedValue);
212 $"/proc/{pidStr}/oom_score_adj",
213 Encoding.UTF8.GetBytes(adjustedValue.ToString(CultureInfo.InvariantCulture)),
214 cancellationToken);
215 }
216 }
217}
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 .
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