diff --git a/SampleExtension/FeaturedApplicationsProvider.cs b/SampleExtension/FeaturedApplicationsProvider.cs index 469a382891..d47152be20 100644 --- a/SampleExtension/FeaturedApplicationsProvider.cs +++ b/SampleExtension/FeaturedApplicationsProvider.cs @@ -31,7 +31,7 @@ public GetFeaturedApplicationsResult GetApplications() { // Sample list of featured applications return new GetFeaturedApplicationsResult(new List() - { + { "x-ms-winget://winget/Microsoft.VisualStudio.2022.Community", "x-ms-winget://winget/Microsoft.VisualStudioCode", "x-ms-winget://winget/Microsoft.PowerShell", diff --git a/SampleExtension/Program.cs b/SampleExtension/Program.cs index 965024af98..e19674c54f 100644 --- a/SampleExtension/Program.cs +++ b/SampleExtension/Program.cs @@ -14,7 +14,7 @@ public static void Main(string[] args) { if (args.Length > 0 && args[0] == "-RegisterProcessAsComServer") { - using ExtensionServer server = new (); + using ExtensionServer server = new(); var extensionDisposedEvent = new ManualResetEvent(false); var extensionInstance = new SampleExtension(extensionDisposedEvent); diff --git a/SampleExtension/RepositoryProvider.cs b/SampleExtension/RepositoryProvider.cs index 2ff4dbe6b3..7125da5cff 100644 --- a/SampleExtension/RepositoryProvider.cs +++ b/SampleExtension/RepositoryProvider.cs @@ -7,6 +7,7 @@ using Windows.Storage.Streams; namespace SampleExtension; + internal sealed class RepositoryProvider : IRepositoryProvider { public string DisplayName => $"Sample {nameof(RepositoryProvider)}"; diff --git a/SampleExtension/SampleExtension.csproj b/SampleExtension/SampleExtension.csproj index 2ef4c992b9..9c965fb4ec 100644 --- a/SampleExtension/SampleExtension.csproj +++ b/SampleExtension/SampleExtension.csproj @@ -26,7 +26,7 @@ - + diff --git a/tools/SetupFlow/DevHome.SetupFlow.Common/Contracts/InstallPackageTaskArguments.cs b/tools/SetupFlow/DevHome.SetupFlow.Common/Contracts/InstallPackageTaskArguments.cs index 8746089b7e..2ccedf1a69 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.Common/Contracts/InstallPackageTaskArguments.cs +++ b/tools/SetupFlow/DevHome.SetupFlow.Common/Contracts/InstallPackageTaskArguments.cs @@ -15,6 +15,7 @@ public class InstallPackageTaskArguments : ITaskArguments { private const string PackageIdArg = "--package-id"; private const string PackageCatalogArg = "--package-catalog"; + private const string PackageVersionArg = "--package-version"; /// /// Gets or sets the package id @@ -30,6 +31,14 @@ public string PackageId public string CatalogName { get; set; + } + + /// + /// Gets or sets the package version + /// + public string Version + { + get; set; } /// @@ -43,17 +52,19 @@ public static bool TryReadArguments(IList argumentList, ref int index, o { result = null; - // --package-id --package-catalog - // [ index ] [index + 1] [ index + 2 ] [index + 3] - const int TaskArgListCount = 4; + // --package-id --package-catalog --package-version + // [ index ] [index + 1] [ index + 2 ] [index + 3][ index + 4 ] [index + 5] + const int TaskArgListCount = 6; if (index + TaskArgListCount <= argumentList.Count && argumentList[index] == PackageIdArg && - argumentList[index + 2] == PackageCatalogArg) + argumentList[index + 2] == PackageCatalogArg && + argumentList[index + 4] == PackageVersionArg) { result = new InstallPackageTaskArguments { PackageId = argumentList[index + 1], - CatalogName = argumentList[index + 3], + CatalogName = argumentList[index + 3], + Version = argumentList[index + 5], }; index += TaskArgListCount; return true; @@ -68,7 +79,8 @@ public IList ToArgumentList() return new List() { PackageIdArg, PackageId, // --package-id - PackageCatalogArg, CatalogName, // --package-catalog + PackageCatalogArg, CatalogName, // --package-catalog + PackageVersionArg, Version, // --package-version }; } diff --git a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/ElevatedComponentOperation.cs b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/ElevatedComponentOperation.cs index f8013c2016..bf21a5470a 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/ElevatedComponentOperation.cs +++ b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/ElevatedComponentOperation.cs @@ -51,16 +51,16 @@ public void WriteToStdOut(string value) Console.WriteLine(value); } - public IAsyncOperation InstallPackageAsync(string packageId, string catalogName) + public IAsyncOperation InstallPackageAsync(string packageId, string catalogName, string version) { - var taskArguments = GetInstallPackageTaskArguments(packageId, catalogName); + var taskArguments = GetInstallPackageTaskArguments(packageId, catalogName, version); return ValidateAndExecuteAsync( taskArguments, async () => { Log.Logger?.ReportInfo(Log.Component.Elevated, $"Installing package elevated: '{packageId}' from '{catalogName}'"); var task = new ElevatedInstallTask(); - return await task.InstallPackage(taskArguments.PackageId, taskArguments.CatalogName); + return await task.InstallPackage(taskArguments.PackageId, taskArguments.CatalogName, version); }, result => result.TaskSucceeded).AsAsyncOperation(); } @@ -120,14 +120,14 @@ public void Terminate() } } - private InstallPackageTaskArguments GetInstallPackageTaskArguments(string packageId, string catalogName) + private InstallPackageTaskArguments GetInstallPackageTaskArguments(string packageId, string catalogName, string version) { // Ensure the package to install has been pre-approved by checking against the process tasks arguments - var taskArguments = _tasksArguments.InstallPackages?.FirstOrDefault(def => def.PackageId == packageId && def.CatalogName == catalogName); + var taskArguments = _tasksArguments.InstallPackages?.FirstOrDefault(def => def.PackageId == packageId && def.CatalogName == catalogName && def.Version == version); if (taskArguments == null) { - Log.Logger?.ReportError(Log.Component.Elevated, $"No match found for PackageId={packageId} and CatalogId={catalogName} in the process tasks arguments."); - throw new ArgumentException($"Failed to install '{packageId}' from '{catalogName}' because it was not in the pre-approved tasks arguments"); + Log.Logger?.ReportError(Log.Component.Elevated, $"No match found for PackageId={packageId}, CatalogId={catalogName} and Version={version} in the process tasks arguments."); + throw new ArgumentException($"Failed to install '{packageId}' ({version}) from '{catalogName}' because it was not in the pre-approved tasks arguments"); } return taskArguments; diff --git a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/IElevatedComponentOperation.cs b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/IElevatedComponentOperation.cs index 66c954ef44..8bcd7cbba8 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/IElevatedComponentOperation.cs +++ b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/IElevatedComponentOperation.cs @@ -33,9 +33,10 @@ public interface IElevatedComponentOperation /// Install a package /// /// Package id - /// Package catalog name + /// Package catalog name + /// Package version /// Install package operation result - public IAsyncOperation InstallPackageAsync(string packageId, string catalogName); + public IAsyncOperation InstallPackageAsync(string packageId, string catalogName, string version); /// /// Create a dev drive diff --git a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedInstallTask.cs b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedInstallTask.cs index 5f50c1e8c4..3ee73240c4 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedInstallTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedInstallTask.cs @@ -35,7 +35,7 @@ public sealed class ElevatedInstallTask /// /// Installs a package given its ID and the ID of the catalog it comes from. /// - public IAsyncOperation InstallPackage(string packageId, string catalogName) + public IAsyncOperation InstallPackage(string packageId, string catalogName, string version) { return Task.Run(async () => { @@ -72,6 +72,14 @@ public IAsyncOperation InstallPackage(string packageI var installOptions = _wingetFactory.CreateInstallOptions(); installOptions.PackageInstallMode = PackageInstallMode.Silent; + if (!string.IsNullOrWhiteSpace(version)) + { + installOptions.PackageVersionId = FindVersionOrThrow(result, packageToInstall, version); + } + else + { + Log.Logger?.ReportInfo(Log.Component.AppManagement, $"Install version not specified. Falling back to default install version {packageToInstall.DefaultInstallVersion.Version}"); + } Log.Logger?.ReportInfo(Log.Component.AppManagement, $"Initiating install of package {packageId}"); var installResult = await packageManager.InstallPackageAsync(packageToInstall, installOptions); @@ -117,4 +125,30 @@ private FindPackagesOptions CreateFindOptionsForPackageId(string packageId) return findOptions; } + + /// + /// Find a specific version in the list of available versions for a package. + /// + /// Target package + /// Version to find + /// Specified version + /// Exception thrown if the specified version was not found + private PackageVersionId FindVersionOrThrow(ElevatedInstallTaskResult result, CatalogPackage package, string version) + { + // Find the version in the list of available versions + for (var i = 0; i < package.AvailableVersions.Count; i++) + { + if (package.AvailableVersions[i].Version == version) + { + return package.AvailableVersions[i]; + } + } + + var installErrorInvalidParameter = unchecked((int)0x8A150112); + result.Status = (int)InstallResultStatus.InvalidOptions; + result.ExtendedErrorCode = installErrorInvalidParameter; + var message = $"Specified install version was not found {version}."; + Log.Logger?.ReportError(Log.Component.AppManagement, message); + throw new ArgumentException(message); + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow.UnitTest/Helpers/PackageHelper.cs b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/Helpers/PackageHelper.cs index 35c3b68334..e96b01eb01 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.UnitTest/Helpers/PackageHelper.cs +++ b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/Helpers/PackageHelper.cs @@ -17,7 +17,7 @@ public static Mock CreatePackage(string id, string catalogId = " package.Setup(p => p.Name).Returns("Mock Package Name"); package.Setup(p => p.PackageUrl).Returns(new Uri("https://packageUrl")); package.Setup(p => p.PublisherUrl).Returns(new Uri("https://publisherUrl")); - package.Setup(p => p.Version).Returns("Mock Version"); + package.Setup(p => p.InstalledVersion).Returns("Mock Version"); // Allow icon properties to be set and get like regular properties package.SetupProperty(p => p.LightThemeIcon); diff --git a/tools/SetupFlow/DevHome.SetupFlow.UnitTest/ViewModels/PackageViewModelTest.cs b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/ViewModels/PackageViewModelTest.cs index 98c94cd400..072614cd26 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.UnitTest/ViewModels/PackageViewModelTest.cs +++ b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/ViewModels/PackageViewModelTest.cs @@ -29,7 +29,7 @@ public void CreatePackageViewModel_Success() for (var i = 0; i < expectedPackages.Count; ++i) { Assert.AreEqual(expectedPackages[i].Name, packages[i].Name); - Assert.AreEqual(expectedPackages[i].Version, packages[i].Version); + Assert.AreEqual(expectedPackages[i].InstalledVersion, packages[i].InstalledVersion); } } @@ -88,7 +88,8 @@ public void PackageDescription_VersionAndPublisherAreOptional_ReturnsExpectedDes package.Setup(p => p.CatalogId).Returns(source); package.Setup(p => p.CatalogName).Returns(source); package.Setup(p => p.PublisherName).Returns(publisher); - package.Setup(p => p.Version).Returns(version); + package.Setup(p => p.IsInstalled).Returns(true); + package.Setup(p => p.InstalledVersion).Returns(version); StringResource .Setup(sr => sr.GetLocalized(StringResourceKey.PackageDescriptionThreeParts, It.IsAny())) .Returns((string key, object[] args) => $"{args[0]} | {args[1]} | {args[2]}"); @@ -100,6 +101,6 @@ public void PackageDescription_VersionAndPublisherAreOptional_ReturnsExpectedDes var packageViewModel = TestHost.CreateInstance(package.Object); // Assert - Assert.AreEqual(expectedDescription, packageViewModel.PackageDescription); + Assert.AreEqual(expectedDescription, packageViewModel.PackageFullDescription); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow.UnitTest/ViewModels/WinGetPackageJsonDataSourceTest.cs b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/ViewModels/WinGetPackageJsonDataSourceTest.cs index 963773315e..22ba42511f 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.UnitTest/ViewModels/WinGetPackageJsonDataSourceTest.cs +++ b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/ViewModels/WinGetPackageJsonDataSourceTest.cs @@ -20,10 +20,11 @@ public void LoadCatalogs_Success_ReturnsWinGetCatalogs() // Prepare expected package var expectedPackages = new List { - PackageHelper.CreatePackage("mock1").Object, - PackageHelper.CreatePackage("mock2").Object, + PackageHelper.CreatePackage("mock1", "winget").Object, + PackageHelper.CreatePackage("mock2", "winget").Object, }; - WindowsPackageManager!.Setup(wpm => wpm.GetPackagesAsync(It.IsAny>())).ReturnsAsync(expectedPackages); + WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny>())).ReturnsAsync(expectedPackages); + WindowsPackageManager.Setup(wpm => wpm.CreatePackageUri(It.IsAny())).Returns(p => new WinGetPackageUri(p.CatalogName, p.Id)); // Act var loadedPackages = LoadCatalogsFromJsonDataSource("AppManagementPackages_Success.json"); @@ -35,6 +36,8 @@ public void LoadCatalogs_Success_ReturnsWinGetCatalogs() Assert.AreEqual(expectedPackages.Count, loadedPackages[0].Packages.Count); Assert.AreEqual(expectedPackages[0].Id, loadedPackages[0].Packages.ElementAt(0).Id); Assert.AreEqual(expectedPackages[1].Id, loadedPackages[0].Packages.ElementAt(1).Id); + Assert.AreEqual(expectedPackages[0].CatalogName, loadedPackages[0].Packages.ElementAt(0).CatalogName); + Assert.AreEqual(expectedPackages[1].CatalogName, loadedPackages[0].Packages.ElementAt(1).CatalogName); } [TestMethod] @@ -42,21 +45,21 @@ public void LoadCatalogs_EmptyPackages_ReturnsNoCatalogs() { // Prepare expected package var expectedPackages = new List(); - WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny>())).ReturnsAsync(expectedPackages); + WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny>())).ReturnsAsync(expectedPackages); // Act var loadedPackages = LoadCatalogsFromJsonDataSource("AppManagementPackages_Empty.json"); // Assert Assert.AreEqual(0, loadedPackages.Count); - WindowsPackageManager.Verify(c => c.GetPackagesAsync(It.IsAny>()), Times.Never()); + WindowsPackageManager.Verify(c => c.GetPackagesAsync(It.IsAny>()), Times.Never()); } [TestMethod] public void LoadCatalogs_ExceptionThrownWhenGettingPackages_ReturnsNoCatalogs() { // Configure package manager - WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny>())).ThrowsAsync(new FindPackagesException(FindPackagesResultStatus.CatalogError)); + WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny>())).ThrowsAsync(new FindPackagesException(FindPackagesResultStatus.CatalogError)); // Act var loadedPackages = LoadCatalogsFromJsonDataSource("AppManagementPackages_Success.json"); @@ -70,7 +73,7 @@ public void LoadCatalogs_ExceptionThrownWhenOpeningFile_ThrowsException() { // Prepare expected package var expectedPackages = new List(); - WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny>())).ReturnsAsync(expectedPackages); + WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny>())).ReturnsAsync(expectedPackages); // Act/Assert var fileName = TestHelpers.GetTestFilePath("file_not_found"); diff --git a/tools/SetupFlow/DevHome.SetupFlow.UnitTest/ViewModels/WinGetPackageRestoreDataSourceTest.cs b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/ViewModels/WinGetPackageRestoreDataSourceTest.cs index cf0b75245d..3a353bfbb2 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.UnitTest/ViewModels/WinGetPackageRestoreDataSourceTest.cs +++ b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/ViewModels/WinGetPackageRestoreDataSourceTest.cs @@ -26,7 +26,7 @@ public void LoadCatalogs_EmptyPackages_ReturnsNoCatalogs() // Arrange var expectedPackages = new List(); var restoreApplicationInfoList = expectedPackages.Select(p => CreateRestoreApplicationInfo(p.Id).Object).ToList(); - WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny>())).ReturnsAsync(expectedPackages); + WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny>())).ReturnsAsync(expectedPackages); ConfigureRestoreDeviceInfo(RestoreDeviceInfoStatus.Ok, restoreApplicationInfoList); // Act @@ -34,14 +34,14 @@ public void LoadCatalogs_EmptyPackages_ReturnsNoCatalogs() // Assert Assert.AreEqual(0, loadedPackages.Count); - WindowsPackageManager.Verify(wpm => wpm.GetPackagesAsync(It.IsAny>()), Times.Never()); + WindowsPackageManager.Verify(wpm => wpm.GetPackagesAsync(It.IsAny>()), Times.Never()); } [TestMethod] public void LoadCatalogs_ExceptionThrownWhenGettingPackages_ReturnsNoCatalogs() { // Arrange - WindowsPackageManager!.Setup(wpm => wpm.GetPackagesAsync(It.IsAny>())).ThrowsAsync(new FindPackagesException(FindPackagesResultStatus.CatalogError)); + WindowsPackageManager!.Setup(wpm => wpm.GetPackagesAsync(It.IsAny>())).ThrowsAsync(new FindPackagesException(FindPackagesResultStatus.CatalogError)); ConfigureRestoreDeviceInfo(RestoreDeviceInfoStatus.Ok, new List()); // Act @@ -78,7 +78,7 @@ public void LoadCatalogs_OrderedPackages_ReturnsWinGetCatalogWithMatchingInputOr PackageHelper.CreatePackage(packageId2).Object, }; var restoreApplicationInfoList = expectedPackages.Select(p => CreateRestoreApplicationInfo(p.Id).Object).ToList(); - WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny>())).ReturnsAsync(expectedPackages); + WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny>())).ReturnsAsync(expectedPackages); ConfigureRestoreDeviceInfo(RestoreDeviceInfoStatus.Ok, restoreApplicationInfoList); // Act @@ -105,7 +105,7 @@ public void LoadCatalogs_Success_ReturnsWinGetCatalogs() PackageHelper.CreatePackage("mock2").Object, }; var restoreApplicationInfoList = expectedPackages.Select(p => CreateRestoreApplicationInfo(p.Id).Object).ToList(); - WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny>())).ReturnsAsync(expectedPackages); + WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny>())).ReturnsAsync(expectedPackages); ConfigureRestoreDeviceInfo(RestoreDeviceInfoStatus.Ok, restoreApplicationInfoList); // Act @@ -140,7 +140,7 @@ public void LoadCatalogs_ExceptionThrownWhenGettingRestoreApplicationIcon_Return return restoreAppInfo.Object; }).ToList(); - WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny>())).ReturnsAsync(expectedPackages); + WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny>())).ReturnsAsync(expectedPackages); ConfigureRestoreDeviceInfo(RestoreDeviceInfoStatus.Ok, restoreApplicationInfoList); // Act @@ -162,8 +162,8 @@ public void LoadCatalogs_GettingRestoreApplicationIconWithEmptyStream_ReturnsNul PackageHelper.CreatePackage("mock").Object, }; var restoreApplicationInfoList = expectedPackages.Select(p => CreateRestoreApplicationInfo(p.Id, EmptyIconStreamSize).Object).ToList(); - WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny>())).ReturnsAsync(expectedPackages); - WindowsPackageManager.Setup(wpm => wpm.CreateWinGetCatalogPackageUri(It.IsAny())).Returns(new Uri("http://mock")); + WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny>())).ReturnsAsync(expectedPackages); + WindowsPackageManager.Setup(wpm => wpm.CreateWinGetCatalogPackageUri(It.IsAny())).Returns(new WinGetPackageUri("x-ms-winget://mock/mock")); ConfigureRestoreDeviceInfo(RestoreDeviceInfoStatus.Ok, restoreApplicationInfoList); // Act diff --git a/tools/SetupFlow/DevHome.SetupFlow.UnitTest/WinGetPackageUriTests.cs b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/WinGetPackageUriTests.cs new file mode 100644 index 0000000000..7c00b8732d --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/WinGetPackageUriTests.cs @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.SetupFlow.Models; + +namespace DevHome.SetupFlow.UnitTest; + +[TestClass] +public class WinGetPackageUriTests +{ + [TestMethod] + [DataRow("x-ms-winget://catalog/package")] + [DataRow("x-ms-winget://catalog/package?version=1")] + [DataRow("x-ms-winget://catalog/package?not_supported=1")] + public void TryCreate_ValidUri_ReturnsTrue(string packageStringUri) + { + // Arrange + var uri = new Uri(packageStringUri); + + // Act + var result = WinGetPackageUri.TryCreate(uri, out var packageUri); + + // Assert + Assert.IsTrue(result); + Assert.AreEqual("catalog", packageUri.CatalogName); + Assert.AreEqual("package", packageUri.PackageId); + } + + [TestMethod] + [DataRow("x-ms-winget://catalog/package")] + [DataRow("x-ms-winget://catalog/package?version=1")] + [DataRow("x-ms-winget://catalog/package?not_supported=1")] + public void TryCreate_ValidStringUri_ReturnsTrue(string packageStringUri) + { + // Act + var result = WinGetPackageUri.TryCreate(packageStringUri, out var packageUri); + + // Assert + Assert.IsTrue(result); + Assert.AreEqual("catalog", packageUri.CatalogName); + Assert.AreEqual("package", packageUri.PackageId); + } + + [TestMethod] + public void TryCreate_NullStringUri_ReturnsFalse() + { + // Act + var result = WinGetPackageUri.TryCreate(null as string, out var packageUri); + + // Assert + Assert.IsFalse(result); + Assert.IsNull(packageUri); + } + + [TestMethod] + public void TryCreate_NullUri_ReturnsFalse() + { + // Act + var result = WinGetPackageUri.TryCreate(null as Uri, out var packageUri); + + // Assert + Assert.IsFalse(result); + Assert.IsNull(packageUri); + } + + [TestMethod] + public void TryCreate_InvalidUri_ReturnsFalse() + { + // Arrange + var uri = new Uri("https://www.microsoft.com"); + + // Act + var result = WinGetPackageUri.TryCreate(uri, out var packageUri); + + // Assert + Assert.IsFalse(result); + Assert.IsNull(packageUri); + } + + [TestMethod] + public void TryCreate_InvalidStringUri_ReturnsFalse() + { + // Act + var result = WinGetPackageUri.TryCreate("https://www.microsoft.com", out var packageUri); + + // Assert + Assert.IsFalse(result); + Assert.IsNull(packageUri); + } + + [TestMethod] + [DataRow("x-ms-winget://catalog/package?version=1", WinGetPackageUriParameters.All, "x-ms-winget://catalog/package?version=1")] + [DataRow("x-ms-winget://catalog/package?version=1", WinGetPackageUriParameters.Version, "x-ms-winget://catalog/package?version=1")] + [DataRow("x-ms-winget://catalog/package?version=1", WinGetPackageUriParameters.None, "x-ms-winget://catalog/package")] + public void ToString_IncludeParameters_ReturnsUriString( + string packageStringUri, + WinGetPackageUriParameters includeParameters, + string toString) + { + // Arrange + WinGetPackageUri.TryCreate(packageStringUri, out var packageUri); + + // Act + var result = packageUri.ToString(includeParameters); + + // Assert + Assert.AreEqual(toString, result); + } + + [TestMethod] + [DataRow("x-ms-winget://catalog/package?version=1", "x-ms-winget://catalog/package?version=1", WinGetPackageUriParameters.All)] + [DataRow("x-ms-winget://catalog/package?version=1", "x-ms-winget://catalog/package?version=1", WinGetPackageUriParameters.Version)] + [DataRow("x-ms-winget://catalog/package?version=1", "x-ms-winget://catalog/package?version=1", WinGetPackageUriParameters.None)] + [DataRow("x-ms-winget://catalog/package?version=1", "x-ms-winget://catalog/package?version=2", WinGetPackageUriParameters.None)] + public void Equals_Uri_ReturnsTrue(string packageStringUri1, string packageStringUri2, WinGetPackageUriParameters includeParameters) + { + // Arrange + WinGetPackageUri.TryCreate(packageStringUri1, out var packageUri1); + WinGetPackageUri.TryCreate(packageStringUri2, out var packageUri2); + + // Act + var result = packageUri1.Equals(packageUri2, includeParameters); + + // Assert + Assert.IsTrue(result); + } + + [TestMethod] + [DataRow("x-ms-winget://catalog1/package1?version=1", "x-ms-winget://catalog2/package1?version=1", WinGetPackageUriParameters.All)] + [DataRow("x-ms-winget://catalog1/package1?version=1", "x-ms-winget://catalog1/package2?version=1", WinGetPackageUriParameters.All)] + [DataRow("x-ms-winget://catalog1/package1?version=1", "x-ms-winget://catalog1/package1?version=2", WinGetPackageUriParameters.All)] + [DataRow("x-ms-winget://catalog1/package1?version=1", "x-ms-winget://catalog1/package1?version=2", WinGetPackageUriParameters.Version)] + [DataRow("x-ms-winget://catalog1/package1?version=1", "x-ms-winget://catalog2/package1?version=1", WinGetPackageUriParameters.None)] + [DataRow("x-ms-winget://catalog1/package1?version=1", "x-ms-winget://catalog1/package2?version=1", WinGetPackageUriParameters.None)] + public void Equals_Uri_ReturnsFalse(string packageStringUri1, string packageStringUri2, WinGetPackageUriParameters includeParameters) + { + // Arrange + WinGetPackageUri.TryCreate(packageStringUri1, out var packageUri1); + WinGetPackageUri.TryCreate(packageStringUri2, out var packageUri2); + + // Act + var result = packageUri1.Equals(packageUri2, includeParameters); + + // Assert + Assert.IsFalse(result); + } + + [TestMethod] + public void Equals_NullUri_ReturnsFalse() + { + // Arrange + WinGetPackageUri.TryCreate("x-ms-winget://catalog/package", out var packageUri); + + // Act + var result = packageUri.Equals(null as WinGetPackageUri, WinGetPackageUriParameters.All); + + // Assert + Assert.IsFalse(result); + } + + [TestMethod] + public void Equals_UriAndStringUri_ReturnsTrue() + { + // Arrange + WinGetPackageUri.TryCreate("x-ms-winget://catalog/package?version=1", out var packageUri); + + // Act + var result = packageUri.Equals("x-ms-winget://catalog/package?version=1", WinGetPackageUriParameters.All); + + // Assert + Assert.IsTrue(result); + } + + [TestMethod] + [DataRow("x-ms-winget://catalog/package")] + [DataRow("x-ms-winget://catalog/package?version=1")] + public void Constructor_ValidUri_InitializesProperties(string uri) + { + // Act + var packageUri = new WinGetPackageUri(uri); + + // Assert + Assert.AreEqual("catalog", packageUri.CatalogName); + Assert.AreEqual("package", packageUri.PackageId); + Assert.IsNotNull(packageUri.Options); + } + + [TestMethod] + [DataRow("https://catalog/package")] + [DataRow("x-ms-winget://catalog?version=1")] + [DataRow("x-ms-winget://")] + [DataRow("x-ms-winget://?version=1")] + public void Constructor_InvalidUri_ThrowsException(string uri) + { + // Act/Assert + Assert.ThrowsException(() => new WinGetPackageUri(uri)); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Controls/PackageDetailsTooltip.xaml b/tools/SetupFlow/DevHome.SetupFlow/Controls/PackageDetailsTooltip.xaml index d60cbfc508..64827b88d0 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Controls/PackageDetailsTooltip.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Controls/PackageDetailsTooltip.xaml @@ -22,7 +22,7 @@ Style="{ThemeResource BodyStrongTextBlockStyle}" Visibility="{x:Bind Package.IsInstalled}"/> - + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/IWinGetPackage.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/IWinGetPackage.cs index eaf61e0a76..2d96f8e737 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/IWinGetPackage.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/IWinGetPackage.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using DevHome.SetupFlow.Common.WindowsPackageManager; using DevHome.SetupFlow.Services; using Windows.Storage.Streams; @@ -57,11 +58,25 @@ public string Name } /// - /// Gets the version of the package which could be of any format supported - /// by WinGet package manager (e.g. alpha-numeric, 'Unknown', '1-preview, etc...). - /// + /// Gets the installed version of the package or null if the package is not installed /// - public string Version + public string InstalledVersion + { + get; + } + + /// + /// Gets the default version to install + /// + public string DefaultInstallVersion + { + get; + } + + /// + /// Gets the list of available versions for the package + /// + public IReadOnlyList AvailableVersions { get; } @@ -127,12 +142,13 @@ public string InstallationNotes /// /// Windows package manager service /// String resource service - /// WinGet factory + /// Version to install + /// Activity id /// Task object for installing this package InstallPackageTask CreateInstallTask( IWindowsPackageManager wpm, ISetupFlowStringResource stringResource, - WindowsPackageManagerFactory wingetFactory, + string installVersion, Guid activityId); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/InstallPackageTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/InstallPackageTask.cs index 5f64b501c0..beb29f68fd 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/InstallPackageTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/InstallPackageTask.cs @@ -26,6 +26,7 @@ public class InstallPackageTask : ISetupTask private readonly WinGetPackage _package; private readonly ISetupFlowStringResource _stringResource; private readonly Guid _activityId; + private readonly string _installVersion; private InstallResultStatus _installResultStatus; private uint _installerErrorCode; @@ -56,12 +57,14 @@ public InstallPackageTask( IWindowsPackageManager wpm, ISetupFlowStringResource stringResource, WinGetPackage package, + string installVersion, Guid activityId) { _wpm = wpm; _stringResource = stringResource; _package = package; _activityId = activityId; + _installVersion = installVersion; } public TaskMessages GetLoadingMessages() @@ -103,6 +106,7 @@ public InstallPackageTaskArguments GetArguments() { PackageId = _package.Id, CatalogName = _package.CatalogName, + Version = _installVersion, }; } @@ -115,7 +119,8 @@ IAsyncOperation ISetupTask.Execute() { Log.Logger?.ReportInfo(Log.Component.AppManagement, $"Starting installation of package {_package.Id}"); AddMessage(_stringResource.GetLocalized(StringResourceKey.StartingInstallPackageMessage, _package.Id)); - var installResult = await _wpm.InstallPackageAsync(_package); + var packageUri = _package.GetUri(_installVersion); + var installResult = await _wpm.InstallPackageAsync(packageUri); RequiresReboot = installResult.RebootRequired; WasInstallSuccessful = true; @@ -152,7 +157,7 @@ IAsyncOperation ISetupTask.ExecuteAsAdmin(IElevatedComponentO { Log.Logger?.ReportInfo(Log.Component.AppManagement, $"Starting installation with elevation of package {_package.Id}"); AddMessage(_stringResource.GetLocalized(StringResourceKey.StartingInstallPackageMessage, _package.Id)); - var elevatedResult = await elevatedComponentOperation.InstallPackageAsync(_package.Id, _package.CatalogName); + var elevatedResult = await elevatedComponentOperation.InstallPackageAsync(_package.Id, _package.CatalogName, _installVersion); WasInstallSuccessful = elevatedResult.TaskSucceeded; RequiresReboot = elevatedResult.RebootRequired; _installResultStatus = (InstallResultStatus)elevatedResult.Status; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackage.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackage.cs index 97ff5ca0d7..e78d31fbbc 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackage.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackage.cs @@ -2,9 +2,10 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; +using System.Linq; using System.Threading; using DevHome.SetupFlow.Common.Helpers; -using DevHome.SetupFlow.Common.WindowsPackageManager; using DevHome.SetupFlow.Services; using Microsoft.Management.Deployment; using Windows.Storage.Streams; @@ -28,8 +29,10 @@ public WinGetPackage(CatalogPackage package, bool requiresElevated) CatalogName = package.DefaultInstallVersion.PackageCatalog.Info.Name; UniqueKey = new(Id, CatalogId); Name = package.Name; - Version = package.DefaultInstallVersion.Version; - IsInstalled = package.InstalledVersion != null; + AvailableVersions = package.AvailableVersions.Select(v => v.Version).ToList(); + InstalledVersion = FindVersion(package.AvailableVersions, package.InstalledVersion); + DefaultInstallVersion = FindVersion(package.AvailableVersions, package.DefaultInstallVersion); + IsInstalled = InstalledVersion != null; IsElevationRequired = requiresElevated; PackageUrl = GetMetadataValue(package, metadata => new Uri(metadata.PackageUrl), nameof(CatalogPackageMetadata.PackageUrl), null); PublisherUrl = GetMetadataValue(package, metadata => new Uri(metadata.PublisherUrl), nameof(CatalogPackageMetadata.PublisherUrl), null); @@ -47,7 +50,11 @@ public WinGetPackage(CatalogPackage package, bool requiresElevated) public string Name { get; } - public string Version { get; } + public string InstalledVersion { get; } + + public string DefaultInstallVersion { get; } + + public IReadOnlyList AvailableVersions { get; } public bool IsInstalled { get; } @@ -68,8 +75,10 @@ public WinGetPackage(CatalogPackage package, bool requiresElevated) public InstallPackageTask CreateInstallTask( IWindowsPackageManager wpm, ISetupFlowStringResource stringResource, - WindowsPackageManagerFactory wingetFactory, - Guid activityId) => new(wpm, stringResource, this, activityId); + string installVersion, + Guid activityId) => new(wpm, stringResource, this, installVersion, activityId); + + public WinGetPackageUri GetUri(string installVersion) => new(CatalogName, Id, new(installVersion)); /// /// Gets the package metadata from the current culture name (e.g. 'en-US') @@ -93,4 +102,37 @@ private T GetMetadataValue(CatalogPackage package, Func + /// Find the provided version in the list of available versions + /// + /// List of available versions + /// Version to find + /// Package version + private string FindVersion(IReadOnlyList availableVersions, PackageVersionInfo versionInfo) + { + if (versionInfo == null) + { + return null; + } + + // Best effort to find the version in the list of available versions + // If CompareToVersion throws an exception, we default to the version provided + try + { + for (var i = 0; i < availableVersions.Count; i++) + { + if (versionInfo.CompareToVersion(availableVersions[i].Version) == CompareResult.Equal) + { + return availableVersions[i].Version; + } + } + } + catch (Exception e) + { + Log.Logger?.ReportError(Log.Component.AppManagement, $"Unable to validate if the version {versionInfo.Version} is in the list of available versions", e); + } + + return versionInfo.Version; + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUri.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUri.cs new file mode 100644 index 0000000000..5e6eb40cf1 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUri.cs @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; + +namespace DevHome.SetupFlow.Models; + +/// +/// Windows package manager (winget) package Uri +/// +public class WinGetPackageUri +{ + /// + /// Windows package manager custom protocol scheme + /// + private const string Scheme = "x-ms-winget"; + + /// + /// Gets the catalog name + /// + public string CatalogName { get; private set; } + + /// + /// Gets the package id + /// + public string PackageId { get; private set; } + + /// + /// Gets the package options + /// + public WinGetPackageUriOptions Options { get; private set; } + + public WinGetPackageUri(string packageStringUri) + { + if (!ValidUriStructure(packageStringUri, out var packageUri)) + { + throw new UriFormatException($"Invalid winget package string uri {packageStringUri}"); + } + + // Create instance from Uri + InitializeFromUri(packageUri); + } + + public WinGetPackageUri(string catalogName, string packageId, WinGetPackageUriOptions options = null) + { + // Create intermediate Uri + var uriString = CreateValidWinGetPackageUriString(catalogName, packageId, options ?? new(), WinGetPackageUriParameters.All); + var uri = new Uri(uriString); + + // Create instance from Uri + InitializeFromUri(uri); + } + + private WinGetPackageUri(Uri packageUri) + { + // Private constructor expects a valid Uri + Debug.Assert(ValidUriStructure(packageUri), $"Expected a valid winget package Uri {packageUri}"); + InitializeFromUri(packageUri); + } + + /// + /// Create a package Uri from a Uri + /// + /// Uri + /// Output package Uri + /// True if the Uri is a valid winget package Uri + public static bool TryCreate(Uri uri, out WinGetPackageUri packageUri) + { + // Ensure the Uri is a WinGet Uri + if (ValidUriStructure(uri)) + { + packageUri = new(uri); + return true; + } + + packageUri = null; + return false; + } + + /// + /// Create a package Uri from a string + /// + /// String Uri + /// Output package Uri + /// True if the string Uri is a valid winget package Uri + public static bool TryCreate(string stringUri, out WinGetPackageUri packageUri) + { + // Ensure the Uri is a WinGet Uri + if (ValidUriStructure(stringUri, out var uri)) + { + packageUri = new(uri); + return true; + } + + packageUri = null; + return false; + } + + /// + /// Generate a string Uri from the package Uri + /// + /// Include parameters + /// Uri string + public string ToString(WinGetPackageUriParameters includeParameters) + { + return CreateValidWinGetPackageUriString(CatalogName, PackageId, Options, includeParameters); + } + + /// + /// Check if the package Uri is equal to the provided string Uri + /// + /// String Uri + /// Include parameters + /// True if the package Uri is equal to the string Uri + public bool Equals(string stringUri, WinGetPackageUriParameters includeParameters) + { + return TryCreate(stringUri, out var packageUri) && Equals(packageUri, includeParameters); + } + + /// + /// Check if the package Uri is equal to the provided package Uri + /// + /// Package Uri + /// Include parameters + /// True if the package Uri is equal to the Uri + public bool Equals(WinGetPackageUri packageUri, WinGetPackageUriParameters includeParameters) + { + if (packageUri == null) + { + return false; + } + + return CatalogName == packageUri.CatalogName && + PackageId == packageUri.PackageId && + Options.Equals(packageUri.Options, includeParameters); + } + + /// + public override bool Equals(object obj) => Equals(obj as WinGetPackageUri, WinGetPackageUriParameters.All); + + /// + public override string ToString() => ToString(WinGetPackageUriParameters.All); + + /// + public override int GetHashCode() => ToString().GetHashCode(); + + /// + /// Validate the string Uri and create a Uri + /// + /// String Uri + /// Output Uri + /// True if the string Uri is a valid winget package Uri + private static bool ValidUriStructure(string stringUri, out Uri uri) => Uri.TryCreate(stringUri, UriKind.Absolute, out uri) && ValidUriStructure(uri); + + /// + /// Validate the Uri structure + /// + /// Uri + /// True if the Uri is a valid winget package Uri + private static bool ValidUriStructure(Uri uri) => uri != null && uri.Scheme == Scheme && uri.Segments.Length == 2; + + /// + /// Initialize the package Uri from a valid Uri + /// + /// Valid package Uri + private void InitializeFromUri(Uri validUri) + { + Debug.Assert(ValidUriStructure(validUri), $"Expected a valid winget package Uri {validUri}"); + CatalogName = validUri.Host; + PackageId = validUri.Segments[1]; + Options = new(validUri); + } + + /// + /// Create a valid Uri string + /// + /// Catalog name + /// Package id + /// Options + /// Include parameters + /// Valid Uri string + private static string CreateValidWinGetPackageUriString(string catalogName, string packageId, WinGetPackageUriOptions options, WinGetPackageUriParameters includeParameters) + { + var queryString = options.ToString(includeParameters); + var uriString = $"{Scheme}://{catalogName}/{packageId}{queryString}"; + Debug.Assert(ValidUriStructure(uriString, out var _), $"Expected to generate a valid winget package Uri {uriString}"); + return uriString; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUriOptions.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUriOptions.cs new file mode 100644 index 0000000000..dceee49b63 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUriOptions.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Web; + +namespace DevHome.SetupFlow.Models; + +/// +/// Windows package manager (winget) package Uri options +/// +public sealed class WinGetPackageUriOptions +{ + // Query parameter names + private const string VersionQueryParameter = "version"; + + public WinGetPackageUriOptions(string version = null) + { + Version = version; + } + + internal WinGetPackageUriOptions(Uri packageUri) + { + var queryParams = HttpUtility.ParseQueryString(packageUri.Query); + Version = queryParams.Get(VersionQueryParameter); + } + + /// + /// Gets the version of the package + /// + public string Version { get; } + + /// + /// Gets a value indicating whether the is specified + /// + public bool VersionSpecified => !string.IsNullOrWhiteSpace(Version); + + /// + /// Returns the string representation of the options + /// + /// The parameters to include in the string + /// Options as a string + public string ToString(WinGetPackageUriParameters includeParameters) + { + var queryParams = HttpUtility.ParseQueryString(string.Empty); + + // Add version + if (includeParameters.HasFlag(WinGetPackageUriParameters.Version) && VersionSpecified) + { + queryParams.Add(VersionQueryParameter, Version); + } + + return queryParams.Count > 0 ? $"?{queryParams}" : string.Empty; + } + + /// + /// Compares the options with another options + /// + /// Target options to compare + /// The parameters to include in the comparison + /// True if the options are equal; otherwise, false + public bool Equals(WinGetPackageUriOptions options, WinGetPackageUriParameters includeParameters) + { + if (options == null) + { + return false; + } + + // Check version + if (includeParameters.HasFlag(WinGetPackageUriParameters.Version) && Version != options.Version) + { + return false; + } + + return true; + } + + /// + public override string ToString() => ToString(WinGetPackageUriParameters.All); + + /// + public override bool Equals(object obj) => Equals(obj as WinGetPackageUriOptions, WinGetPackageUriParameters.All); + + /// + public override int GetHashCode() => ToString().GetHashCode(); +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUriParameters.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUriParameters.cs new file mode 100644 index 0000000000..806c00abb4 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackageUriParameters.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace DevHome.SetupFlow.Models; + +[Flags] +public enum WinGetPackageUriParameters +{ + None = 0, + Version = 1 << 0, + + // Add all parameters here + All = Version, +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/IWindowsPackageManager.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/IWindowsPackageManager.cs index 23079891f7..cccbb46b85 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/IWindowsPackageManager.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/IWindowsPackageManager.cs @@ -22,10 +22,10 @@ public interface IWindowsPackageManager public Task InitializeAsync(); /// - public Task InstallPackageAsync(IWinGetPackage package); + public Task InstallPackageAsync(WinGetPackageUri packageUri); /// - public Task> GetPackagesAsync(IList packageUris); + public Task> GetPackagesAsync(IList packageUris); /// public Task> SearchAsync(string query, uint limit); @@ -46,14 +46,14 @@ public interface IWindowsPackageManager public bool IsWinGetPackage(IWinGetPackage package); /// - public Uri CreatePackageUri(IWinGetPackage package); + public WinGetPackageUri CreatePackageUri(IWinGetPackage package); /// - public Uri CreateWinGetCatalogPackageUri(string packageId); + public WinGetPackageUri CreateWinGetCatalogPackageUri(string packageId); /// - public Uri CreateMsStoreCatalogPackageUri(string packageId); + public WinGetPackageUri CreateMsStoreCatalogPackageUri(string packageId); /// - public Uri CreateCustomCatalogPackageUri(string packageId, string catalogName); + public WinGetPackageUri CreateCustomCatalogPackageUri(string packageId, string catalogName); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetOperations.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetOperations.cs index b2c220ec27..b2442d87d9 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetOperations.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetOperations.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Collections.Generic; using System.Threading.Tasks; using DevHome.SetupFlow.Models; @@ -11,10 +10,10 @@ namespace DevHome.SetupFlow.Services.WinGet.Operations; internal interface IWinGetOperations { /// " - public Task InstallPackageAsync(IWinGetPackage package); + public Task InstallPackageAsync(WinGetPackageUri packageUri); /// " - public Task> GetPackagesAsync(IList packageUris); + public Task> GetPackagesAsync(IList packageUris); /// " public Task> SearchAsync(string query, uint limit); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetPackageCache.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetPackageCache.cs index 35c0433158..402f02f3c7 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetPackageCache.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetPackageCache.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Collections.Generic; using DevHome.SetupFlow.Models; @@ -18,7 +17,7 @@ internal interface IWinGetPackageCache /// Package URIs to find /// Output package URIs not found /// List of packages found - public IList GetPackages(IEnumerable packageUris, out IEnumerable packageUrisNotFound); + public IList GetPackages(IEnumerable packageUris, out IEnumerable packageUrisNotFound); /// /// Try to get a package in the cache. @@ -26,7 +25,7 @@ internal interface IWinGetPackageCache /// Package URI to find /// Output package /// True if the package was found, false otherwise. - public bool TryGetPackage(Uri packageUri, out IWinGetPackage package); + public bool TryGetPackage(WinGetPackageUri packageUri, out IWinGetPackage package); /// /// Try to add a package to the cache. @@ -34,7 +33,7 @@ internal interface IWinGetPackageCache /// Package URI to add /// Package to add /// True if the package was added, false otherwise. - public bool TryAddPackage(Uri packageUri, IWinGetPackage package); + public bool TryAddPackage(WinGetPackageUri packageUri, IWinGetPackage package); /// /// Clear the cache. diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetPackageInstaller.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetPackageInstaller.cs index 6aa290e03f..f4c8cc6a56 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetPackageInstaller.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetPackageInstaller.cs @@ -13,6 +13,7 @@ internal interface IWinGetPackageInstaller /// /// Catalog from which to install the package /// Package id to install + /// Version of the package to install /// Result of the installation - public Task InstallPackageAsync(WinGetCatalog catalog, string packageId); + public Task InstallPackageAsync(WinGetCatalog catalog, string packageId, string version = null); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetProtocolParser.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetProtocolParser.cs index d479ae1406..c26026c934 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetProtocolParser.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/IWinGetProtocolParser.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Threading.Tasks; using DevHome.SetupFlow.Models; @@ -14,21 +13,21 @@ internal interface IWinGetProtocolParser /// /// Package /// Package uri - public Uri CreatePackageUri(IWinGetPackage package); + public WinGetPackageUri CreatePackageUri(IWinGetPackage package); /// /// Create a winget catalog package uri from a package id /// /// Package id /// Package uri - public Uri CreateWinGetCatalogPackageUri(string packageId); + public WinGetPackageUri CreateWinGetCatalogPackageUri(string packageId); /// /// Create a Microsoft store catalog package uri from a package id /// /// Package id /// Package uri - public Uri CreateMsStoreCatalogPackageUri(string packageId); + public WinGetPackageUri CreateMsStoreCatalogPackageUri(string packageId); /// /// Create a custom catalog package uri from a package id and catalog name @@ -36,21 +35,14 @@ internal interface IWinGetProtocolParser /// Package id /// Catalog name /// Package uri - public Uri CreateCustomCatalogPackageUri(string packageId, string catalogName); - - /// - /// Get the package id and catalog from a package uri - /// - /// Input package uri - /// Package id and catalog, or null if the URI protocol is inaccurate - public WinGetProtocolParserResult ParsePackageUri(Uri packageUri); + public WinGetPackageUri CreateCustomCatalogPackageUri(string packageId, string catalogName); /// /// Resolve a catalog from a parser result /// - /// Parser result + /// Package uri /// Catalog - public Task ResolveCatalogAsync(WinGetProtocolParserResult result); + public Task ResolveCatalogAsync(WinGetPackageUri packageUri); /// /// Create a package uri from a package id and catalog @@ -58,5 +50,5 @@ internal interface IWinGetProtocolParser /// Package id /// Catalog /// Package uri - public Uri CreatePackageUri(string packageId, WinGetCatalog catalog); + public WinGetPackageUri CreatePackageUri(string packageId, WinGetCatalog catalog); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/IWinGetGetPackageOperation.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/IWinGetGetPackageOperation.cs index 4f313441cd..9ce3b7eaa9 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/IWinGetGetPackageOperation.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/IWinGetGetPackageOperation.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Collections.Generic; using System.Threading.Tasks; using DevHome.SetupFlow.Models; @@ -15,5 +14,5 @@ internal interface IWinGetGetPackageOperation /// /// List of package uri /// List of winget package matches - public Task> GetPackagesAsync(IList packageUris); + public Task> GetPackagesAsync(IList packageUris); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/IWinGetInstallOperation.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/IWinGetInstallOperation.cs index e041be8dd0..5c0c51fa1a 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/IWinGetInstallOperation.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/IWinGetInstallOperation.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Threading.Tasks; using DevHome.SetupFlow.Models; @@ -9,17 +8,10 @@ namespace DevHome.SetupFlow.Services.WinGet.Operations; internal interface IWinGetInstallOperation { - /// - /// Install a package on the user's machine. - /// - /// Package to install - /// Install package result - public Task InstallPackageAsync(IWinGetPackage package); - /// /// Installs a package from a URI. /// /// Uri of the package to install. /// Result of the installation. - public Task InstallPackageAsync(Uri packageUri); + public Task InstallPackageAsync(WinGetPackageUri packageUri); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/WinGetGetPackageOperation.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/WinGetGetPackageOperation.cs index 811464ee78..681918dfbf 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/WinGetGetPackageOperation.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/WinGetGetPackageOperation.cs @@ -33,7 +33,7 @@ public WinGetGetPackageOperation( } /// - public async Task> GetPackagesAsync(IList packageUris) + public async Task> GetPackagesAsync(IList packageUris) { // Remove duplicates (optimization to prevent querying the same package multiple times) var distinctPackageUris = packageUris.Distinct(); @@ -45,7 +45,8 @@ public async Task> GetPackagesAsync(IList packageUris // Get packages grouped by catalog var getPackagesTasks = new List>>(); - foreach (var parsedUrisGroup in GroupParsedUrisByCatalog(packageUrisToQuery)) + var groupedParsedUris = packageUrisToQuery.GroupBy(p => p.CatalogName).Select(p => p.ToList()).ToList(); + foreach (var parsedUrisGroup in groupedParsedUris) { if (parsedUrisGroup.Count != 0) { @@ -55,7 +56,7 @@ public async Task> GetPackagesAsync(IList packageUris // All parsed URIs in the group have the same catalog, resolve catalog from the first entry var firstParsedUri = parsedUrisGroup.First(); var packageIds = parsedUrisGroup.Select(p => p.PackageId).ToHashSet(); - Log.Logger?.ReportInfo(Log.Component.AppManagement, $"Getting packages [{string.Join(", ", packageIds)}] from parsed uri catalog name: {firstParsedUri.CatalogUriName}"); + Log.Logger?.ReportInfo(Log.Component.AppManagement, $"Getting packages [{string.Join(", ", packageIds)}] from parsed uri catalog name: {firstParsedUri.CatalogName}"); // Get packages from the catalog var catalog = await _protocolParser.ResolveCatalogAsync(firstParsedUri); @@ -75,13 +76,13 @@ public async Task> GetPackagesAsync(IList packageUris var unorderedPackagesMap = getPackagesTasks .SelectMany(p => p.Result) .Concat(cachedPackages) - .ToDictionary(p => _protocolParser.CreatePackageUri(p), p => p); + .ToDictionary(p => _protocolParser.CreatePackageUri(p).ToString(WinGetPackageUriParameters.None), p => p); // Order packages by the order of the input URIs using a dictionary var orderedPackages = new List(); foreach (var packageUri in packageUris) { - if (unorderedPackagesMap.TryGetValue(packageUri, out var package)) + if (unorderedPackagesMap.TryGetValue(packageUri.ToString(WinGetPackageUriParameters.None), out var package)) { orderedPackages.Add(package); } @@ -93,31 +94,4 @@ public async Task> GetPackagesAsync(IList packageUris return orderedPackages; } - - /// - /// Group packages by their catalogs - /// - /// Package URIs - /// Dictionary of package ids by catalog - private List> GroupParsedUrisByCatalog(IEnumerable packageUriSet) - { - var parsedUris = new List(); - - // 1. Parse all package URIs and log invalid ones - foreach (var packageUri in packageUriSet) - { - var uriInfo = _protocolParser.ParsePackageUri(packageUri); - if (uriInfo != null) - { - parsedUris.Add(uriInfo); - } - else - { - Log.Logger?.ReportWarn(Log.Component.AppManagement, $"Failed to get URI details from '{packageUri}'"); - } - } - - // 2. Group package ids by catalog - return parsedUris.GroupBy(p => p.CatalogUriName).Select(p => p.ToList()).ToList(); - } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/WinGetInstallOperation.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/WinGetInstallOperation.cs index a004159b61..7d42fc27f3 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/WinGetInstallOperation.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/Operations/WinGetInstallOperation.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Threading.Tasks; using DevHome.SetupFlow.Models; @@ -30,28 +29,12 @@ public WinGetInstallOperation( } /// - public async Task InstallPackageAsync(IWinGetPackage package) + public async Task InstallPackageAsync(WinGetPackageUri packageUri) { return await _recovery.DoWithRecoveryAsync(async () => { - var catalog = await _catalogConnector.GetPackageCatalogAsync(package); - return await _packageInstaller.InstallPackageAsync(catalog, package.Id); - }); - } - - /// - public async Task InstallPackageAsync(Uri packageUri) - { - var parsedPackageUri = _protocolParser.ParsePackageUri(packageUri); - if (parsedPackageUri == null) - { - throw new ArgumentException($"Invalid package URI ${packageUri}"); - } - - return await _recovery.DoWithRecoveryAsync(async () => - { - var catalog = await _protocolParser.ResolveCatalogAsync(parsedPackageUri); - return await _packageInstaller.InstallPackageAsync(catalog, parsedPackageUri.PackageId); + var catalog = await _protocolParser.ResolveCatalogAsync(packageUri); + return await _packageInstaller.InstallPackageAsync(catalog, packageUri.PackageId, packageUri.Options.Version); }); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetOperations.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetOperations.cs index 4145b4fae3..8ddc086943 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetOperations.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetOperations.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Collections.Generic; using System.Threading.Tasks; using DevHome.SetupFlow.Models; @@ -25,10 +24,10 @@ public WinGetOperations( } /// - public async Task InstallPackageAsync(IWinGetPackage package) => await _installOperation.InstallPackageAsync(package); + public async Task InstallPackageAsync(WinGetPackageUri packageUri) => await _installOperation.InstallPackageAsync(packageUri); /// - public async Task> GetPackagesAsync(IList packageUris) => await _getPackageOperation.GetPackagesAsync(packageUris); + public async Task> GetPackagesAsync(IList packageUris) => await _getPackageOperation.GetPackagesAsync(packageUris); /// public async Task> SearchAsync(string query, uint limit) => await _searchOperation.SearchAsync(query, limit); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetPackageCache.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetPackageCache.cs index 94b327b3af..7e21237f27 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetPackageCache.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetPackageCache.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Collections.Generic; using DevHome.SetupFlow.Models; @@ -12,17 +11,17 @@ namespace DevHome.SetupFlow.Services.WinGet; /// internal sealed class WinGetPackageCache : IWinGetPackageCache { - private readonly Dictionary _cache = new(); + private readonly Dictionary _cache = new(); private readonly object _lock = new(); /// - public IList GetPackages(IEnumerable packageUris, out IEnumerable packageUrisNotFound) + public IList GetPackages(IEnumerable packageUris, out IEnumerable packageUrisNotFound) { // Lock to ensure all packages fetched are from the same cache state lock (_lock) { var foundPackages = new List(); - var notFoundPackageUris = new List(); + var notFoundPackageUris = new List(); foreach (var packageUri in packageUris) { @@ -42,11 +41,12 @@ public IList GetPackages(IEnumerable packageUris, out IEnum } /// - public bool TryGetPackage(Uri packageUri, out IWinGetPackage package) + public bool TryGetPackage(WinGetPackageUri packageUri, out IWinGetPackage package) { lock (_lock) { - if (_cache.TryGetValue(packageUri, out package)) + var key = CreateKey(packageUri); + if (_cache.TryGetValue(key, out package)) { return true; } @@ -57,11 +57,12 @@ public bool TryGetPackage(Uri packageUri, out IWinGetPackage package) } /// - public bool TryAddPackage(Uri packageUri, IWinGetPackage package) + public bool TryAddPackage(WinGetPackageUri packageUri, IWinGetPackage package) { lock (_lock) { - return _cache.TryAdd(packageUri, package); + var key = CreateKey(packageUri); + return _cache.TryAdd(key, package); } } @@ -73,4 +74,14 @@ public void Clear() _cache.Clear(); } } + + /// + /// Create a key from a package URI + /// + /// Package URI + /// Unique key from a package URI + private string CreateKey(WinGetPackageUri packageUri) + { + return packageUri.ToString(WinGetPackageUriParameters.None); + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetPackageInstaller.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetPackageInstaller.cs index a2b8908228..6852bbea74 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetPackageInstaller.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetPackageInstaller.cs @@ -28,7 +28,7 @@ public WinGetPackageInstaller(WindowsPackageManagerFactory wingetFactory, IWinGe } /// - public async Task InstallPackageAsync(WinGetCatalog catalog, string packageId) + public async Task InstallPackageAsync(WinGetCatalog catalog, string packageId, string version = null) { if (catalog == null) { @@ -45,7 +45,7 @@ public async Task InstallPackageAsync(WinGetCatalog catalo // 2. Install package Log.Logger?.ReportInfo(Log.Component.AppManagement, $"Starting package installation for {packageId} from catalog {catalog.GetDescriptiveName()}"); - var installResult = await InstallPackageInternalAsync(package); + var installResult = await InstallPackageInternalAsync(package, version); var extendedErrorCode = installResult.ExtendedErrorCode?.HResult ?? HRESULT.S_OK; var installErrorCode = installResult.GetValueOrDefault(res => res.InstallerErrorCode, HRESULT.S_OK); // WPM API V4 @@ -69,11 +69,42 @@ public async Task InstallPackageAsync(WinGetCatalog catalo /// /// Package to install /// Install result - private async Task InstallPackageInternalAsync(CatalogPackage package) + private async Task InstallPackageInternalAsync(CatalogPackage package, string version = null) { var installOptions = _wingetFactory.CreateInstallOptions(); installOptions.PackageInstallMode = PackageInstallMode.Silent; + if (!string.IsNullOrWhiteSpace(version)) + { + installOptions.PackageVersionId = FindVersionOrThrow(package, version); + } + else + { + Log.Logger?.ReportInfo(Log.Component.AppManagement, $"Install version not specified. Falling back to default install version {package.DefaultInstallVersion.Version}"); + } + var packageManager = _wingetFactory.CreatePackageManager(); return await packageManager.InstallPackageAsync(package, installOptions).AsTask(); } + + /// + /// Find a specific version in the list of available versions for a package. + /// + /// Target package + /// Version to find + /// Specified version + /// Exception thrown if the specified version was not found + private PackageVersionId FindVersionOrThrow(CatalogPackage package, string version) + { + // Find the version in the list of available versions + for (var i = 0; i < package.AvailableVersions.Count; i++) + { + if (package.AvailableVersions[i].Version == version) + { + return package.AvailableVersions[i]; + } + } + + Log.Logger?.ReportError(Log.Component.AppManagement, $"Specified install version was not found {version}."); + throw new InstallPackageException(InstallResultStatus.InvalidOptions, InstallPackageException.InstallErrorInvalidParameter, HRESULT.S_OK); + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetProtocolParser.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetProtocolParser.cs index bba9c9bbd0..f8502743dc 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetProtocolParser.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetProtocolParser.cs @@ -20,11 +20,6 @@ public WinGetProtocolParser(IWinGetCatalogConnector catalogConnector) _catalogConnector = catalogConnector; } - /// - /// Windows package manager custom protocol scheme - /// - private const string Scheme = "x-ms-winget"; - /// /// Reserved URI name for the WinGet catalog /// @@ -36,22 +31,9 @@ public WinGetProtocolParser(IWinGetCatalogConnector catalogConnector) private const string ReservedMsStoreCatalogURIName = "msstore"; /// - public WinGetProtocolParserResult ParsePackageUri(Uri packageUri) + public async Task ResolveCatalogAsync(WinGetPackageUri packageUri) { - if (packageUri.Scheme == Scheme && packageUri.Segments.Length == 2) - { - var packageId = packageUri.Segments[1]; - var catalogUriName = packageUri.Host; - return new(packageId, catalogUriName); - } - - return null; - } - - /// - public async Task ResolveCatalogAsync(WinGetProtocolParserResult result) - { - var catalogName = result.CatalogUriName; + var catalogName = packageUri.CatalogName; // 'winget' catalog if (catalogName == ReservedWingetCatalogURIName) @@ -70,16 +52,16 @@ public async Task ResolveCatalogAsync(WinGetProtocolParserResult } /// - public Uri CreateWinGetCatalogPackageUri(string packageId) => new($"{Scheme}://{ReservedWingetCatalogURIName}/{packageId}"); + public WinGetPackageUri CreateWinGetCatalogPackageUri(string packageId) => new(ReservedWingetCatalogURIName, packageId); /// - public Uri CreateMsStoreCatalogPackageUri(string packageId) => new($"{Scheme}://{ReservedMsStoreCatalogURIName}/{packageId}"); + public WinGetPackageUri CreateMsStoreCatalogPackageUri(string packageId) => new(ReservedMsStoreCatalogURIName, packageId); /// - public Uri CreateCustomCatalogPackageUri(string packageId, string catalogName) => new($"{Scheme}://{catalogName}/{packageId}"); + public WinGetPackageUri CreateCustomCatalogPackageUri(string packageId, string catalogName) => new(catalogName, packageId); /// - public Uri CreatePackageUri(string packageId, WinGetCatalog catalog) + public WinGetPackageUri CreatePackageUri(string packageId, WinGetCatalog catalog) { return catalog.Type switch { @@ -91,10 +73,5 @@ public Uri CreatePackageUri(string packageId, WinGetCatalog catalog) } /// - public Uri CreatePackageUri(IWinGetPackage package) - { - return CreateCustomCatalogPackageUri(package.Id, package.CatalogName); - } + public WinGetPackageUri CreatePackageUri(IWinGetPackage package) => CreateCustomCatalogPackageUri(package.Id, package.CatalogName); } - -public record class WinGetProtocolParserResult(string PackageId, string CatalogUriName); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetFeaturedApplicationsDataSource.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetFeaturedApplicationsDataSource.cs index 81979549d3..abc9939f8f 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetFeaturedApplicationsDataSource.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetFeaturedApplicationsDataSource.cs @@ -142,18 +142,18 @@ private async Task LoadCatalogAsync(IFeaturedApplicationsGroup g /// /// List of package URI strings /// List of package URIs - private List ParseURIs(IReadOnlyList uriStrings) + private List ParseURIs(IReadOnlyList uriStrings) { - var result = new List(); - foreach (var app in uriStrings) + var result = new List(); + foreach (var uriString in uriStrings) { - if (Uri.TryCreate(app, UriKind.Absolute, out var uri)) + if (WinGetPackageUri.TryCreate(uriString, out var packageUri)) { - result.Add(uri); + result.Add(packageUri); } else { - Log.Logger?.ReportWarn(Log.Component.AppManagement, $"Invalid package uri: {app}"); + Log.Logger?.ReportWarn(Log.Component.AppManagement, $"Invalid package uri: {uriString}"); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageDataSource.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageDataSource.cs index 955695bee6..0905c49155 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageDataSource.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageDataSource.cs @@ -44,7 +44,7 @@ public WinGetPackageDataSource(IWindowsPackageManager wpm) /// Input type /// List of package URIs /// List of packages - protected async Task> GetPackagesAsync(IList packageUris) + protected async Task> GetPackagesAsync(IList packageUris) { List result = new(); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageJsonDataSource.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageJsonDataSource.cs index b45274b324..2712229692 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageJsonDataSource.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageJsonDataSource.cs @@ -1,108 +1,138 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; -using DevHome.Common.Extensions; -using DevHome.SetupFlow.Common.Helpers; -using DevHome.SetupFlow.Models; -using Windows.Storage; -using Windows.Storage.Streams; - -namespace DevHome.SetupFlow.Services; - -/// -/// Class for loading package catalogs from a JSON data source -/// -public class WinGetPackageJsonDataSource : WinGetPackageDataSource -{ - /// - /// Class for deserializing a JSON winget package - /// - private sealed class JsonWinGetPackage - { - public Uri Uri { get; set; } - - public string Icon { get; set; } - } - - /// - /// Class for deserializing a JSON package catalog with package ids from - /// winget - /// - private sealed class JsonWinGetPackageCatalog - { - public string NameResourceKey { get; set; } - - public string DescriptionResourceKey { get; set; } - - public IList WinGetPackages { get; set; } - } - - private readonly ISetupFlowStringResource _stringResource; - private readonly string _fileName; - private readonly JsonSerializerOptions jsonSerializerOptions = new() { ReadCommentHandling = JsonCommentHandling.Skip }; - private IList _jsonCatalogs = new List(); - - public override int CatalogCount => _jsonCatalogs.Count; - - public WinGetPackageJsonDataSource( - ISetupFlowStringResource stringResource, - IWindowsPackageManager wpm, - string fileName) - : base(wpm) - { - _stringResource = stringResource; - _fileName = fileName; - } - - public async override Task InitializeAsync() - { - // Open and deserialize JSON file - Log.Logger?.ReportInfo(Log.Component.AppManagement, $"Reading package list from JSON file {_fileName}"); - using var fileStream = File.OpenRead(_fileName); - - _jsonCatalogs = await JsonSerializer.DeserializeAsync>(fileStream, jsonSerializerOptions); - } - - public async override Task> LoadCatalogsAsync() - { - var result = new List(); - foreach (var jsonCatalog in _jsonCatalogs) - { - var packageCatalog = await LoadCatalogAsync(jsonCatalog); - if (packageCatalog != null) - { - result.Add(packageCatalog); - } - } - - return result; - } - - /// - /// Load a package catalog with the list of winget packages sorted based on - /// the input JSON catalog - /// - /// JSON catalog - /// Package catalog - private async Task LoadCatalogAsync(JsonWinGetPackageCatalog jsonCatalog) - { - var catalogName = _stringResource.GetLocalized(jsonCatalog.NameResourceKey); - Log.Logger?.ReportInfo(Log.Component.AppManagement, $"Attempting to read JSON package catalog {catalogName}"); - - try - { - var packages = await GetPackagesAsync(jsonCatalog.WinGetPackages.Select(p => p.Uri).ToList()); +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using DevHome.Common.Extensions; +using DevHome.SetupFlow.Common.Helpers; +using DevHome.SetupFlow.Models; +using Windows.Storage; +using Windows.Storage.Streams; + +namespace DevHome.SetupFlow.Services; + +/// +/// Class for loading package catalogs from a JSON data source +/// +public class WinGetPackageJsonDataSource : WinGetPackageDataSource +{ + /// + /// Class for deserializing a JSON winget package + /// + private sealed class JsonWinGetPackage + { + public Uri Uri { get; set; } + + public string Icon { get; set; } + + public WinGetPackageUri GetPackageUri() + { + if (WinGetPackageUri.TryCreate(Uri, out var packageUri)) + { + return packageUri; + } + + return null; + } + } + + /// + /// Class for deserializing a JSON package catalog with package ids from + /// winget + /// + private sealed class JsonWinGetPackageCatalog + { + public string NameResourceKey { get; set; } + + public string DescriptionResourceKey { get; set; } + + public IList WinGetPackages { get; set; } + } + + private readonly ISetupFlowStringResource _stringResource; + private readonly string _fileName; + private readonly JsonSerializerOptions jsonSerializerOptions = new() { ReadCommentHandling = JsonCommentHandling.Skip }; + private IList _jsonCatalogs = new List(); + + public override int CatalogCount => _jsonCatalogs.Count; + + public WinGetPackageJsonDataSource( + ISetupFlowStringResource stringResource, + IWindowsPackageManager wpm, + string fileName) + : base(wpm) + { + _stringResource = stringResource; + _fileName = fileName; + } + + public async override Task InitializeAsync() + { + // Open and deserialize JSON file + Log.Logger?.ReportInfo(Log.Component.AppManagement, $"Reading package list from JSON file {_fileName}"); + using var fileStream = File.OpenRead(_fileName); + + _jsonCatalogs = await JsonSerializer.DeserializeAsync>(fileStream, jsonSerializerOptions); + } + + public async override Task> LoadCatalogsAsync() + { + var result = new List(); + foreach (var jsonCatalog in _jsonCatalogs) + { + var packageCatalog = await LoadCatalogAsync(jsonCatalog); + if (packageCatalog != null) + { + result.Add(packageCatalog); + } + } + + return result; + } + + private List GetPackageUris(IList jsonPackages) + { + var result = new List(); + foreach (var jsonPackage in jsonPackages) + { + var packageUri = jsonPackage.GetPackageUri(); + if (packageUri != null) + { + result.Add(packageUri); + } + else + { + Log.Logger?.ReportWarn(Log.Component.AppManagement, $"Skipping {jsonPackage.Uri} because it is not a valid winget package uri"); + } + } + + return result; + } + + /// + /// Load a package catalog with the list of winget packages sorted based on + /// the input JSON catalog + /// + /// JSON catalog + /// Package catalog + private async Task LoadCatalogAsync(JsonWinGetPackageCatalog jsonCatalog) + { + var catalogName = _stringResource.GetLocalized(jsonCatalog.NameResourceKey); + Log.Logger?.ReportInfo(Log.Component.AppManagement, $"Attempting to read JSON package catalog {catalogName}"); + + try + { + var packageUris = GetPackageUris(jsonCatalog.WinGetPackages); + var packages = await GetPackagesAsync(packageUris); Log.Logger?.ReportInfo(Log.Component.AppManagement, $"Obtaining icon information for JSON packages: [{string.Join(", ", packages.Select(p => $"({p.Name}, {p.CatalogName})"))}]"); foreach (var package in packages) { var packageUri = WindowsPackageManager.CreatePackageUri(package); - var jsonPackage = jsonCatalog.WinGetPackages.FirstOrDefault(p => packageUri == p.Uri); + var jsonPackage = jsonCatalog.WinGetPackages.FirstOrDefault(p => packageUri.Equals(p.GetPackageUri(), WinGetPackageUriParameters.None)); if (jsonPackage != null) { var icon = await GetJsonApplicationIconAsync(jsonPackage); @@ -111,51 +141,51 @@ private async Task LoadCatalogAsync(JsonWinGetPackageCatalog jso } } - if (packages.Any()) - { - return new PackageCatalog() - { - Name = catalogName, - Description = _stringResource.GetLocalized(jsonCatalog.DescriptionResourceKey), - Packages = packages.ToReadOnlyCollection(), - }; - } - else - { - Log.Logger?.ReportWarn(Log.Component.AppManagement, $"JSON package catalog [{catalogName}] is empty"); - } - } - catch (Exception e) - { - Log.Logger?.ReportError(Log.Component.AppManagement, $"Error loading packages from winget catalog.", e); - } - - return null; - } - - private async Task GetJsonApplicationIconAsync(JsonWinGetPackage package) - { - try - { - if (!string.IsNullOrEmpty(package.Icon)) - { - // Load icon from application assets - var iconFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri(package.Icon)); - var icon = await iconFile.OpenAsync(FileAccessMode.Read); - - // Ensure stream is not empty to prevent rendering an empty image - if (icon.Size > 0) - { - return icon; - } - } - } - catch (Exception e) - { - Log.Logger?.ReportError(Log.Component.AppManagement, $"Failed to get icon for JSON package {package.Uri}.", e); - } - - Log.Logger?.ReportWarn(Log.Component.AppManagement, $"No icon found for JSON package {package.Uri}. A default one will be provided."); - return null; - } -} + if (packages.Any()) + { + return new PackageCatalog() + { + Name = catalogName, + Description = _stringResource.GetLocalized(jsonCatalog.DescriptionResourceKey), + Packages = packages.ToReadOnlyCollection(), + }; + } + else + { + Log.Logger?.ReportWarn(Log.Component.AppManagement, $"JSON package catalog [{catalogName}] is empty"); + } + } + catch (Exception e) + { + Log.Logger?.ReportError(Log.Component.AppManagement, $"Error loading packages from winget catalog.", e); + } + + return null; + } + + private async Task GetJsonApplicationIconAsync(JsonWinGetPackage package) + { + try + { + if (!string.IsNullOrEmpty(package.Icon)) + { + // Load icon from application assets + var iconFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri(package.Icon)); + var icon = await iconFile.OpenAsync(FileAccessMode.Read); + + // Ensure stream is not empty to prevent rendering an empty image + if (icon.Size > 0) + { + return icon; + } + } + } + catch (Exception e) + { + Log.Logger?.ReportError(Log.Component.AppManagement, $"Failed to get icon for JSON package {package.Uri}.", e); + } + + Log.Logger?.ReportWarn(Log.Component.AppManagement, $"No icon found for JSON package {package.Uri}. A default one will be provided."); + return null; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageRestoreDataSource.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageRestoreDataSource.cs index 3330ad8e85..de0518f02e 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageRestoreDataSource.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageRestoreDataSource.cs @@ -157,7 +157,7 @@ private string GetDescription() /// Application information /// Package URI /// All restored applications are from winget catalog - private Uri GetPackageUri(IRestoreApplicationInfo appInfo) + private WinGetPackageUri GetPackageUri(IRestoreApplicationInfo appInfo) { return WindowsPackageManager.CreateWinGetCatalogPackageUri(appInfo.Id); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WindowsPackageManager.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WindowsPackageManager.cs index 9073f29ee1..3945c48371 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WindowsPackageManager.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WindowsPackageManager.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Collections.Generic; using System.Threading.Tasks; using DevHome.SetupFlow.Models; @@ -46,10 +45,10 @@ public async Task InitializeAsync() } /// - public async Task InstallPackageAsync(IWinGetPackage package) => await _operations.InstallPackageAsync(package); + public async Task InstallPackageAsync(WinGetPackageUri packageUri) => await _operations.InstallPackageAsync(packageUri); /// - public async Task> GetPackagesAsync(IList packageUris) => await _operations.GetPackagesAsync(packageUris); + public async Task> GetPackagesAsync(IList packageUris) => await _operations.GetPackagesAsync(packageUris); /// public async Task> SearchAsync(string query, uint limit) => await _operations.SearchAsync(query, limit); @@ -70,14 +69,14 @@ public async Task InitializeAsync() public bool IsWinGetPackage(IWinGetPackage package) => _catalogConnector.IsWinGetPackage(package); /// - public Uri CreatePackageUri(IWinGetPackage package) => _protocolParser.CreatePackageUri(package); + public WinGetPackageUri CreatePackageUri(IWinGetPackage package) => _protocolParser.CreatePackageUri(package); /// - public Uri CreateWinGetCatalogPackageUri(string packageId) => _protocolParser.CreateWinGetCatalogPackageUri(packageId); + public WinGetPackageUri CreateWinGetCatalogPackageUri(string packageId) => _protocolParser.CreateWinGetCatalogPackageUri(packageId); /// - public Uri CreateMsStoreCatalogPackageUri(string packageId) => _protocolParser.CreateMsStoreCatalogPackageUri(packageId); + public WinGetPackageUri CreateMsStoreCatalogPackageUri(string packageId) => _protocolParser.CreateMsStoreCatalogPackageUri(packageId); /// - public Uri CreateCustomCatalogPackageUri(string packageId, string catalogName) => _protocolParser.CreateCustomCatalogPackageUri(packageId, catalogName); + public WinGetPackageUri CreateCustomCatalogPackageUri(string packageId, string catalogName) => _protocolParser.CreateCustomCatalogPackageUri(packageId, catalogName); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Styles/AppManagement_ThemeResources.xaml b/tools/SetupFlow/DevHome.SetupFlow/Styles/AppManagement_ThemeResources.xaml index 734204943e..274dd5cce7 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Styles/AppManagement_ThemeResources.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Styles/AppManagement_ThemeResources.xaml @@ -50,13 +50,14 @@ - + + @@ -71,15 +72,28 @@ - + + + | + + - +