tgstation-server 6.19.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
SessionPersistor.cs
Go to the documentation of this file.
1using System;
2using System.Linq;
3using System.Threading;
4using System.Threading.Tasks;
5
6using Microsoft.EntityFrameworkCore;
7using Microsoft.Extensions.Logging;
8
13
15{
18 {
23
28
33
37 readonly ILogger<SessionPersistor> logger;
38
43
56 ILogger<SessionPersistor> logger,
57 Api.Models.Instance metadata)
58 {
59 this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
60 this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
61 this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
62 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
63 this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
64 }
65
67 public ValueTask Save(ReattachInformation reattachInformation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async (db) =>
68 {
69 ArgumentNullException.ThrowIfNull(reattachInformation);
70
71 logger.LogTrace("Saving reattach information: {info}...", reattachInformation);
72
73 await ClearImpl(db, false, cancellationToken);
74
75 var dbReattachInfo = new Models.ReattachInformation(reattachInformation.AccessIdentifier)
76 {
77 CompileJobId = reattachInformation.Dmb.CompileJob.Require(x => x.Id),
78 InitialCompileJobId = reattachInformation.InitialDmb?.CompileJob.Require(x => x.Id),
79 Port = reattachInformation.Port,
80 ProcessId = reattachInformation.ProcessId,
81 RebootState = reattachInformation.RebootState,
82 LaunchSecurityLevel = reattachInformation.LaunchSecurityLevel,
83 LaunchVisibility = reattachInformation.LaunchVisibility,
84 TopicPort = reattachInformation.TopicPort,
85 };
86
87 db.ReattachInformations.Add(dbReattachInfo);
88 await db.Save(cancellationToken);
89
90 reattachInformation.Id = dbReattachInfo.Id!.Value;
91 logger.LogDebug("Saved reattach information: {info}", reattachInformation);
92 });
93
95 public ValueTask Update(ReattachInformation reattachInformation, CancellationToken cancellationToken) => databaseContextFactory.UseContextTaskReturn(async db =>
96 {
97 ArgumentNullException.ThrowIfNull(reattachInformation);
98 if (!reattachInformation.Id.HasValue)
99 throw new InvalidOperationException("Provided reattachInformation has no Id!");
100
101 logger.LogTrace("Updating reattach information: {info}...", reattachInformation);
102
103 var dbReattachInfo = new Models.ReattachInformation(String.Empty)
104 {
105 Id = reattachInformation.Id.Value,
106 };
107
108 db.ReattachInformations.Attach(dbReattachInfo);
109
110 dbReattachInfo.AccessIdentifier = reattachInformation.AccessIdentifier;
111 dbReattachInfo.CompileJobId = reattachInformation.Dmb.CompileJob.Require(x => x.Id);
112 dbReattachInfo.InitialCompileJobId = reattachInformation.InitialDmb?.CompileJob.Require(x => x.Id);
113 dbReattachInfo.Port = reattachInformation.Port;
114 dbReattachInfo.ProcessId = reattachInformation.ProcessId;
115 dbReattachInfo.RebootState = reattachInformation.RebootState;
116 dbReattachInfo.LaunchSecurityLevel = reattachInformation.LaunchSecurityLevel;
117 dbReattachInfo.LaunchVisibility = reattachInformation.LaunchVisibility;
118 dbReattachInfo.TopicPort = reattachInformation.TopicPort;
119
120 await db.Save(cancellationToken);
121
122 logger.LogDebug("Updated reattach information: {info}", reattachInformation);
123 });
124
126 public async ValueTask<ReattachInformation?> Load(CancellationToken cancellationToken)
127 {
128 Models.ReattachInformation? result = null;
129 TimeSpan? topicTimeout = null;
130
131 async ValueTask KillProcess(Models.ReattachInformation reattachInfo)
132 {
133 try
134 {
135 await using var process = processExecutor.GetProcess(reattachInfo.ProcessId);
136 if (process != null)
137 {
138 if (reattachInfo == result)
139 {
140 logger.LogWarning("Killing PID {pid} associated with CompileJob-less reattach information...", reattachInfo.ProcessId);
141 }
142 else
143 {
144 logger.LogWarning("Killing PID {pid} associated with extra reattach information...", reattachInfo.ProcessId);
145 }
146
147 process.Terminate();
148 await process.Lifetime;
149 }
150 }
151 catch (Exception ex)
152 {
153 logger.LogWarning(ex, "Failed to kill process!");
154 }
155 }
156
157 await databaseContextFactory.UseContext(async (db) =>
158 {
159 var dbReattachInfos = await db
160 .ReattachInformations
161 .Where(x => x.CompileJob!.Job.Instance!.Id == metadata.Id)
162 .Include(x => x.CompileJob)
163 .Include(x => x.InitialCompileJob)
164 .ToListAsync(cancellationToken);
165 result = dbReattachInfos.FirstOrDefault();
166 if (result == null)
167 return;
168
169 var timeoutMilliseconds = await db
170 .Instances
171 .Where(x => x.Id == metadata.Id)
172 .Select(x => x.DreamDaemonSettings!.TopicRequestTimeout)
173 .FirstOrDefaultAsync(cancellationToken);
174
175 if (timeoutMilliseconds == default)
176 {
177 logger.LogCritical("Missing TopicRequestTimeout!");
178 return;
179 }
180
181 topicTimeout = TimeSpan.FromMilliseconds(timeoutMilliseconds.Value);
182
183 bool first = true;
184 foreach (var reattachInfo in dbReattachInfos)
185 {
186 if (first)
187 {
188 first = false;
189 continue;
190 }
191
192 await KillProcess(reattachInfo);
193
194 db.ReattachInformations.Remove(reattachInfo);
195 logger.LogTrace("Deleting ReattachInformation {id}...", reattachInfo.Id);
196 }
197
198 await db.Save(cancellationToken);
199 });
200
201 if (!topicTimeout.HasValue)
202 {
203 logger.LogDebug("Reattach information not found!");
204 return null;
205 }
206
207 var dmb = await dmbFactory.FromCompileJob(result!.CompileJob!, "Session Loading Main Deployment", cancellationToken);
208 if (dmb == null)
209 {
210 logger.LogError("Unable to reattach! Could not load .dmb!");
211 await KillProcess(result);
212
213 await databaseContextFactory.UseContext(async db =>
214 {
215 logger.LogTrace("Deleting ReattachInformation {id}...", result.Id);
216 await db
217 .ReattachInformations
218 .Where(x => x.Id == result.Id)
219 .ExecuteDeleteAsync(cancellationToken);
220 });
221 return null;
222 }
223
224 IDmbProvider? initialDmb = null;
225 if (result.InitialCompileJob != null)
226 {
227 logger.LogTrace("Loading initial compile job...");
228 initialDmb = await dmbFactory.FromCompileJob(result.InitialCompileJob, "Session Loading Initial Deployment", cancellationToken);
229 }
230
231 logger.LogTrace("Retrieved ReattachInformation");
232
233 var info = new ReattachInformation(
234 result,
235 dmb,
236 initialDmb,
237 topicTimeout.Value);
238
239 logger.LogDebug("Reattach information loaded: {info}", info);
240
241 return info;
242 }
243
245 public ValueTask Clear(CancellationToken cancellationToken) => databaseContextFactory
246 .UseContext(
247 db =>
248 {
249 logger.LogDebug("Clearing reattach information");
250 return ClearImpl(db, true, cancellationToken);
251 });
252
260 async ValueTask ClearImpl(IDatabaseContext databaseContext, bool instant, CancellationToken cancellationToken)
261 {
262 var baseQuery = databaseContext
264 .Where(x => x.CompileJob!.Job.Instance!.Id == metadata.Id);
265
266 if (instant)
267 await baseQuery
268 .ExecuteDeleteAsync(cancellationToken);
269 else
270 {
271 var results = await baseQuery.ToListAsync(cancellationToken);
272 foreach (var result in results)
273 databaseContext.ReattachInformations.Remove(result);
274 }
275 }
276 }
277}
Metadata about a server instance.
Definition Instance.cs:9
Parameters necessary for duplicating a ISessionController session.
readonly ILogger< SessionPersistor > logger
The ILogger for the SessionPersistor.
readonly IProcessExecutor processExecutor
The IProcessExecutor for the SessionPersistor.
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the SessionPersistor.
readonly Api.Models.Instance metadata
The Api.Models.Instance for the SessionPersistor.
ValueTask Update(ReattachInformation reattachInformation, CancellationToken cancellationToken)
Update some reattachInformation .A ValueTask representing the running operation.
readonly IDmbFactory dmbFactory
The IDmbFactory for the SessionPersistor.
async ValueTask< ReattachInformation?> Load(CancellationToken cancellationToken)
Load a saved ReattachInformation.A ValueTask<TResult> resulting in the stored ReattachInformation if ...
SessionPersistor(IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, IProcessExecutor processExecutor, ILogger< SessionPersistor > logger, Api.Models.Instance metadata)
Initializes a new instance of the SessionPersistor class.
ValueTask Clear(CancellationToken cancellationToken)
Clear any stored ReattachInformation.A ValueTask representing the running operation.
ValueTask Save(ReattachInformation reattachInformation, CancellationToken cancellationToken)
Save some reattachInformation .A ValueTask representing the running operation.
async ValueTask ClearImpl(IDatabaseContext databaseContext, bool instant, CancellationToken cancellationToken)
Clear any stored ReattachInformation.
ValueTask< IDmbProvider?> FromCompileJob(CompileJob compileJob, string reason, CancellationToken cancellationToken, [CallerFilePath] string? callerFile=null, [CallerLineNumber] int callerLine=default)
Gets a IDmbProvider for a given CompileJob.
Provides absolute paths to the latest compiled .dmbs.
Handles saving and loading ReattachInformation.
void Remove(TModel model)
Remove a given model from the the working set.
Factory for scoping usage of IDatabaseContexts. Meant for use by Components.
ValueTask UseContextTaskReturn(Func< IDatabaseContext, Task > operation)
Run an operation in the scope of an IDatabaseContext.
ValueTask UseContext(Func< IDatabaseContext, ValueTask > operation)
Run an operation in the scope of an IDatabaseContext.
IDatabaseCollection< ReattachInformation > ReattachInformations
The DbSet<TEntity> for ReattachInformations.
IProcess? GetProcess(int id)
Get a IProcess by id .
void Terminate()
Asycnhronously terminates the process.
RebootState
Represents the action to take when /world/Reboot() is called.
Definition RebootState.cs:7