Skip to content

Commit e2b8ef9

Browse files
committed
gvfs dehydrate --folders without unmounting
1 parent f877d10 commit e2b8ef9

File tree

3 files changed

+172
-45
lines changed

3 files changed

+172
-45
lines changed

GVFS/GVFS.Platform.Windows/WindowsFileSystemVirtualizer.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,14 @@ public override FileSystemResult UpdatePlaceholderIfNeeded(
201201

202202
public override FileSystemResult DehydrateFolder(string relativePath)
203203
{
204-
// Don't need to do anything here because the parent will reproject the folder.
205-
return new FileSystemResult(FSResult.Ok, 0);
204+
// The folder should have already been deleted, but
205+
// its tombstone also needs to be deleted to allow reprojection.
206+
var result = this.virtualizationInstance.DeleteFile(
207+
relativePath,
208+
UpdateType.AllowTombstone,
209+
out UpdateFailureCause failureCause);
210+
211+
return new FileSystemResult(HResultToFSResult(result), unchecked((int)result));
206212
}
207213

208214
// TODO: Need ProjFS 13150199 to be fixed so that GVFS doesn't leak memory if the enumeration cancelled.

GVFS/GVFS.Virtualization/FileSystemCallbacks.cs

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public class FileSystemCallbacks : IDisposable, IHeartBeatMetadataProvider
3030
GitCommandLineParser.Verbs.UpdateIndex;
3131

3232
private readonly string logsHeadPath;
33+
private readonly ConcurrentHashSet<string> dehydratingFolders = new ConcurrentHashSet<string>();
3334

3435
private GVFSContext context;
3536
private IPlaceholderCollection placeholderDatabase;
@@ -305,6 +306,10 @@ public bool TryDehydrateFolder(string relativePath, out string errorMessage)
305306

306307
try
307308
{
309+
this.dehydratingFolders.Add(relativePath);
310+
311+
var absolutePath = Path.Combine(this.context.Enlistment.WorkingDirectoryBackingRoot, relativePath);
312+
this.context.FileSystem.DeleteDirectory(absolutePath, recursive: true);
308313
relativePath = GVFSDatabase.NormalizePath(relativePath);
309314
removedPlaceholders = this.placeholderDatabase.RemoveAllEntriesForFolder(relativePath);
310315
removedModifiedPaths = this.modifiedPaths.RemoveAllEntriesForFolder(relativePath);
@@ -321,6 +326,10 @@ public bool TryDehydrateFolder(string relativePath, out string errorMessage)
321326
EventMetadata metadata = this.CreateEventMetadata(relativePath, ex);
322327
this.context.Tracer.RelatedError(metadata, errorMessage);
323328
}
329+
finally
330+
{
331+
this.dehydratingFolders.TryRemove(relativePath);
332+
}
324333

325334
if (!string.IsNullOrEmpty(errorMessage))
326335
{
@@ -466,12 +475,18 @@ public virtual void OnFileSymLinkCreated(string newLinkRelativePath)
466475

467476
public void OnFileDeleted(string relativePath)
468477
{
469-
this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFileDeleted(relativePath));
478+
if (!this.IsDehydrating(relativePath))
479+
{
480+
this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFileDeleted(relativePath));
481+
}
470482
}
471483

472484
public void OnFilePreDelete(string relativePath)
473485
{
474-
this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFilePreDelete(relativePath));
486+
if (!this.IsDehydrating(relativePath))
487+
{
488+
this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFilePreDelete(relativePath));
489+
}
475490
}
476491

477492
/// <summary>
@@ -506,17 +521,26 @@ public virtual void OnFolderRenamed(string oldRelativePath, string newRelativePa
506521

507522
public void OnFolderDeleted(string relativePath)
508523
{
509-
this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFolderDeleted(relativePath));
524+
if (!this.IsDehydrating(relativePath))
525+
{
526+
this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFolderDeleted(relativePath));
527+
}
510528
}
511529

512530
public void OnPossibleTombstoneFolderCreated(string relativePath)
513531
{
514-
this.GitIndexProjection.OnPossibleTombstoneFolderCreated(relativePath);
532+
if (!this.IsDehydrating(relativePath))
533+
{
534+
this.GitIndexProjection.OnPossibleTombstoneFolderCreated(relativePath);
535+
}
515536
}
516537

517538
public void OnFolderPreDelete(string relativePath)
518539
{
519-
this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFolderPreDelete(relativePath));
540+
if (!this.IsDehydrating(relativePath))
541+
{
542+
this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFolderPreDelete(relativePath));
543+
}
520544
}
521545

522546
public void OnPlaceholderFileCreated(string relativePath, string sha, string triggeringProcessImageFileName)
@@ -1025,6 +1049,12 @@ private EventMetadata CreateEventMetadata(
10251049
return metadata;
10261050
}
10271051

1052+
private bool IsDehydrating(string relativePath)
1053+
{
1054+
return this.dehydratingFolders.Any(f =>
1055+
relativePath.StartsWith(f + GVFSConstants.GitPathSeparatorString, GVFSPlatform.Instance.Constants.PathComparison));
1056+
}
1057+
10281058
private class PlaceHolderCreateCounter
10291059
{
10301060
private long count;

GVFS/GVFS/CommandLine/DehydrateVerb.cs

Lines changed: 129 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -181,16 +181,16 @@ from a parent of the folders list.
181181

182182
this.Output.WriteLine();
183183

184-
this.Unmount(tracer);
185-
186-
string error;
187-
if (!DiskLayoutUpgrade.TryCheckDiskLayoutVersion(tracer, enlistment.EnlistmentRoot, out error))
188-
{
189-
this.ReportErrorAndExit(tracer, error);
190-
}
191-
192184
if (fullDehydrate)
193185
{
186+
this.Unmount(tracer);
187+
188+
string error;
189+
if (!DiskLayoutUpgrade.TryCheckDiskLayoutVersion(tracer, enlistment.EnlistmentRoot, out error))
190+
{
191+
this.ReportErrorAndExit(tracer, error);
192+
}
193+
194194
RetryConfig retryConfig;
195195
if (!RetryConfig.TryLoadFromGitConfig(tracer, enlistment, out retryConfig, out error))
196196
{
@@ -216,7 +216,7 @@ from a parent of the folders list.
216216
{
217217
if (cleanStatus)
218218
{
219-
this.DehydrateFolders(tracer, enlistment, folders);
219+
this.DehydrateFolders(tracer, enlistment, folders, backupRoot);
220220
}
221221
else
222222
{
@@ -231,8 +231,15 @@ from a parent of the folders list.
231231
}
232232
}
233233

234-
private void DehydrateFolders(JsonTracer tracer, GVFSEnlistment enlistment, string[] folders)
234+
private void DehydrateFolders(JsonTracer tracer, GVFSEnlistment enlistment, string[] folders, string backupRoot)
235235
{
236+
if (!this.TryBackupNonSrcFiles(tracer, enlistment, backupRoot))
237+
{
238+
this.Output.WriteLine();
239+
this.WriteMessage(tracer, "ERROR: Backup failed. ");
240+
return;
241+
}
242+
236243
List<string> foldersToDehydrate = new List<string>();
237244
List<string> folderErrors = new List<string>();
238245

@@ -241,7 +248,7 @@ private void DehydrateFolders(JsonTracer tracer, GVFSEnlistment enlistment, stri
241248
{
242249
if (!ModifiedPathsDatabase.TryLoadOrCreate(
243250
tracer,
244-
Path.Combine(enlistment.DotGVFSRoot, GVFSConstants.DotGVFS.Databases.ModifiedPaths),
251+
Path.Combine(GetBackupDatabasesPath(backupRoot), GVFSConstants.DotGVFS.Databases.ModifiedPaths),
245252
this.fileSystem,
246253
out ModifiedPathsDatabase modifiedPaths,
247254
out string error))
@@ -271,26 +278,13 @@ private void DehydrateFolders(JsonTracer tracer, GVFSEnlistment enlistment, stri
271278
else
272279
{
273280
string fullPath = Path.Combine(enlistment.WorkingDirectoryBackingRoot, folder);
274-
if (this.fileSystem.DirectoryExists(fullPath))
281+
if (!this.fileSystem.DirectoryExists(fullPath))
275282
{
276-
// Since directories are deleted last and will be empty at that point we can skip errors
277-
// while trying to delete it and leave the empty directory and continue to dehydrate
278-
if (!this.TryIO(tracer, () => this.fileSystem.DeleteDirectory(fullPath, ignoreDirectoryDeleteExceptions: true), $"Deleting '{fullPath}'", out ioError))
279-
{
280-
this.WriteMessage(tracer, $"Cannot {this.ActionName} folder '{folder}': removing '{folder}' failed.");
281-
this.WriteMessage(tracer, "Ensure no applications are accessing the folder and retry.");
282-
this.WriteMessage(tracer, $"More details: {ioError}");
283-
folderErrors.Add($"{folder}\0{ioError}");
284-
}
285-
else
286-
{
287-
foldersToDehydrate.Add(folder);
288-
}
283+
this.WriteMessage(tracer, $"Cannot {this.ActionName} folder '{folder}': '{folder}' does not exist.");
284+
foldersToDehydrate.Add(folder);
289285
}
290286
else
291287
{
292-
this.WriteMessage(tracer, $"Cannot {this.ActionName} folder '{folder}': '{folder}' does not exist.");
293-
294288
// Still add to foldersToDehydrate so that any placeholders or modified paths get cleaned up
295289
foldersToDehydrate.Add(folder);
296290
}
@@ -306,15 +300,9 @@ private void DehydrateFolders(JsonTracer tracer, GVFSEnlistment enlistment, stri
306300
this.ReportErrorAndExit(tracer, $"{this.ActionName} for folders failed.");
307301
}
308302

309-
// We can skip the version check because dehydrating folders requires that a git status
310-
// be run first, and running git status requires that the repo already be mounted (meaning
311-
// we don't need to perform another version check again)
312-
this.Mount(
313-
tracer,
314-
skipVersionCheck: true);
315-
316303
if (foldersToDehydrate.Count > 0)
317304
{
305+
string backupSrc = GetBackupSrcPath(backupRoot);
318306
this.SendDehydrateMessage(tracer, enlistment, folderErrors, foldersToDehydrate);
319307
}
320308

@@ -329,6 +317,11 @@ private void DehydrateFolders(JsonTracer tracer, GVFSEnlistment enlistment, stri
329317
}
330318
}
331319

320+
private static string GetBackupSrcPath(string backupRoot)
321+
{
322+
return Path.Combine(backupRoot, "src");
323+
}
324+
332325
private bool IsFolderValid(string folderPath)
333326
{
334327
if (folderPath == GVFSConstants.DotGit.Root ||
@@ -353,10 +346,15 @@ private void SendDehydrateMessage(ITracer tracer, GVFSEnlistment enlistment, Lis
353346
{
354347
if (!pipeClient.Connect())
355348
{
356-
this.ReportErrorAndExit("Unable to connect to GVFS. Try running 'gvfs mount'");
349+
this.Output.WriteLine("Mounting...");
350+
this.Mount(tracer, skipVersionCheck: false);
351+
if (!pipeClient.Connect())
352+
{
353+
this.ReportErrorAndExit("Unable to connect to GVFS. Try running 'gvfs mount'");
354+
}
357355
}
358356

359-
NamedPipeMessages.DehydrateFolders.Request request = new NamedPipeMessages.DehydrateFolders.Request(string.Join(FolderListSeparator, folders));
357+
NamedPipeMessages.DehydrateFolders.Request request = new NamedPipeMessages.DehydrateFolders.Request(string.Join(";", folders));
360358
pipeClient.SendRequest(request.CreateMessage());
361359
response = NamedPipeMessages.DehydrateFolders.Response.FromMessage(NamedPipeMessages.Message.FromString(pipeClient.ReadRawResponse()));
362360
}
@@ -535,12 +533,83 @@ private void PrepareSrcFolder(ITracer tracer, GVFSEnlistment enlistment)
535533
}
536534
}
537535

536+
private bool TryBackupNonSrcFiles(ITracer tracer, GVFSEnlistment enlistment, string backupRoot)
537+
{
538+
string backupSrc = GetBackupSrcPath(backupRoot);
539+
string backupGit = Path.Combine(backupRoot, ".git");
540+
string backupGvfs = Path.Combine(backupRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot);
541+
string backupDatabases = GetBackupDatabasesPath(backupGvfs);
542+
543+
string errorMessage = string.Empty;
544+
if (!this.ShowStatusWhileRunning(
545+
() =>
546+
{
547+
string ioError;
548+
if (!this.TryIO(tracer, () => Directory.CreateDirectory(backupRoot), "Create backup directory", out ioError) ||
549+
!this.TryIO(tracer, () => Directory.CreateDirectory(backupGit), "Create backup .git directory", out ioError) ||
550+
!this.TryIO(tracer, () => Directory.CreateDirectory(backupGvfs), "Create backup .gvfs directory", out ioError) ||
551+
!this.TryIO(tracer, () => Directory.CreateDirectory(backupDatabases), "Create backup .gvfs databases directory", out ioError))
552+
{
553+
errorMessage = "Failed to create backup folders at " + backupRoot + ": " + ioError;
554+
return false;
555+
}
556+
557+
// ... backup the .gvfs hydration-related data structures...
558+
string databasesFolder = Path.Combine(enlistment.DotGVFSRoot, GVFSConstants.DotGVFS.Databases.Name);
559+
if (!this.TryCopyFilesInFolder(tracer, databasesFolder, backupDatabases, searchPattern: "*", filenamesToSkip: "RepoMetadata.dat"))
560+
{
561+
return false;
562+
}
563+
564+
// ... backup everything related to the .git\index...
565+
if (!this.TryIO(
566+
tracer,
567+
() => File.Copy(
568+
Path.Combine(enlistment.DotGitRoot, GVFSConstants.DotGit.IndexName),
569+
Path.Combine(backupGit, GVFSConstants.DotGit.IndexName)),
570+
"Backup the git index",
571+
out errorMessage) ||
572+
!this.TryIO(
573+
tracer,
574+
() => File.Copy(
575+
Path.Combine(enlistment.DotGVFSRoot, GitIndexProjection.ProjectionIndexBackupName),
576+
Path.Combine(backupGvfs, GitIndexProjection.ProjectionIndexBackupName)),
577+
"Backup GVFS_projection",
578+
out errorMessage))
579+
{
580+
return false;
581+
}
582+
583+
// ... backup all .git\*.lock files
584+
if (!this.TryCopyFilesInFolder(tracer, enlistment.DotGitRoot, backupGit, searchPattern: "*.lock"))
585+
{
586+
return false;
587+
}
588+
589+
return true;
590+
},
591+
"Backing up your files"))
592+
{
593+
this.Output.WriteLine();
594+
this.WriteMessage(tracer, "ERROR: " + errorMessage);
595+
596+
return false;
597+
}
598+
599+
return true;
600+
}
601+
602+
private static string GetBackupDatabasesPath(string backupGvfs)
603+
{
604+
return Path.Combine(backupGvfs, GVFSConstants.DotGVFS.Databases.Name);
605+
}
606+
538607
private bool TryBackupFiles(ITracer tracer, GVFSEnlistment enlistment, string backupRoot)
539608
{
540-
string backupSrc = Path.Combine(backupRoot, "src");
609+
string backupSrc = GetBackupSrcPath(backupRoot);
541610
string backupGit = Path.Combine(backupRoot, ".git");
542611
string backupGvfs = Path.Combine(backupRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot);
543-
string backupDatabases = Path.Combine(backupGvfs, GVFSConstants.DotGVFS.Databases.Name);
612+
string backupDatabases = GetBackupDatabasesPath(backupRoot);
544613

545614
string errorMessage = string.Empty;
546615
if (!this.ShowStatusWhileRunning(
@@ -638,6 +707,28 @@ private bool TryBackupFilesInFolder(ITracer tracer, string folderPath, string ba
638707
return true;
639708
}
640709

710+
private bool TryCopyFilesInFolder(ITracer tracer, string folderPath, string backupPath, string searchPattern, params string[] filenamesToSkip)
711+
{
712+
string errorMessage;
713+
foreach (string file in Directory.GetFiles(folderPath, searchPattern))
714+
{
715+
string fileName = Path.GetFileName(file);
716+
if (!filenamesToSkip.Any(x => x.Equals(fileName, GVFSPlatform.Instance.Constants.PathComparison)))
717+
{
718+
if (!this.TryIO(
719+
tracer,
720+
() => File.Copy(file, file.Replace(folderPath, backupPath)),
721+
$"Backing up {Path.GetFileName(file)}",
722+
out errorMessage))
723+
{
724+
return false;
725+
}
726+
}
727+
}
728+
729+
return true;
730+
}
731+
641732
private bool TryDownloadGitObjects(ITracer tracer, GVFSEnlistment enlistment, RetryConfig retryConfig)
642733
{
643734
string errorMessage = null;

0 commit comments

Comments
 (0)