diff --git a/GVFS/GVFS.Common/GVFSConstants.cs b/GVFS/GVFS.Common/GVFSConstants.cs index d098f595..3e13342e 100644 --- a/GVFS/GVFS.Common/GVFSConstants.cs +++ b/GVFS/GVFS.Common/GVFSConstants.cs @@ -44,6 +44,7 @@ namespace GVFS.Common public const string UpgradeRing = "upgrade.ring"; public const string UpgradeFeedPackageName = "upgrade.feedpackagename"; public const string UpgradeFeedUrl = "upgrade.feedurl"; + public const string OrgInfoServerUrl = "upgrade.orgInfoServerUrl"; } public static class GitStatusCache diff --git a/GVFS/GVFS.Common/GVFSPlatform.cs b/GVFS/GVFS.Common/GVFSPlatform.cs index 4924fa34..f5e0c807 100644 --- a/GVFS/GVFS.Common/GVFSPlatform.cs +++ b/GVFS/GVFS.Common/GVFSPlatform.cs @@ -25,6 +25,7 @@ namespace GVFS.Common public GVFSPlatformConstants Constants { get; } public UnderConstructionFlags UnderConstruction { get; } + public abstract string Name { get; } public static void Register(GVFSPlatform platform) { diff --git a/GVFS/GVFS.Common/NuGetUpgrade/NuGetUpgrader.cs b/GVFS/GVFS.Common/NuGetUpgrade/NuGetUpgrader.cs index 1aae7e85..bfff7cd1 100644 --- a/GVFS/GVFS.Common/NuGetUpgrade/NuGetUpgrader.cs +++ b/GVFS/GVFS.Common/NuGetUpgrade/NuGetUpgrader.cs @@ -1,6 +1,7 @@ using GVFS.Common.FileSystem; using GVFS.Common.Git; using GVFS.Common.Tracing; +using NuGet.Packaging.Core; using NuGet.Protocol.Core.Types; using System; using System.Collections.Generic; @@ -14,13 +15,14 @@ namespace GVFS.Common.NuGetUpgrade { public class NuGetUpgrader : ProductUpgrader { + protected readonly NuGetUpgraderConfig nuGetUpgraderConfig; + protected Version highestVersionAvailable; + private const string ContentDirectoryName = "content"; private const string InstallManifestFileName = "install-manifest.json"; private const string ExtractedInstallerDirectoryName = "InstallerTemp"; - private readonly NuGetUpgraderConfig nuGetUpgraderConfig; private InstallManifest installManifest; - private IPackageSearchMetadata highestVersionAvailable; private NuGetFeed nuGetFeed; private ICredentialStore credentialStore; private bool isNuGetFeedInitialized; @@ -216,33 +218,33 @@ namespace GVFS.Common.NuGetUpgrade IList queryResults = this.QueryFeed(firstAttempt: true); // Find the package with the highest version - IPackageSearchMetadata highestVersionAvailable = null; + IPackageSearchMetadata newestPackage = null; foreach (IPackageSearchMetadata result in queryResults) { - if (highestVersionAvailable == null || result.Identity.Version > highestVersionAvailable.Identity.Version) + if (newestPackage == null || result.Identity.Version > newestPackage.Identity.Version) { - highestVersionAvailable = result; + newestPackage = result; } } - if (highestVersionAvailable != null && - highestVersionAvailable.Identity.Version.Version > this.installedVersion) + if (newestPackage != null && + newestPackage.Identity.Version.Version > this.installedVersion) { - this.highestVersionAvailable = highestVersionAvailable; + this.highestVersionAvailable = newestPackage.Identity.Version.Version; } - newVersion = this.highestVersionAvailable?.Identity?.Version?.Version; + newVersion = this.highestVersionAvailable; if (newVersion != null) { this.tracer.RelatedInfo($"{nameof(this.TryQueryNewestVersion)} - new version available: installedVersion: {this.installedVersion}, highestVersionAvailable: {newVersion}"); - message = $"New version {highestVersionAvailable.Identity.Version} is available."; + message = $"New version {newestPackage.Identity.Version} is available."; return true; } - else if (highestVersionAvailable != null) + else if (newestPackage != null) { this.tracer.RelatedInfo($"{nameof(this.TryQueryNewestVersion)} - up-to-date"); - message = $"highest version available is {highestVersionAvailable.Identity.Version}, you are up-to-date"; + message = $"highest version available is {newestPackage.Identity.Version}, you are up-to-date"; return true; } else @@ -277,6 +279,11 @@ namespace GVFS.Common.NuGetUpgrade return false; } + if (!this.EnsureNuGetFeedInitialized(out errorMessage)) + { + return false; + } + if (!this.TryCreateAndConfigureDownloadDirectory(this.tracer, out errorMessage)) { this.tracer.RelatedError($"{nameof(NuGetUpgrader)}.{nameof(this.TryCreateAndConfigureDownloadDirectory)} failed. {errorMessage}"); @@ -287,7 +294,15 @@ namespace GVFS.Common.NuGetUpgrade { try { - this.DownloadedPackagePath = this.nuGetFeed.DownloadPackageAsync(this.highestVersionAvailable.Identity).GetAwaiter().GetResult(); + PackageIdentity packageId = this.GetPackageForVersion(this.highestVersionAvailable); + + if (packageId == null) + { + errorMessage = $"The specified version {this.highestVersionAvailable} was not found in the NuGet feed. Please check with your administrator to make sure the feed is set up correctly."; + return false; + } + + this.DownloadedPackagePath = this.nuGetFeed.DownloadPackageAsync(packageId).GetAwaiter().GetResult(); } catch (Exception ex) { @@ -463,6 +478,23 @@ namespace GVFS.Common.NuGetUpgrade return "{" + tokenString + "}"; } + private PackageIdentity GetPackageForVersion(Version version) + { + IList queryResults = this.QueryFeed(firstAttempt: true); + + IPackageSearchMetadata packageForVersion = null; + foreach (IPackageSearchMetadata result in queryResults) + { + if (result.Identity.Version.Version == version) + { + packageForVersion = result; + break; + } + } + + return packageForVersion?.Identity; + } + private bool TryGetPersonalAccessToken(string credentialUrl, ITracer tracer, out string token, out string error) { error = null; @@ -619,8 +651,8 @@ namespace GVFS.Common.NuGetUpgrade public class NuGetUpgraderConfig { - private readonly ITracer tracer; - private readonly LocalGVFSConfig localConfig; + protected readonly ITracer tracer; + protected readonly LocalGVFSConfig localConfig; public NuGetUpgraderConfig(ITracer tracer, LocalGVFSConfig localGVFSConfig) { @@ -648,7 +680,7 @@ namespace GVFS.Common.NuGetUpgrade /// NuGetUpgrader is considered ready if all required /// config settings are present. /// - public bool IsReady(out string error) + public virtual bool IsReady(out string error) { if (string.IsNullOrEmpty(this.FeedUrl) || string.IsNullOrEmpty(this.PackageFeedName)) @@ -667,7 +699,7 @@ namespace GVFS.Common.NuGetUpgrade /// /// Check if the NuGetUpgrader is configured. /// - public bool IsConfigured(out string error) + public virtual bool IsConfigured(out string error) { if (string.IsNullOrEmpty(this.FeedUrl) && string.IsNullOrEmpty(this.PackageFeedName)) @@ -686,7 +718,7 @@ namespace GVFS.Common.NuGetUpgrade /// /// Try to load the config for a NuGet upgrader. Returns false if there was an error reading the config. /// - public bool TryLoad(out string error) + public virtual bool TryLoad(out string error) { string configValue; if (!this.localConfig.TryGetConfig(GVFSConstants.LocalGVFSConfig.UpgradeFeedUrl, out configValue, out error)) diff --git a/GVFS/GVFS.Common/NuGetUpgrade/OrgNuGetUpgrader.cs b/GVFS/GVFS.Common/NuGetUpgrade/OrgNuGetUpgrader.cs new file mode 100644 index 00000000..5c87ec89 --- /dev/null +++ b/GVFS/GVFS.Common/NuGetUpgrade/OrgNuGetUpgrader.cs @@ -0,0 +1,262 @@ +using GVFS.Common.FileSystem; +using GVFS.Common.Git; +using GVFS.Common.Tracing; +using System; +using System.Net.Http; +using System.Runtime.Serialization; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace GVFS.Common.NuGetUpgrade +{ + public class OrgNuGetUpgrader : NuGetUpgrader + { + private HttpClient httpClient; + private string platform; + + public OrgNuGetUpgrader( + string currentVersion, + ITracer tracer, + PhysicalFileSystem fileSystem, + HttpClient httpClient, + bool dryRun, + bool noVerify, + OrgNuGetUpgraderConfig config, + string downloadFolder, + string platform, + ICredentialStore credentialStore) + : base( + currentVersion, + tracer, + fileSystem, + dryRun, + noVerify, + config, + downloadFolder, + credentialStore) + { + this.httpClient = httpClient; + this.platform = platform; + } + + public OrgNuGetUpgrader( + string currentVersion, + ITracer tracer, + PhysicalFileSystem fileSystem, + HttpClient httpClient, + bool dryRun, + bool noVerify, + OrgNuGetUpgraderConfig config, + string platform, + NuGetFeed nuGetFeed, + ICredentialStore credentialStore) + : base( + currentVersion, + tracer, + dryRun, + noVerify, + fileSystem, + config, + nuGetFeed, + credentialStore) + { + this.httpClient = httpClient; + this.platform = platform; + } + + public override bool SupportsAnonymousVersionQuery { get => true; } + + private OrgNuGetUpgraderConfig Config { get => this.nuGetUpgraderConfig as OrgNuGetUpgraderConfig; } + private string OrgInfoServerUrl { get => this.Config.OrgInfoServer; } + private string Ring { get => this.Config.UpgradeRing; } + + public static bool TryCreate( + ITracer tracer, + PhysicalFileSystem fileSystem, + LocalGVFSConfig gvfsConfig, + HttpClient httpClient, + ICredentialStore credentialStore, + bool dryRun, + bool noVerify, + out OrgNuGetUpgrader upgrader, + out string error) + { + OrgNuGetUpgraderConfig upgraderConfig = new OrgNuGetUpgraderConfig(tracer, gvfsConfig); + upgrader = null; + + if (!upgraderConfig.TryLoad(out error)) + { + upgrader = null; + return false; + } + + if (!upgraderConfig.IsConfigured(out error)) + { + return false; + } + + if (!upgraderConfig.IsReady(out error)) + { + return false; + } + + string platform = GVFSPlatform.Instance.Name; + + upgrader = new OrgNuGetUpgrader( + ProcessHelper.GetCurrentProcessVersion(), + tracer, + fileSystem, + httpClient, + dryRun, + noVerify, + upgraderConfig, + ProductUpgraderInfo.GetAssetDownloadsPath(), + platform, + credentialStore); + + return true; + } + + public override bool TryQueryNewestVersion(out Version newVersion, out string message) + { + newVersion = null; + + if (!TryParseOrgFromNuGetFeedUrl(this.Config.FeedUrl, out string orgName)) + { + message = "OrgNuGetUpgrader is not able to parse org name from NuGet Package Feed URL"; + return false; + } + + OrgInfoApiClient infoServer = new OrgInfoApiClient(this.httpClient, this.OrgInfoServerUrl); + + try + { + this.highestVersionAvailable = infoServer.QueryNewestVersion(orgName, this.platform, this.Ring); + } + catch (Exception exception) when (exception is HttpRequestException || + exception is TaskCanceledException) + { + // GetStringAsync can also throw a TaskCanceledException to indicate a timeout + // https://github.com/dotnet/corefx/issues/20296 + message = string.Format("Network error: could not connect to server ({0}). {1}", this.OrgInfoServerUrl, exception.Message); + this.TraceException(exception, nameof(this.TryQueryNewestVersion), "Error connecting to server."); + + return false; + } + catch (SerializationException exception) + { + message = string.Format("Parse error: could not parse response from server({0}). {1}", this.OrgInfoServerUrl, exception.Message); + this.TraceException(exception, nameof(this.TryQueryNewestVersion), "Error parsing response from server."); + + return false; + } + catch (Exception exception) when (exception is ArgumentException || + exception is FormatException || + exception is OverflowException) + { + message = string.Format("Unexpected response from server: could nor parse version({0}). {1}", this.OrgInfoServerUrl, exception.Message); + this.TraceException(exception, nameof(this.TryQueryNewestVersion), "Error parsing response from server."); + + return false; + } + + if (this.highestVersionAvailable != null && + this.highestVersionAvailable > this.installedVersion) + { + newVersion = this.highestVersionAvailable; + } + + if (newVersion != null) + { + this.tracer.RelatedInfo($"{nameof(this.TryQueryNewestVersion)} - new version available: installedVersion: {this.installedVersion}, highestVersionAvailable: {newVersion}"); + message = $"New version {newVersion} is available."; + return true; + } + else if (this.highestVersionAvailable != null) + { + this.tracer.RelatedInfo($"{nameof(this.TryQueryNewestVersion)} - up-to-date"); + message = $"Highest version available is {this.highestVersionAvailable}, you are up-to-date"; + return true; + } + else + { + this.tracer.RelatedInfo($"{nameof(this.TryQueryNewestVersion)} - no versions available from feed."); + message = "No versions available via endpoint."; + return true; + } + } + + private static bool TryParseOrgFromNuGetFeedUrl(string packageFeedUrl, out string orgName) + { + // We expect a URL of the form https://pkgs.dev.azure.com/{org} + // and want to convert it to a URL of the form https://{org}.visualstudio.com + Regex packageUrlRegex = new Regex( + @"^https://pkgs.dev.azure.com/(?.+?)/", + RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + Match urlMatch = packageUrlRegex.Match(packageFeedUrl); + + if (!urlMatch.Success) + { + orgName = null; + return false; + } + + orgName = urlMatch.Groups["org"].Value; + return true; + } + + public class OrgNuGetUpgraderConfig : NuGetUpgraderConfig + { + public OrgNuGetUpgraderConfig(ITracer tracer, LocalGVFSConfig localGVFSConfig) + : base(tracer, localGVFSConfig) + { + } + + public string OrgInfoServer { get; set; } + + public string UpgradeRing { get; set; } + + public override bool TryLoad(out string error) + { + if (!base.TryLoad(out error)) + { + return false; + } + + if (!this.localConfig.TryGetConfig(GVFSConstants.LocalGVFSConfig.OrgInfoServerUrl, out string orgInfoServerUrl, out error)) + { + this.tracer.RelatedError(error); + return false; + } + + this.OrgInfoServer = orgInfoServerUrl; + + if (!this.localConfig.TryGetConfig(GVFSConstants.LocalGVFSConfig.UpgradeRing, out string upgradeRing, out error)) + { + this.tracer.RelatedError(error); + return false; + } + + this.UpgradeRing = upgradeRing; + + return true; + } + + public override bool IsReady(out string error) + { + if (!base.IsReady(out error) || + string.IsNullOrEmpty(this.UpgradeRing) || + string.IsNullOrEmpty(this.OrgInfoServer)) + { + error = string.Join( + Environment.NewLine, + "One or more required settings for OrgNuGetUpgrader are missing.", + "Use `gvfs config [{GVFSConstants.LocalGVFSConfig.UpgradeFeedUrl} | {GVFSConstants.LocalGVFSConfig.UpgradeFeedPackageName} | {GVFSConstants.LocalGVFSConfig.UpgradeRing} | {GVFSConstants.LocalGVFSConfig.OrgInfoServerUrl}] ` to set the config."); + return false; + } + + return true; + } + } + } +} diff --git a/GVFS/GVFS.Common/OrgInfoApiClient.cs b/GVFS/GVFS.Common/OrgInfoApiClient.cs index 99499769..9387c66a 100644 --- a/GVFS/GVFS.Common/OrgInfoApiClient.cs +++ b/GVFS/GVFS.Common/OrgInfoApiClient.cs @@ -43,6 +43,11 @@ namespace GVFS.Common string responseString = this.client.GetStringAsync(this.ConstructRequest(this.VersionUrl, queryParams)).GetAwaiter().GetResult(); VersionResponse versionResponse = VersionResponse.FromJsonString(responseString); + if (string.IsNullOrEmpty(versionResponse.Version)) + { + return null; + } + return new Version(versionResponse.Version); } diff --git a/GVFS/GVFS.Platform.Mac/MacPlatform.cs b/GVFS/GVFS.Platform.Mac/MacPlatform.cs index e3d67fc5..352a2381 100644 --- a/GVFS/GVFS.Platform.Mac/MacPlatform.cs +++ b/GVFS/GVFS.Platform.Mac/MacPlatform.cs @@ -13,6 +13,7 @@ namespace GVFS.Platform.Mac public override IDiskLayoutUpgradeData DiskLayoutUpgrade { get; } = new MacDiskLayoutUpgradeData(); public override IKernelDriver KernelDriver { get; } = new ProjFSKext(); + public override string Name { get => "macOS"; } public override string GetOSVersionInformation() { diff --git a/GVFS/GVFS.Platform.Windows/WindowsPlatform.cs b/GVFS/GVFS.Platform.Windows/WindowsPlatform.cs index f51e91f9..5c2c8783 100644 --- a/GVFS/GVFS.Platform.Windows/WindowsPlatform.cs +++ b/GVFS/GVFS.Platform.Windows/WindowsPlatform.cs @@ -37,6 +37,7 @@ namespace GVFS.Platform.Windows public override IGitInstallation GitInstallation { get; } = new WindowsGitInstallation(); public override IDiskLayoutUpgradeData DiskLayoutUpgrade { get; } = new WindowsDiskLayoutUpgradeData(); public override IPlatformFileSystem FileSystem { get; } = new WindowsFileSystem(); + public override string Name { get => "Windows"; } public static string GetStringFromRegistry(string key, string valueName) { diff --git a/GVFS/GVFS.UnitTests/Common/NuGetUpgrade/OrgNuGetUpgraderTests.cs b/GVFS/GVFS.UnitTests/Common/NuGetUpgrade/OrgNuGetUpgraderTests.cs new file mode 100644 index 00000000..df5350dc --- /dev/null +++ b/GVFS/GVFS.UnitTests/Common/NuGetUpgrade/OrgNuGetUpgraderTests.cs @@ -0,0 +1,191 @@ +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.NuGetUpgrade; +using GVFS.Common.Tracing; +using GVFS.Tests.Should; +using GVFS.UnitTests.Category; +using GVFS.UnitTests.Mock.Common; +using GVFS.UnitTests.Mock.FileSystem; +using Moq; +using Moq.Protected; +using NuGet.Packaging.Core; +using NuGet.Protocol.Core.Types; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace GVFS.UnitTests.Common.NuGetUpgrade +{ + [TestFixture] + public class OrgNuGetUpgraderTests + { + private const string CurrentVersion = "1.5.1185.0"; + private const string NewerVersion = "1.6.1185.0"; + + private const string DefaultUpgradeFeedPackageName = "package"; + private const string DefaultUpgradeFeedUrl = "https://pkgs.dev.azure.com/contoso/"; + private const string DefaultOrgInfoServerUrl = "https://www.contoso.com"; + private const string DefaultRing = "slow"; + + private OrgNuGetUpgrader upgrader; + + private MockTracer tracer; + + private OrgNuGetUpgrader.OrgNuGetUpgraderConfig upgraderConfig; + + private Mock mockNuGetFeed; + private MockFileSystem mockFileSystem; + private Mock mockCredentialManager; + private Mock httpMessageHandlerMock; + + private string downloadDirectoryPath = Path.Combine( + $"mock:{Path.DirectorySeparatorChar}", + ProductUpgraderInfo.UpgradeDirectoryName, + ProductUpgraderInfo.DownloadDirectory); + + private interface IHttpMessageHandlerProtectedMembers + { + Task SendAsync(HttpRequestMessage message, CancellationToken token); + } + + public static IEnumerable NetworkFailureCases() + { + yield return new HttpRequestException("Response status code does not indicate success: 401: (Unauthorized)"); + yield return new TaskCanceledException("Task canceled"); + } + + [SetUp] + public void SetUp() + { + MockLocalGVFSConfig mockGvfsConfig = new MockLocalGVFSConfigBuilder( + DefaultRing, + DefaultUpgradeFeedUrl, + DefaultUpgradeFeedPackageName, + DefaultOrgInfoServerUrl) + .WithUpgradeRing() + .WithUpgradeFeedPackageName() + .WithUpgradeFeedUrl() + .WithOrgInfoServerUrl() + .Build(); + + this.upgraderConfig = new OrgNuGetUpgrader.OrgNuGetUpgraderConfig(this.tracer, mockGvfsConfig); + this.upgraderConfig.TryLoad(out _); + + this.tracer = new MockTracer(); + + this.mockNuGetFeed = new Mock( + DefaultUpgradeFeedUrl, + DefaultUpgradeFeedPackageName, + this.downloadDirectoryPath, + null, + this.tracer); + + this.mockFileSystem = new MockFileSystem( + new MockDirectory( + Path.GetDirectoryName(this.downloadDirectoryPath), + new[] { new MockDirectory(this.downloadDirectoryPath, null, null) }, + null)); + + this.mockCredentialManager = new Mock(); + string credentialManagerString = "value"; + string emptyString = string.Empty; + this.mockCredentialManager.Setup(foo => foo.TryGetCredential(It.IsAny(), It.IsAny(), out credentialManagerString, out credentialManagerString, out credentialManagerString)).Returns(true); + + this.httpMessageHandlerMock = new Mock(); + + this.httpMessageHandlerMock.Protected().As() + .Setup(m => m.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new HttpResponseMessage() + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(this.ConstructResponseContent(NewerVersion)) + }); + + HttpClient httpClient = new HttpClient(this.httpMessageHandlerMock.Object); + + this.upgrader = new OrgNuGetUpgrader( + CurrentVersion, + this.tracer, + this.mockFileSystem, + httpClient, + false, + false, + this.upgraderConfig, + "windows", + this.mockNuGetFeed.Object, + this.mockCredentialManager.Object); + } + + [TestCase] + public void SupportsAnonymousQuery() + { + this.upgrader.SupportsAnonymousVersionQuery.ShouldBeTrue(); + } + + [TestCase] + public void TryQueryNewestVersion() + { + Version newVersion; + string message; + + bool success = this.upgrader.TryQueryNewestVersion(out newVersion, out message); + + success.ShouldBeTrue(); + newVersion.ShouldNotBeNull(); + newVersion.ShouldEqual(new Version(NewerVersion)); + message.ShouldNotBeNull(); + message.ShouldEqual($"New version {OrgNuGetUpgraderTests.NewerVersion} is available."); + } + + [TestCaseSource("NetworkFailureCases")] + public void HandlesNetworkErrors(Exception ex) + { + Version newVersion; + string message; + + this.httpMessageHandlerMock.Protected().As() + .Setup(m => m.SendAsync(It.IsAny(), It.IsAny())) + .ThrowsAsync(ex); + + bool success = this.upgrader.TryQueryNewestVersion(out newVersion, out message); + + success.ShouldBeFalse(); + newVersion.ShouldBeNull(); + message.ShouldNotBeNull(); + message.ShouldContain("Network error"); + } + + [TestCase] + public void HandlesEmptyVersion() + { + Version newVersion; + string message; + + this.httpMessageHandlerMock.Protected().As() + .Setup(m => m.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new HttpResponseMessage() + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(this.ConstructResponseContent(string.Empty)) + }); + + bool success = this.upgrader.TryQueryNewestVersion(out newVersion, out message); + + success.ShouldBeTrue(); + newVersion.ShouldBeNull(); + message.ShouldNotBeNull(); + message.ShouldContain("No versions available"); + } + + private string ConstructResponseContent(string version) + { + return $"{{\"version\" : \"{version}\"}} "; + } + } +} diff --git a/GVFS/GVFS.UnitTests/Mock/Common/MockPlatform.cs b/GVFS/GVFS.UnitTests/Mock/Common/MockPlatform.cs index b17936b7..baf3c956 100644 --- a/GVFS/GVFS.UnitTests/Mock/Common/MockPlatform.cs +++ b/GVFS/GVFS.UnitTests/Mock/Common/MockPlatform.cs @@ -26,6 +26,7 @@ namespace GVFS.UnitTests.Mock.Common public override IDiskLayoutUpgradeData DiskLayoutUpgrade => throw new NotSupportedException(); public override IPlatformFileSystem FileSystem { get; } = new MockPlatformFileSystem(); + public override string Name { get => "Mock"; } public HashSet ActiveProcesses { get; } = new HashSet();