diff --git a/GVFS/GVFS.Common/DiskLayoutUpgrades/DiskLayoutUpgrade.cs b/GVFS/GVFS.Common/DiskLayoutUpgrades/DiskLayoutUpgrade.cs index d268191ab..2dccefa75 100644 --- a/GVFS/GVFS.Common/DiskLayoutUpgrades/DiskLayoutUpgrade.cs +++ b/GVFS/GVFS.Common/DiskLayoutUpgrades/DiskLayoutUpgrade.cs @@ -16,10 +16,12 @@ public abstract class DiskLayoutUpgrade protected abstract int SourceMinorVersion { get; } protected abstract bool IsMajorUpgrade { get; } - public static bool TryRunAllUpgrades(string enlistmentRoot) + public static bool TryRunAllUpgrades(string enlistmentRoot, out string error, out ReturnCode returnCode) { majorVersionUpgrades = new Dictionary(); minorVersionUpgrades = new Dictionary>(); + error = null; + returnCode = ReturnCode.Success; foreach (DiskLayoutUpgrade upgrade in GVFSPlatform.Instance.DiskLayoutUpgrade.Upgrades) { @@ -31,7 +33,7 @@ public static bool TryRunAllUpgrades(string enlistmentRoot) try { DiskLayoutUpgrade upgrade = null; - while (TryFindUpgrade(tracer, enlistmentRoot, out upgrade)) + while (TryFindUpgrade(tracer, enlistmentRoot, out upgrade, out error, out returnCode)) { if (upgrade == null) { @@ -40,11 +42,13 @@ public static bool TryRunAllUpgrades(string enlistmentRoot) if (!upgrade.TryUpgrade(tracer, enlistmentRoot)) { + returnCode = ReturnCode.GenericError; return false; } if (!CheckLayoutVersionWasIncremented(tracer, enlistmentRoot, upgrade)) { + returnCode = ReturnCode.GenericError; return false; } } @@ -55,6 +59,8 @@ public static bool TryRunAllUpgrades(string enlistmentRoot) { StartLogFile(enlistmentRoot, tracer); tracer.RelatedError(e.ToString()); + error = e.ToString(); + returnCode = ReturnCode.GenericError; return false; } finally @@ -64,17 +70,19 @@ public static bool TryRunAllUpgrades(string enlistmentRoot) } } - public static bool TryCheckDiskLayoutVersion(ITracer tracer, string enlistmentRoot, out string error) + public static bool TryCheckDiskLayoutVersion(ITracer tracer, string enlistmentRoot, out string error, out ReturnCode returnCode) { error = string.Empty; + returnCode = ReturnCode.Success; int majorVersion; int minorVersion; try { - if (TryGetDiskLayoutVersion(tracer, enlistmentRoot, out majorVersion, out minorVersion, out error)) + if (TryGetDiskLayoutVersion(tracer, enlistmentRoot, out majorVersion, out minorVersion, out error, out returnCode)) { if (majorVersion < GVFSPlatform.Instance.DiskLayoutUpgrade.Version.MinimumSupportedMajorVersion) { + returnCode = ReturnCode.GenericError; error = string.Format( "Breaking change to GVFS disk layout has been made since cloning. \r\nEnlistment disk layout version: {0} \r\nGVFS disk layout version: {1} \r\nMinimum supported version: {2}", majorVersion, @@ -85,6 +93,7 @@ public static bool TryCheckDiskLayoutVersion(ITracer tracer, string enlistmentRo } else if (majorVersion > GVFSPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion) { + returnCode = ReturnCode.GenericError; error = string.Format( "Changes to GVFS disk layout do not allow mounting after downgrade. Try mounting again using a more recent version of GVFS. \r\nEnlistment disk layout version: {0} \r\nGVFS disk layout version: {1}", majorVersion, @@ -94,6 +103,7 @@ public static bool TryCheckDiskLayoutVersion(ITracer tracer, string enlistmentRo } else if (majorVersion != GVFSPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion) { + returnCode = ReturnCode.GenericError; error = string.Format( "GVFS disk layout version doesn't match current version. Try running 'gvfs mount' to upgrade. \r\nEnlistment disk layout version: {0}.{1} \r\nGVFS disk layout version: {2}.{3}", majorVersion, @@ -112,7 +122,12 @@ public static bool TryCheckDiskLayoutVersion(ITracer tracer, string enlistmentRo RepoMetadata.Shutdown(); } - error = "Failed to read disk layout version. " + ConsoleHelper.GetGVFSLogMessage(enlistmentRoot); + if (returnCode != ReturnCode.MissingDiskLayoutVersion) + { + returnCode = ReturnCode.GenericError; + error = "Failed to read disk layout version. " + ConsoleHelper.GetGVFSLogMessage(enlistmentRoot); + } + return false; } @@ -256,13 +271,12 @@ private static bool CheckLayoutVersionWasIncremented(JsonTracer tracer, string e return true; } - private static bool TryFindUpgrade(JsonTracer tracer, string enlistmentRoot, out DiskLayoutUpgrade upgrade) + private static bool TryFindUpgrade(JsonTracer tracer, string enlistmentRoot, out DiskLayoutUpgrade upgrade, out string error, out ReturnCode returnCode) { int majorVersion; int minorVersion; - string error; - if (!TryGetDiskLayoutVersion(tracer, enlistmentRoot, out majorVersion, out minorVersion, out error)) + if (!TryGetDiskLayoutVersion(tracer, enlistmentRoot, out majorVersion, out minorVersion, out error, out returnCode)) { StartLogFile(enlistmentRoot, tracer); tracer.RelatedError(error); @@ -300,6 +314,8 @@ private static bool TryFindUpgrade(JsonTracer tracer, string enlistmentRoot, out // return true to indicate that we succeeded, and no upgrader was found upgrade = null; + error = null; + returnCode = ReturnCode.Success; return true; } @@ -309,9 +325,22 @@ private static bool TryGetDiskLayoutVersion( out int majorVersion, out int minorVersion, out string error) + { + ReturnCode returnCode; + return TryGetDiskLayoutVersion(tracer, enlistmentRoot, out majorVersion, out minorVersion, out error, out returnCode); + } + + private static bool TryGetDiskLayoutVersion( + ITracer tracer, + string enlistmentRoot, + out int majorVersion, + out int minorVersion, + out string error, + out ReturnCode returnCode) { majorVersion = 0; minorVersion = 0; + returnCode = ReturnCode.GenericError; string dotGVFSPath = Path.Combine(enlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot); @@ -323,13 +352,19 @@ private static bool TryGetDiskLayoutVersion( return false; } - if (!RepoMetadata.Instance.TryGetOnDiskLayoutVersion(out majorVersion, out minorVersion, out error)) + if (!RepoMetadata.Instance.TryGetOnDiskLayoutVersion(out majorVersion, out minorVersion, out error, out returnCode)) { + if (returnCode == ReturnCode.MissingDiskLayoutVersion) + { + error += " " + ConsoleHelper.GetGVFSLogMessage(enlistmentRoot); + } + return false; } } error = null; + returnCode = ReturnCode.Success; return true; } diff --git a/GVFS/GVFS.Common/RepoMetadata.cs b/GVFS/GVFS.Common/RepoMetadata.cs index ea3d3ebf1..00854fc69 100644 --- a/GVFS/GVFS.Common/RepoMetadata.cs +++ b/GVFS/GVFS.Common/RepoMetadata.cs @@ -8,6 +8,8 @@ namespace GVFS.Common { public class RepoMetadata { + public const string MissingDiskLayoutVersionMessage = "Enlistment disk layout version not found. The .gvfs metadata is incomplete. Make sure your work is pushed or backed up, then reclone this enlistment."; + private FileBasedDictionary repoMetadata; private ITracer tracer; @@ -90,16 +92,24 @@ public static void Shutdown() } public bool TryGetOnDiskLayoutVersion(out int majorVersion, out int minorVersion, out string error) + { + ReturnCode returnCode; + return this.TryGetOnDiskLayoutVersion(out majorVersion, out minorVersion, out error, out returnCode); + } + + public bool TryGetOnDiskLayoutVersion(out int majorVersion, out int minorVersion, out string error, out ReturnCode returnCode) { majorVersion = 0; minorVersion = 0; + returnCode = ReturnCode.GenericError; try { string value; if (!this.repoMetadata.TryGetValue(Keys.DiskLayoutMajorVersion, out value)) { - error = "Enlistment disk layout version not found, check if a breaking change has been made to GVFS since cloning this enlistment."; + error = MissingDiskLayoutVersionMessage; + returnCode = ReturnCode.MissingDiskLayoutVersion; return false; } @@ -125,6 +135,7 @@ public bool TryGetOnDiskLayoutVersion(out int majorVersion, out int minorVersion } error = null; + returnCode = ReturnCode.Success; return true; } diff --git a/GVFS/GVFS.Common/ReturnCode.cs b/GVFS/GVFS.Common/ReturnCode.cs index 31842f396..4c5347cc6 100644 --- a/GVFS/GVFS.Common/ReturnCode.cs +++ b/GVFS/GVFS.Common/ReturnCode.cs @@ -14,5 +14,6 @@ public enum ReturnCode AuthenticationError = 9, CredentialTimeout = 10, RemoteGvfsConfigError = 11, + MissingDiskLayoutVersion = 12, } } diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MountTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MountTests.cs index 576534d1c..259a4dc98 100644 --- a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MountTests.cs +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MountTests.cs @@ -18,6 +18,10 @@ namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture public class MountTests : TestsWithEnlistmentPerFixture { private const int GVFSGenericError = 3; + // These values are a shipped command-line contract. Do not bind them to product constants. + private const int GVFSMissingDiskLayoutVersion = 12; + private const string MissingDiskLayoutVersionMessage = "The .gvfs metadata is incomplete"; + private const string GVFSLogMessage = "Run 'gvfs log"; private const uint GenericRead = 2147483648; private const uint FileFlagBackupSemantics = 3355443; private readonly int fileDeletedBackgroundOperationCode; @@ -99,6 +103,34 @@ public void MountChangesMountId() .ShouldNotEqual(mountId, "gvfs.mount-id should change on every mount"); } + [TestCase] + public void MountFailsWithUniqueExitCodeWhenDiskLayoutVersionIsMissing() + { + this.Enlistment.UnmountGVFS(); + + string majorVersion; + string minorVersion; + GVFSHelpers.GetPersistedDiskLayoutVersion(this.Enlistment.DotGVFSRoot, out majorVersion, out minorVersion); + majorVersion.ShouldNotBeNull(); + minorVersion.ShouldNotBeNull(); + + try + { + GVFSHelpers.DeletePersistedDiskLayoutMajorVersion(this.Enlistment.DotGVFSRoot); + + this.MountShouldFail( + GVFSMissingDiskLayoutVersion, + MissingDiskLayoutVersionMessage, + additionalExpectedErrorMessage: GVFSLogMessage); + } + finally + { + GVFSHelpers.SaveDiskLayoutVersion(this.Enlistment.DotGVFSRoot, majorVersion, minorVersion); + } + + this.Enlistment.MountGVFS(); + } + [TestCase] public void MountFailsWhenNoOnDiskVersion() { @@ -126,7 +158,10 @@ public void MountFailsWhenNoOnDiskVersion() this.fileSystem.MoveFile(versionDatabasePath, tempDatabasePath); versionDatabasePath.ShouldNotExistOnDisk(this.fileSystem); - this.MountShouldFail("Failed to upgrade repo disk layout"); + this.MountShouldFail( + GVFSMissingDiskLayoutVersion, + MissingDiskLayoutVersionMessage, + additionalExpectedErrorMessage: GVFSLogMessage); } finally { @@ -359,7 +394,7 @@ public void ProjFS_CMDHangNoneActiveInstance() this.Enlistment.MountGVFS(); } - private void MountShouldFail(int expectedExitCode, string expectedErrorMessage, string mountWorkingDirectory = null) + private void MountShouldFail(int expectedExitCode, string expectedErrorMessage, string mountWorkingDirectory = null, string additionalExpectedErrorMessage = null) { string enlistmentRoot = this.Enlistment.EnlistmentRoot; @@ -379,6 +414,11 @@ private void MountShouldFail(int expectedExitCode, string expectedErrorMessage, { string combinedOutput = result.Output + "\n" + result.Errors; combinedOutput.ShouldContain(expectedErrorMessage); + + if (additionalExpectedErrorMessage != null) + { + combinedOutput.ShouldContain(additionalExpectedErrorMessage); + } } } diff --git a/GVFS/GVFS.FunctionalTests/Tools/GVFSHelpers.cs b/GVFS/GVFS.FunctionalTests/Tools/GVFSHelpers.cs index d1b356f3d..632a5869f 100644 --- a/GVFS/GVFS.FunctionalTests/Tools/GVFSHelpers.cs +++ b/GVFS/GVFS.FunctionalTests/Tools/GVFSHelpers.cs @@ -50,6 +50,11 @@ public static void SaveDiskLayoutVersion(string dotGVFSRoot, string majorVersion SavePersistedValue(dotGVFSRoot, DiskLayoutMinorVersionKey, minorVersion); } + public static void DeletePersistedDiskLayoutMajorVersion(string dotGVFSRoot) + { + DeletePersistedValue(dotGVFSRoot, DiskLayoutMajorVersionKey); + } + public static void GetPersistedDiskLayoutVersion(string dotGVFSRoot, out string majorVersion, out string minorVersion) { majorVersion = GetPersistedValue(dotGVFSRoot, DiskLayoutMajorVersionKey); @@ -314,31 +319,29 @@ private static byte CharToByte(char c) private static string GetPersistedValue(string dotGVFSRoot, string key) { - string metadataPath = Path.Combine(dotGVFSRoot, RepoMetadataName); - string json; - using (FileStream fs = new FileStream(metadataPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) - using (StreamReader reader = new StreamReader(fs)) - { - while (!reader.EndOfStream) - { - json = reader.ReadLine(); - json.Substring(0, 2).ShouldEqual("A "); + Dictionary repoMetadata = ReadPersistedValues(dotGVFSRoot); + string value; + repoMetadata.TryGetValue(key, out value); + return value; + } - KeyValuePair kvp = GVFSJsonOptions.Deserialize>(json.Substring(2)); - if (kvp.Key == key) - { - return kvp.Value; - } - } - } + private static void SavePersistedValue(string dotGVFSRoot, string key, string value) + { + Dictionary repoMetadata = ReadPersistedValues(dotGVFSRoot); + repoMetadata[key] = value; + WritePersistedValues(dotGVFSRoot, repoMetadata); + } - return null; + private static void DeletePersistedValue(string dotGVFSRoot, string key) + { + Dictionary repoMetadata = ReadPersistedValues(dotGVFSRoot); + repoMetadata.Remove(key); + WritePersistedValues(dotGVFSRoot, repoMetadata); } - private static void SavePersistedValue(string dotGVFSRoot, string key, string value) + private static Dictionary ReadPersistedValues(string dotGVFSRoot) { string metadataPath = Path.Combine(dotGVFSRoot, RepoMetadataName); - Dictionary repoMetadata = new Dictionary(); string json; using (FileStream fs = new FileStream(metadataPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) @@ -350,12 +353,16 @@ private static void SavePersistedValue(string dotGVFSRoot, string key, string va json.Substring(0, 2).ShouldEqual("A "); KeyValuePair kvp = GVFSJsonOptions.Deserialize>(json.Substring(2)); - repoMetadata.Add(kvp.Key, kvp.Value); + repoMetadata[kvp.Key] = kvp.Value; } } - repoMetadata[key] = value; + return repoMetadata; + } + private static void WritePersistedValues(string dotGVFSRoot, Dictionary repoMetadata) + { + string metadataPath = Path.Combine(dotGVFSRoot, RepoMetadataName); string newRepoMetadataContents = string.Empty; foreach (KeyValuePair kvp in repoMetadata) diff --git a/GVFS/GVFS.UnitTests/Common/RepoMetadataTests.cs b/GVFS/GVFS.UnitTests/Common/RepoMetadataTests.cs new file mode 100644 index 000000000..f6ba95437 --- /dev/null +++ b/GVFS/GVFS.UnitTests/Common/RepoMetadataTests.cs @@ -0,0 +1,82 @@ +using GVFS.Common; +using GVFS.Tests.Should; +using GVFS.UnitTests.Mock.Common; +using GVFS.UnitTests.Mock.FileSystem; +using NUnit.Framework; +using System.Collections.Generic; +using System.IO; + +namespace GVFS.UnitTests.Common +{ + [TestFixture] + public class RepoMetadataTests + { + private const string DiskLayoutMajorVersionKey = "DiskLayoutVersion"; + private const string DotGVFSPath = "mock:\\repo\\.gvfs"; + + private MockFileSystem fileSystem; + + [SetUp] + public void SetUp() + { + if (GVFSPlatform.Instance == null) + { + GVFSPlatform.Register(new MockPlatform()); + } + + MockDirectory root = new MockDirectory("mock:", null, null); + root.CreateDirectory(Path.Combine(DotGVFSPath, "databases")); + this.fileSystem = new MockFileSystem(root); + + string error; + RepoMetadata.TryInitialize(new MockTracer(), this.fileSystem, DotGVFSPath, out error) + .ShouldEqual(true); + } + + [TearDown] + public void TearDown() + { + RepoMetadata.Shutdown(); + } + + [TestCase] + public void MissingDiskLayoutVersionReturnsSpecificReturnCode() + { + int majorVersion; + int minorVersion; + string error; + ReturnCode returnCode; + + RepoMetadata.Instance.TryGetOnDiskLayoutVersion(out majorVersion, out minorVersion, out error, out returnCode) + .ShouldEqual(false); + + returnCode.ShouldEqual(ReturnCode.MissingDiskLayoutVersion); + error.ShouldEqual(RepoMetadata.MissingDiskLayoutVersionMessage); + } + + [TestCase] + public void InvalidDiskLayoutVersionReturnsGenericError() + { + string metadataPath = RepoMetadata.Instance.DataFilePath; + KeyValuePair diskLayoutVersion = new KeyValuePair(DiskLayoutMajorVersionKey, "not-an-int"); + this.fileSystem.WriteAllText(metadataPath, "A " + GVFSJsonOptions.Serialize(diskLayoutVersion).Trim() + "\r\n"); + + RepoMetadata.Shutdown(); + + string initializeError; + RepoMetadata.TryInitialize(new MockTracer(), this.fileSystem, DotGVFSPath, out initializeError) + .ShouldEqual(true); + + int majorVersion; + int minorVersion; + string error; + ReturnCode returnCode; + + RepoMetadata.Instance.TryGetOnDiskLayoutVersion(out majorVersion, out minorVersion, out error, out returnCode) + .ShouldEqual(false); + + returnCode.ShouldEqual(ReturnCode.GenericError); + error.ShouldContain("Failed to parse persisted disk layout version number"); + } + } +} diff --git a/GVFS/GVFS/CommandLine/DehydrateVerb.cs b/GVFS/GVFS/CommandLine/DehydrateVerb.cs index 28ce30357..9c3922ede 100644 --- a/GVFS/GVFS/CommandLine/DehydrateVerb.cs +++ b/GVFS/GVFS/CommandLine/DehydrateVerb.cs @@ -223,9 +223,10 @@ from a parent of the folders list. this.Unmount(tracer); string error; - if (!DiskLayoutUpgrade.TryCheckDiskLayoutVersion(tracer, enlistment.PrimaryEnlistmentRoot, out error)) + ReturnCode returnCode; + if (!DiskLayoutUpgrade.TryCheckDiskLayoutVersion(tracer, enlistment.PrimaryEnlistmentRoot, out error, out returnCode)) { - this.ReportErrorAndExit(tracer, error); + this.ReportErrorAndExit(tracer, returnCode, error); } RetryConfig retryConfig; diff --git a/GVFS/GVFS/CommandLine/MountVerb.cs b/GVFS/GVFS/CommandLine/MountVerb.cs index 5ea8c5781..2c4ab26cb 100644 --- a/GVFS/GVFS/CommandLine/MountVerb.cs +++ b/GVFS/GVFS/CommandLine/MountVerb.cs @@ -130,15 +130,31 @@ protected override void PreCreateEnlistment() } } - if (!DiskLayoutUpgrade.TryRunAllUpgrades(enlistmentRoot)) + string diskLayoutError; + ReturnCode diskLayoutReturnCode; + if (!DiskLayoutUpgrade.TryRunAllUpgrades(enlistmentRoot, out diskLayoutError, out diskLayoutReturnCode)) { - this.ReportErrorAndExit("Failed to upgrade repo disk layout. " + ConsoleHelper.GetGVFSLogMessage(enlistmentRoot)); + if (diskLayoutReturnCode == ReturnCode.MissingDiskLayoutVersion) + { + this.ReportErrorAndExit(tracer: null, exitCode: diskLayoutReturnCode, error: "Error: " + diskLayoutError); + } + else + { + string upgradeErrorMessage = "Failed to upgrade repo disk layout."; + if (!string.IsNullOrWhiteSpace(diskLayoutError)) + { + upgradeErrorMessage += " " + diskLayoutError; + } + + this.ReportErrorAndExit(upgradeErrorMessage + " " + ConsoleHelper.GetGVFSLogMessage(enlistmentRoot)); + } } string error; - if (!DiskLayoutUpgrade.TryCheckDiskLayoutVersion(tracer: null, enlistmentRoot: enlistmentRoot, error: out error)) + ReturnCode returnCode; + if (!DiskLayoutUpgrade.TryCheckDiskLayoutVersion(tracer: null, enlistmentRoot: enlistmentRoot, error: out error, returnCode: out returnCode)) { - this.ReportErrorAndExit("Error: " + error); + this.ReportErrorAndExit(tracer: null, exitCode: returnCode, error: "Error: " + error); } } diff --git a/GVFS/GVFS/CommandLine/RepairVerb.cs b/GVFS/GVFS/CommandLine/RepairVerb.cs index dc0034adc..839765629 100644 --- a/GVFS/GVFS/CommandLine/RepairVerb.cs +++ b/GVFS/GVFS/CommandLine/RepairVerb.cs @@ -92,9 +92,10 @@ To actually execute any necessary repair(s), run 'gvfs repair --confirm' } string error; - if (!DiskLayoutUpgrade.TryCheckDiskLayoutVersion(tracer: null, enlistmentRoot: enlistment.PrimaryEnlistmentRoot, error: out error)) + ReturnCode returnCode; + if (!DiskLayoutUpgrade.TryCheckDiskLayoutVersion(tracer: null, enlistmentRoot: enlistment.PrimaryEnlistmentRoot, error: out error, returnCode: out returnCode)) { - this.ReportErrorAndExit(error); + this.ReportErrorAndExit(tracer: null, exitCode: returnCode, error: error); } if (!ConsoleHelper.ShowStatusWhileRunning(