tgstation-server 6.19.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
HardLinkDmbProvider.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Diagnostics;
4using System.Globalization;
5using System.IO;
6using System.IO.Abstractions;
7using System.Runtime.Versioning;
8using System.Threading;
9using System.Threading.Tasks;
10
11using Microsoft.Extensions.Logging;
12
18
20{
24 [UnsupportedOSPlatform("windows")]
26 {
30 readonly CancellationTokenSource cancellationTokenSource;
31
35 readonly Task<string?> mirroringTask;
36
40 readonly ILogger logger;
41
52 IDmbProvider baseProvider,
53 IIOManager ioManager,
54 IFilesystemLinkFactory linkFactory,
55 ILogger logger,
56 GeneralConfiguration generalConfiguration,
57 DreamDaemonSecurity securityLevel)
58 : base(
59 baseProvider,
60 ioManager,
61 linkFactory)
62 {
63 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
64 cancellationTokenSource = new CancellationTokenSource();
65 try
66 {
67 mirroringTask = MirrorSourceDirectory(generalConfiguration.GetCopyDirectoryTaskThrottle(), securityLevel, cancellationTokenSource.Token);
68 }
69 catch
70 {
72 throw;
73 }
74 }
75
77 public override async ValueTask DisposeAsync()
78 {
81 var mirroredDir = await mirroringTask;
82 if (mirroredDir != null && !Swapped)
83 {
84 logger.LogDebug("Cancelled mirroring task, we must cleanup!");
85
86 // We shouldn't be doing long running I/O ops because this could be under an HTTP request (DELETE /api/DreamDaemon)
87 // dirty shit to follow:
88 async void AsyncCleanup()
89 {
90 try
91 {
92 await IOManager.DeleteDirectory(mirroredDir, CancellationToken.None); // DCT: None available
93 logger.LogTrace("Completed async cleanup of unused mirror directory: {mirroredDir}", mirroredDir);
94 }
95 catch (Exception ex)
96 {
97 logger.LogError(ex, "Error cleaning up mirrored directory {mirroredDir}!", mirroredDir);
98 }
99 }
100
101 AsyncCleanup();
102 }
103
104 await base.DisposeAsync();
105 }
106
108 public override Task FinishActivationPreparation(CancellationToken cancellationToken)
109 {
110 if (!mirroringTask.IsCompleted)
111 logger.LogTrace("Waiting for mirroring to complete...");
112
113 return mirroringTask.WaitAsync(cancellationToken);
114 }
115
117 protected override async ValueTask DoSwap(CancellationToken cancellationToken)
118 {
119 logger.LogTrace("Begin DoSwap, mirroring task complete: {complete}...", mirroringTask.IsCompleted);
120 var mirroredDir = await mirroringTask.WaitAsync(cancellationToken);
121 if (mirroredDir == null)
122 {
123 // huh, how?
124 cancellationToken.ThrowIfCancellationRequested();
125 throw new InvalidOperationException("mirroringTask was cancelled without us being cancelled?");
126 }
127
128 var goAheadTcs = new TaskCompletionSource();
129
130 // I feel dirty...
131 async void DisposeOfOldDirectory()
132 {
133 var directoryMoved = false;
134 var disposeGuid = Guid.NewGuid();
135 var disposePath = disposeGuid.ToString();
136 logger.LogTrace("Moving Live directory to {path} for deletion...", disposeGuid);
137 try
138 {
139 await IOManager.MoveDirectory(LiveGameDirectory, disposePath, cancellationToken);
140 directoryMoved = true;
141 goAheadTcs.SetResult();
142 logger.LogTrace("Deleting old Live directory {path}...", disposePath);
143 await IOManager.DeleteDirectory(disposePath, CancellationToken.None); // DCT: We're detached at this point
144 logger.LogTrace("Completed async cleanup of old Live directory: {disposePath}", disposePath);
145 }
146 catch (DirectoryNotFoundException ex)
147 {
148 logger.LogDebug(ex, "Live directory appears to not exist");
149 if (!directoryMoved)
150 goAheadTcs.SetResult();
151 }
152 catch (Exception ex)
153 {
154 logger.LogWarning(ex, "Failed to delete hard linked directory: {disposePath}", disposePath);
155 if (!directoryMoved)
156 goAheadTcs.SetException(ex);
157 }
158 }
159
160 DisposeOfOldDirectory();
161 await goAheadTcs.Task;
162 logger.LogTrace("Moving mirror directory {path} to Live...", mirroredDir);
163 await IOManager.MoveDirectory(mirroredDir, LiveGameDirectory, cancellationToken);
164 logger.LogTrace("Swap complete!");
165 }
166
174 async Task<string?> MirrorSourceDirectory(int? taskThrottle, DreamDaemonSecurity securityLevel, CancellationToken cancellationToken)
175 {
176 if (taskThrottle.HasValue && taskThrottle < 1)
177 throw new ArgumentOutOfRangeException(nameof(taskThrottle), taskThrottle, "taskThrottle must be at least 1!");
178
179 string? dest = null;
180 try
181 {
182 var stopwatch = Stopwatch.StartNew();
183 var mirrorGuid = Guid.NewGuid();
184
185 logger.LogDebug("Starting to mirror {sourceDir} as hard links to {mirrorGuid}...", CompileJob.DirectoryName, mirrorGuid);
186
187 var src = IOManager.ResolvePath(CompileJob.DirectoryName!.Value.ToString());
188 dest = IOManager.ResolvePath(mirrorGuid.ToString());
189
190 using var semaphore = taskThrottle.HasValue ? new SemaphoreSlim(taskThrottle.Value) : null;
191
192 var dir = await IOManager.DirectoryInfo(src, cancellationToken);
193 await Task.WhenAll(MirrorDirectoryImpl(
194 dir,
195 dest,
196 semaphore,
197 securityLevel,
198 cancellationToken));
199 stopwatch.Stop();
200
201 logger.LogDebug(
202 "Finished mirror of {sourceDir} to {mirrorGuid} in {seconds}s...",
203 CompileJob.DirectoryName,
204 mirrorGuid,
205 stopwatch.Elapsed.TotalSeconds.ToString("0.##", CultureInfo.InvariantCulture));
206 }
207 catch (OperationCanceledException ex)
208 {
209 logger.LogDebug(ex, "Cancelled while mirroring");
210 }
211 catch (Exception ex)
212 {
213 logger.LogError(ex, "Could not mirror!");
214
215 if (dest != null)
216 try
217 {
218 logger.LogDebug("Cleaning up mirror attempt: {dest}", dest);
219 await IOManager.DeleteDirectory(dest, cancellationToken);
220 }
221 catch (OperationCanceledException ex2)
222 {
223 logger.LogDebug(ex2, "Errored cleanup cancellation edge case!");
224 return dest;
225 }
226
227 return null;
228 }
229
230 return dest;
231 }
232
243 IEnumerable<Task> MirrorDirectoryImpl(IDirectoryInfo src, string dest, SemaphoreSlim? semaphore, DreamDaemonSecurity securityLevel, CancellationToken cancellationToken)
244 {
245 Task? subdirCreationTask = null;
246 var dreamDaemonWillAcceptOutOfDirectorySymlinks = securityLevel == DreamDaemonSecurity.Trusted;
247 foreach (var subDirectory in src.EnumerateDirectories())
248 {
249 var mirroredName = IOManager.ConcatPath(dest, subDirectory.Name);
250
251 // check if we are a symbolic link
252 if (subDirectory.Attributes.HasFlag(FileAttributes.ReparsePoint))
253 if (dreamDaemonWillAcceptOutOfDirectorySymlinks)
254 {
255 var target = subDirectory.ResolveLinkTarget(false)
256 ?? throw new InvalidOperationException($"\"{subDirectory.FullName}\" was incorrectly identified as a symlinked directory!");
257 logger.LogDebug("Recreating directory {name} as symlink to {target}", subDirectory.Name, target);
258 if (subdirCreationTask == null)
259 {
260 subdirCreationTask = IOManager.CreateDirectory(dest, cancellationToken);
261 yield return subdirCreationTask;
262 }
263
264 async Task CopyLink()
265 {
266 await subdirCreationTask.WaitAsync(cancellationToken);
267 using var lockContext = semaphore != null
268 ? await SemaphoreSlimContext.Lock(semaphore, cancellationToken)
269 : null;
270 await LinkFactory.CreateSymbolicLink(target.FullName, mirroredName, cancellationToken);
271 }
272
273 yield return CopyLink();
274 continue;
275 }
276 else
277 logger.LogDebug("Recreating symlinked directory {name} as hard links...", subDirectory.Name);
278
279 var checkingSubdirCreationTask = true;
280 foreach (var copyTask in MirrorDirectoryImpl(subDirectory, mirroredName, semaphore, securityLevel, cancellationToken))
281 {
282 if (subdirCreationTask == null)
283 {
284 subdirCreationTask = copyTask;
285 yield return subdirCreationTask;
286 }
287 else if (!checkingSubdirCreationTask)
288 yield return copyTask;
289
290 checkingSubdirCreationTask = false;
291 }
292 }
293
294 foreach (var fileInfo in src.EnumerateFiles())
295 {
296 if (subdirCreationTask == null)
297 {
298 subdirCreationTask = IOManager.CreateDirectory(dest, cancellationToken);
299 yield return subdirCreationTask;
300 }
301
302 var sourceFile = fileInfo.FullName;
303 var destFile = IOManager.ConcatPath(dest, fileInfo.Name);
304
305 async Task LinkThisFile()
306 {
307 await subdirCreationTask.WaitAsync(cancellationToken);
308 using var lockContext = semaphore != null
309 ? await SemaphoreSlimContext.Lock(semaphore, cancellationToken)
310 : null;
311
312 if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint))
313 {
314 // AHHHHHHHHHHHHH
315 var target = fileInfo.ResolveLinkTarget(!dreamDaemonWillAcceptOutOfDirectorySymlinks)
316 ?? throw new InvalidOperationException($"\"{fileInfo.FullName}\" was incorrectly identified as a symlinked file!");
317
318 if (dreamDaemonWillAcceptOutOfDirectorySymlinks)
319 {
320 logger.LogDebug("Recreating symlinked file {name} as symlink to {target}", fileInfo.Name, target.FullName);
321 await LinkFactory.CreateSymbolicLink(target.FullName, destFile, cancellationToken);
322 }
323 else
324 {
325 logger.LogDebug("Recreating symlinked file {name} as hard link to {target}", fileInfo.Name, target.FullName);
326 await LinkFactory.CreateHardLink(target.FullName, destFile, cancellationToken);
327 }
328 }
329 else
330 await LinkFactory.CreateHardLink(sourceFile, destFile, cancellationToken);
331 }
332
333 yield return LinkThisFile();
334 }
335 }
336 }
337}
A IDmbProvider that uses filesystem links to change directory structure underneath the server process...
bool Swapped
If MakeActive(CancellationToken) has been run.
const string LiveGameDirectory
The directory where the BaseProvider is symlinked to.
IFilesystemLinkFactory LinkFactory
The IFilesystemLinkFactory to use.
static async ValueTask< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken, ILogger? logger=null)
Asyncronously locks a semaphore .
Provides absolute paths to the latest compiled .dmbs.
Interface for using filesystems.
Definition IIOManager.cs:14
string ResolvePath()
Retrieve the full path of the current working directory.
Task< IDirectoryInfo > DirectoryInfo(string path, CancellationToken cancellationToken)
Gets a IDirectoryInfo for the given path .
string ConcatPath(params string[] paths)
Combines an array of strings into a path.
Task CreateDirectory(string path, CancellationToken cancellationToken)
Create a directory at 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 .
DreamDaemonSecurity
DreamDaemon's security level.