tgstation-server 5.12.7
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.gitHubService = gitHubService ?? throw new ArgumentNullException(nameof(gitHubService));
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 Task<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 Task<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 Task TryAbort(Exception exception)
201 {
202 try
203 {
205 }
206 catch (Exception e2)
207 {
208 throw new AggregateException(exception, e2);
209 }
210 }
211
219 async Task<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 Task<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 releases = await gitHubService.GetTgsReleases(cancellationToken);
292 foreach (var kvp in releases)
293 {
294 var version = kvp.Key;
295 var release = kvp.Value;
296 if (version == newVersion)
297 {
298 var asset = release.Assets.Where(x => x.Name.Equals(updatesConfiguration.UpdatePackageAssetName, StringComparison.Ordinal)).FirstOrDefault();
299 if (asset == default)
300 continue;
301
302 logger.LogTrace("Creating download provider for {assetName}...", updatesConfiguration.UpdatePackageAssetName);
303 var bearerToken = generalConfiguration.GitHubAccessToken;
304 if (String.IsNullOrWhiteSpace(bearerToken))
305 bearerToken = null;
306
307 fileStreamProvider = fileDownloader.DownloadFile(new Uri(asset.BrowserDownloadUrl), bearerToken);
308 break;
309 }
310 }
311
312 if (fileStreamProvider == null)
313 {
314 if (!recursed)
315 {
316 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.");
317 return await BeginUpdateImpl(swarmService, null, newVersion, true, cancellationToken);
318 }
319
320 return ServerUpdateResult.ReleaseMissing;
321 }
322 }
323
325 {
326 if (serverUpdateOperation == null)
327 {
328 ourUpdateOperation = new ServerUpdateOperation(
329 fileStreamProvider,
330 swarmService,
331 newVersion);
332
333 serverUpdateOperation = ourUpdateOperation;
334
335 bool updateStarted = serverControl.TryStartUpdate(this, newVersion);
336 if (updateStarted)
337 {
338 fileStreamProvider = null; // belongs to serverUpdateOperation now
339 return ServerUpdateResult.Started;
340 }
341 }
342
343 return ServerUpdateResult.UpdateInProgress;
344 }
345 }
346 catch
347 {
349 if (serverUpdateOperation == ourUpdateOperation)
351
352 throw;
353 }
354 finally
355 {
356 if (fileStreamProvider != null)
357 await fileStreamProvider.DisposeAsync();
358 }
359 }
360 }
361}
string GitHubAccessToken
A GitHub personal access token to use for bypassing rate limits on requests. Requires no scopes.
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 Task< 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 Task< bool > ExecuteUpdate(string updatePath, CancellationToken cancellationToken, CancellationToken criticalCancellationToken)
Executes a pending server update by extracting the new server to a given updatePath ....
readonly IIOManager ioManager
The IIOManager for the ServerUpdater.
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the ServerUpdater.
readonly IFileDownloader fileDownloader
The IFileDownloader for the ServerUpdater.
readonly ILogger< ServerUpdater > logger
The ILogger for the ServerUpdater.
readonly object updateInitiationLock
Lock object used when initiating an update.
async Task< Tuple< BufferedFileStreamProvider, bool > > PrepareUpdateClearStagingAndBufferStream(string stagingDirectory, CancellationToken cancellationToken)
Prepares the swarm update, deletes the stagingDirectory , and buffers the ServerUpdateOperation....
ServerUpdateOperation serverUpdateOperation
ServerUpdateOperation for an in-progress update operation.
async Task< 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 Task<TResult> res...
async Task TryAbort(Exception exception)
Attempt to abort a prepared swarm update.
ServerUpdater(IGitHubService gitHubService, IIOManager ioManager, IFileDownloader fileDownloader, IServerControl serverControl, ILogger< ServerUpdater > logger, IOptions< GeneralConfiguration > generalConfigurationOptions, IOptions< UpdatesConfiguration > updatesConfigurationOptions)
Initializes a new instance of the ServerUpdater class.
readonly IGitHubService gitHubService
The IGitHubService for the ServerUpdater.
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.
Task< SwarmCommitResult > CommitUpdate(CancellationToken cancellationToken)
Signal to the swarm that an update is ready to be applied.
Task< 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.
Task AbortUpdate()
Attempt to abort an uncommitted update.
Service for interacting with the GitHub API.
Task< Dictionary< Version, Release > > GetTgsReleases(CancellationToken cancellationToken)
Get all valid TGS Releases from the configured update source.
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.