tgstation-server 6.12.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
ServerUpdater.cs
Go to the documentation of this file.
1using System;
2using System.Linq;
3using System.Threading;
4using System.Threading.Tasks;
5
6using Microsoft.Extensions.Logging;
7using Microsoft.Extensions.Options;
8
13
15{
18 {
23
28
33
38
42 readonly ILogger<ServerUpdater> logger;
43
48
53
57 readonly object updateInitiationLock;
58
63
79 ILogger<ServerUpdater> logger,
80 IOptions<GeneralConfiguration> generalConfigurationOptions,
81 IOptions<UpdatesConfiguration> updatesConfigurationOptions)
82 {
83 this.gitHubServiceFactory = gitHubServiceFactory ?? throw new ArgumentNullException(nameof(gitHubServiceFactory));
84 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
85 this.fileDownloader = fileDownloader ?? throw new ArgumentNullException(nameof(fileDownloader));
86 this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
87 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
88 generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
89 updatesConfiguration = updatesConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(updatesConfigurationOptions));
90
91 updateInitiationLock = new object();
92 }
93
95 public async ValueTask<ServerUpdateResult> BeginUpdate(ISwarmService swarmService, IFileStreamProvider? fileStreamProvider, Version version, CancellationToken cancellationToken)
96 {
97 ArgumentNullException.ThrowIfNull(swarmService);
98
99 ArgumentNullException.ThrowIfNull(version);
100
101 if (!swarmService.ExpectedNumberOfNodesConnected)
102 return ServerUpdateResult.SwarmIntegrityCheckFailed;
103
104 return await BeginUpdateImpl(swarmService, fileStreamProvider, version, false, cancellationToken);
105 }
106
108 public async ValueTask<bool> ExecuteUpdate(string updatePath, CancellationToken cancellationToken, CancellationToken criticalCancellationToken)
109 {
110 ArgumentNullException.ThrowIfNull(updatePath);
111
113 if (serverUpdateOperation == null)
114 throw new InvalidOperationException($"{nameof(serverUpdateOperation)} was null!");
115
116 var inMustCommitUpdate = false;
117 try
118 {
119 var stagingDirectory = $"{updatePath}-stage";
120 var tuple = await PrepareUpdateClearStagingAndBufferStream(stagingDirectory, cancellationToken);
121 if (tuple == null)
122 return false;
123
124 await using var bufferedStream = tuple.Item1;
125 var needStreamUntilCommit = tuple.Item2;
126 var createdStagingDirectory = false;
127 try
128 {
129 try
130 {
131 logger.LogTrace("Extracting zip package to {stagingDirectory}...", stagingDirectory);
132 var updateZipData = await bufferedStream.GetResult(cancellationToken);
133
134 createdStagingDirectory = true;
135 await ioManager.ZipToDirectory(stagingDirectory, updateZipData, cancellationToken);
136
137 if (!needStreamUntilCommit)
138 {
139 logger.LogTrace("Early disposing update stream provider...");
140 await bufferedStream.DisposeAsync(); // don't leave this in memory
141 }
142 }
143 catch (Exception ex)
144 {
145 await TryAbort(ex);
146 throw;
147 }
148
149 var updateCommitResult = await serverUpdateOperation.SwarmService.CommitUpdate(criticalCancellationToken);
150 if (updateCommitResult == SwarmCommitResult.AbortUpdate)
151 {
152 logger.LogError("Swarm distributed commit failed, not applying update!");
153 return false;
154 }
155
156 inMustCommitUpdate = updateCommitResult == SwarmCommitResult.MustCommitUpdate;
157 logger.LogTrace("Moving {stagingDirectory} to {updateDirectory}...", stagingDirectory, updatePath);
158 await ioManager.MoveDirectory(stagingDirectory, updatePath, criticalCancellationToken);
159 }
160 catch (Exception e) when (createdStagingDirectory)
161 {
162 try
163 {
164 // important to not leave this directory around if possible
165 await ioManager.DeleteDirectory(stagingDirectory, default);
166 }
167 catch (Exception e2)
168 {
169 throw new AggregateException(e, e2);
170 }
171
172 throw;
173 }
174
175 return true;
176 }
177 catch (OperationCanceledException) when (!inMustCommitUpdate)
178 {
179 logger.LogInformation("Server update cancelled!");
180 }
181 catch (Exception ex) when (!inMustCommitUpdate)
182 {
183 logger.LogError(ex, "Error updating server!");
184 }
185 catch (Exception ex) when (inMustCommitUpdate)
186 {
187 logger.LogCritical(ex, "Could not complete committed swarm update!");
188 }
189
190 return false;
191 }
192
200 async ValueTask TryAbort(Exception exception)
201 {
202 try
203 {
205 }
206 catch (Exception e2)
207 {
208 throw new AggregateException(exception, e2);
209 }
210 }
211
219 async ValueTask<Tuple<BufferedFileStreamProvider, bool>?> PrepareUpdateClearStagingAndBufferStream(string stagingDirectory, CancellationToken cancellationToken)
220 {
221 await using var fileStreamProvider = serverUpdateOperation!.FileStreamProvider;
222
223 var bufferedStream = new BufferedFileStreamProvider(
224 await fileStreamProvider.GetResult(cancellationToken));
225 try
226 {
227 var updatePrepareResult = await serverUpdateOperation.SwarmService.PrepareUpdate(
228 bufferedStream,
230 cancellationToken);
231 if (updatePrepareResult == SwarmPrepareResult.Failure)
232 {
233 await bufferedStream.DisposeAsync();
234 return null;
235 }
236
237 try
238 {
239 // simply buffer the result at this point
240 var bufferingTask = bufferedStream.EnsureBuffered(cancellationToken);
241
242 // clear out the staging directory first
243 await ioManager.DeleteDirectory(stagingDirectory, cancellationToken);
244
245 // Dispose warning avoidance
246 var result = Tuple.Create(
247 bufferedStream,
248 updatePrepareResult == SwarmPrepareResult.SuccessHoldProviderUntilCommit);
249
250 await bufferingTask;
251 bufferedStream = null;
252
253 return result;
254 }
255 catch (Exception ex)
256 {
257 await TryAbort(ex);
258 throw;
259 }
260 }
261 finally
262 {
263 if (bufferedStream != null)
264 await bufferedStream.DisposeAsync();
265 }
266 }
267
277 async ValueTask<ServerUpdateResult> BeginUpdateImpl(
278 ISwarmService swarmService,
279 IFileStreamProvider? fileStreamProvider,
280 Version newVersion,
281 bool recursed,
282 CancellationToken cancellationToken)
283 {
284 ServerUpdateOperation? ourUpdateOperation = null;
285 try
286 {
287 if (fileStreamProvider == null)
288 {
289 logger.LogDebug("Looking for GitHub releases version {version}...", newVersion);
290
291 var gitHubService = await gitHubServiceFactory.CreateService(cancellationToken);
292 var releases = await gitHubService.GetTgsReleases(cancellationToken);
293 foreach (var kvp in releases)
294 {
295 var version = kvp.Key;
296 var release = kvp.Value;
297 if (version == newVersion)
298 {
299 var asset = release.Assets.Where(x => x.Name.Equals(updatesConfiguration.UpdatePackageAssetName, StringComparison.Ordinal)).FirstOrDefault();
300 if (asset == default)
301 continue;
302
303 logger.LogTrace("Creating download provider for {assetName}...", updatesConfiguration.UpdatePackageAssetName);
304 var bearerToken = generalConfiguration.GitHubAccessToken;
305 if (String.IsNullOrWhiteSpace(bearerToken))
306 bearerToken = null;
307
308 fileStreamProvider = fileDownloader.DownloadFile(new Uri(asset.BrowserDownloadUrl), bearerToken);
309 break;
310 }
311 }
312
313 if (fileStreamProvider == null)
314 {
315 if (!recursed)
316 {
317 logger.LogWarning("We didn't find the requested release, but GitHub has been known to just not give full results when querying all releases. We'll try one more time.");
318 return await BeginUpdateImpl(swarmService, null, newVersion, true, cancellationToken);
319 }
320
321 return ServerUpdateResult.ReleaseMissing;
322 }
323 }
324
326 {
327 if (serverUpdateOperation == null)
328 {
329 ourUpdateOperation = new ServerUpdateOperation(
330 fileStreamProvider,
331 swarmService,
332 newVersion);
333
334 serverUpdateOperation = ourUpdateOperation;
335
336 bool updateStarted = serverControl.TryStartUpdate(this, newVersion);
337 if (updateStarted)
338 {
339 fileStreamProvider = null; // belongs to serverUpdateOperation now
340 return ServerUpdateResult.Started;
341 }
342 }
343
344 return ServerUpdateResult.UpdateInProgress;
345 }
346 }
347 catch
348 {
350 if (serverUpdateOperation == ourUpdateOperation)
352
353 throw;
354 }
355 finally
356 {
357 if (fileStreamProvider != null)
358 await fileStreamProvider.DisposeAsync();
359 }
360 }
361 }
362}
string? GitHubAccessToken
A classic GitHub personal access token to use for bypassing rate limits on requests....
Configuration for the automatic update system.
string? UpdatePackageAssetName
Asset package containing the new Host assembly in zip form.
Necessary data for performing a server update.
IFileStreamProvider FileStreamProvider
The IFileStreamProvider that contains the update zip file.
ISwarmService SwarmService
The ISwarmService for the operation.
Version TargetVersion
The Version being updated to.
async ValueTask< bool > ExecuteUpdate(string updatePath, CancellationToken cancellationToken, CancellationToken criticalCancellationToken)
Executes a pending server update by extracting the new server to a given updatePath ....
ServerUpdater(IGitHubServiceFactory gitHubServiceFactory, IIOManager ioManager, IFileDownloader fileDownloader, IServerControl serverControl, ILogger< ServerUpdater > logger, IOptions< GeneralConfiguration > generalConfigurationOptions, IOptions< UpdatesConfiguration > updatesConfigurationOptions)
Initializes a new instance of the ServerUpdater class.
async ValueTask< Tuple< BufferedFileStreamProvider, bool >?> PrepareUpdateClearStagingAndBufferStream(string stagingDirectory, CancellationToken cancellationToken)
Prepares the swarm update, deletes the stagingDirectory , and buffers the ServerUpdateOperation....
readonly IIOManager ioManager
The IIOManager for the ServerUpdater.
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the ServerUpdater.
readonly IFileDownloader fileDownloader
The IFileDownloader for the ServerUpdater.
ServerUpdateOperation? serverUpdateOperation
ServerUpdateOperation for an in-progress update operation.
readonly ILogger< ServerUpdater > logger
The ILogger for the ServerUpdater.
readonly object updateInitiationLock
Lock object used when initiating an update.
readonly IGitHubServiceFactory gitHubServiceFactory
The IGitHubServiceFactory for the ServerUpdater.
async ValueTask TryAbort(Exception exception)
Attempt to abort a prepared swarm update.
async ValueTask< ServerUpdateResult > BeginUpdateImpl(ISwarmService swarmService, IFileStreamProvider? fileStreamProvider, Version newVersion, bool recursed, CancellationToken cancellationToken)
Start the process of downloading and applying an update to a new server version. Doesn't perform argu...
async ValueTask< ServerUpdateResult > BeginUpdate(ISwarmService swarmService, IFileStreamProvider? fileStreamProvider, Version version, CancellationToken cancellationToken)
Start the process of downloading and applying an update to a new server version .A ValueTask<TResult>...
readonly IServerControl serverControl
The IServerControl for the ServerUpdater.
readonly UpdatesConfiguration updatesConfiguration
The UpdatesConfiguration for the ServerUpdater.
IFileStreamProvider that provides a ISeekableFileStreamProvider from an input Stream.
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
bool TryStartUpdate(IServerUpdateExecutor updateExecutor, Version newVersion)
Attempt to update with a given updateExecutor .
IFileStreamProvider DownloadFile(Uri url, string? bearerToken)
Downloads a file from a given url .
Interface for asynchronously consuming Streams of files.
Interface for using filesystems.
Definition IIOManager.cs:13
Task ZipToDirectory(string path, Stream zipFile, CancellationToken cancellationToken)
Extract a set of zipFile to a given path .
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory, removes and does not enter any symlinks encounterd.
Task MoveDirectory(string source, string destination, CancellationToken cancellationToken)
Moves a directory at source to destination .
Used for swarm operations. Functions may be no-op based on configuration.
ValueTask< SwarmPrepareResult > PrepareUpdate(ISeekableFileStreamProvider fileStreamProvider, Version version, CancellationToken cancellationToken)
Signal to the swarm that an update is requested.
bool ExpectedNumberOfNodesConnected
Gets a value indicating if the expected amount of nodes are connected to the swarm.
ValueTask< SwarmCommitResult > CommitUpdate(CancellationToken cancellationToken)
Signal to the swarm that an update is ready to be applied.
ValueTask AbortUpdate()
Attempt to abort an uncommitted update.
ValueTask< IGitHubService > CreateService(CancellationToken cancellationToken)
Create a IGitHubService.
ServerUpdateResult
The result of a call to start a server update.
SwarmCommitResult
How to proceed on the commit step of an update.
SwarmPrepareResult
Indicates the result of a swarm update prepare operation.