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