tgstation-server 6.12.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 .AsQueryable()
162 .Where(x => x.CompileJob!.Job.Instance!.Id == metadata.Id)
163 .Include(x => x.CompileJob)
164 .Include(x => x.InitialCompileJob)
165 .ToListAsync(cancellationToken);
166 result = dbReattachInfos.FirstOrDefault();
167 if (result == null)
168 return;
169
170 var timeoutMilliseconds = await db
171 .Instances
172 .AsQueryable()
173 .Where(x => x.Id == metadata.Id)
174 .Select(x => x.DreamDaemonSettings!.TopicRequestTimeout)
175 .FirstOrDefaultAsync(cancellationToken);
176
177 if (timeoutMilliseconds == default)
178 {
179 logger.LogCritical("Missing TopicRequestTimeout!");
180 return;
181 }
182
183 topicTimeout = TimeSpan.FromMilliseconds(timeoutMilliseconds.Value);
184
185 bool first = true;
186 foreach (var reattachInfo in dbReattachInfos)
187 {
188 if (first)
189 {
190 first = false;
191 continue;
192 }
193
194 await KillProcess(reattachInfo);
195
196 db.ReattachInformations.Remove(reattachInfo);
197 logger.LogTrace("Deleting ReattachInformation {id}...", reattachInfo.Id);
198 }
199
200 await db.Save(cancellationToken);
201 });
202
203 if (!topicTimeout.HasValue)
204 {
205 logger.LogDebug("Reattach information not found!");
206 return null;
207 }
208
209 var dmb = await dmbFactory.FromCompileJob(result!.CompileJob!, "Session Loading Main Deployment", cancellationToken);
210 if (dmb == null)
211 {
212 logger.LogError("Unable to reattach! Could not load .dmb!");
213 await KillProcess(result);
214
215 await databaseContextFactory.UseContext(async db =>
216 {
217 logger.LogTrace("Deleting ReattachInformation {id}...", result.Id);
218 await db
219 .ReattachInformations
220 .AsQueryable()
221 .Where(x => x.Id == result.Id)
222 .ExecuteDeleteAsync(cancellationToken);
223 });
224 return null;
225 }
226
227 IDmbProvider? initialDmb = null;
228 if (result.InitialCompileJob != null)
229 {
230 logger.LogTrace("Loading initial compile job...");
231 initialDmb = await dmbFactory.FromCompileJob(result.InitialCompileJob, "Session Loading Initial Deployment", cancellationToken);
232 }
233
234 logger.LogTrace("Retrieved ReattachInformation");
235
236 var info = new ReattachInformation(
237 result,
238 dmb,
239 initialDmb,
240 topicTimeout.Value);
241
242 logger.LogDebug("Reattach information loaded: {info}", info);
243
244 return info;
245 }
246
248 public ValueTask Clear(CancellationToken cancellationToken) => databaseContextFactory
249 .UseContext(
250 db =>
251 {
252 logger.LogDebug("Clearing reattach information");
253 return ClearImpl(db, true, cancellationToken);
254 });
255
263 async ValueTask ClearImpl(IDatabaseContext databaseContext, bool instant, CancellationToken cancellationToken)
264 {
265 var baseQuery = databaseContext
267 .AsQueryable()
268 .Where(x => x.CompileJob!.Job.Instance!.Id == metadata.Id);
269
270 if (instant)
271 await baseQuery
272 .ExecuteDeleteAsync(cancellationToken);
273 else
274 {
275 var results = await baseQuery.ToListAsync(cancellationToken);
276 foreach (var result in results)
277 databaseContext.ReattachInformations.Remove(result);
278 }
279 }
280 }
281}
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