Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Select app version to install #2272

Merged
merged 17 commits into from
Feb 21, 2024
Merged
2 changes: 1 addition & 1 deletion SampleExtension/FeaturedApplicationsProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public GetFeaturedApplicationsResult GetApplications()
{
// Sample list of featured applications
return new GetFeaturedApplicationsResult(new List<string>()
{
{
"x-ms-winget://winget/Microsoft.VisualStudio.2022.Community",
"x-ms-winget://winget/Microsoft.VisualStudioCode",
"x-ms-winget://winget/Microsoft.PowerShell",
Expand Down
2 changes: 1 addition & 1 deletion SampleExtension/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
1 change: 1 addition & 0 deletions SampleExtension/RepositoryProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Windows.Storage.Streams;

namespace SampleExtension;

internal sealed class RepositoryProvider : IRepositoryProvider
{
public string DisplayName => $"Sample {nameof(RepositoryProvider)}";
Expand Down
2 changes: 1 addition & 1 deletion SampleExtension/SampleExtension.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.0.4" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.3.230724000" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.4.231115000" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/// <summary>
/// Gets or sets the package id
Expand All @@ -30,6 +31,14 @@ public string PackageId
public string CatalogName
{
get; set;
}

/// <summary>
/// Gets or sets the package version
/// </summary>
public string Version
{
get; set;
}

/// <summary>
Expand All @@ -43,17 +52,19 @@ public static bool TryReadArguments(IList<string> argumentList, ref int index, o
{
result = null;

// --package-id <id> --package-catalog <catalog>
// [ index ] [index + 1] [ index + 2 ] [index + 3]
const int TaskArgListCount = 4;
// --package-id <id> --package-catalog <catalog> --package-version <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;
Expand All @@ -68,7 +79,8 @@ public IList<string> ToArgumentList()
return new List<string>()
{
PackageIdArg, PackageId, // --package-id <id>
PackageCatalogArg, CatalogName, // --package-catalog <catalog>
PackageCatalogArg, CatalogName, // --package-catalog <catalog>
PackageVersionArg, Version, // --package-version <version>
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,16 @@ public void WriteToStdOut(string value)
Console.WriteLine(value);
}

public IAsyncOperation<ElevatedInstallTaskResult> InstallPackageAsync(string packageId, string catalogName)
public IAsyncOperation<ElevatedInstallTaskResult> 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();
}
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ public interface IElevatedComponentOperation
/// Install a package
/// </summary>
/// <param name="packageId">Package id</param>
/// <param name="catalogName">Package catalog name</param>
/// <param name="catalogName">Package catalog name</param>
/// <param name="version">Package version</param>
/// <returns>Install package operation result</returns>
public IAsyncOperation<ElevatedInstallTaskResult> InstallPackageAsync(string packageId, string catalogName);
public IAsyncOperation<ElevatedInstallTaskResult> InstallPackageAsync(string packageId, string catalogName, string version);

/// <summary>
/// Create a dev drive
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public sealed class ElevatedInstallTask
/// <summary>
/// Installs a package given its ID and the ID of the catalog it comes from.
/// </summary>
public IAsyncOperation<ElevatedInstallTaskResult> InstallPackage(string packageId, string catalogName)
public IAsyncOperation<ElevatedInstallTaskResult> InstallPackage(string packageId, string catalogName, string version)
{
return Task.Run(async () =>
{
Expand Down Expand Up @@ -72,6 +72,14 @@ public IAsyncOperation<ElevatedInstallTaskResult> 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);
Expand Down Expand Up @@ -117,4 +125,30 @@ private FindPackagesOptions CreateFindOptionsForPackageId(string packageId)

return findOptions;
}

/// <summary>
/// Find a specific version in the list of available versions for a package.
/// </summary>
/// <param name="package">Target package</param>
/// <param name="version">Version to find</param>
/// <returns>Specified version</returns>
/// <exception>Exception thrown if the specified version was not found</exception>
private PackageVersionId FindVersionOrThrow(ElevatedInstallTaskResult result, CatalogPackage package, string version)
AmelBawa-msft marked this conversation as resolved.
Show resolved Hide resolved
{
// 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public static Mock<IWinGetPackage> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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<object[]>()))
.Returns((string key, object[] args) => $"{args[0]} | {args[1]} | {args[2]}");
Expand All @@ -100,6 +101,6 @@ public void PackageDescription_VersionAndPublisherAreOptional_ReturnsExpectedDes
var packageViewModel = TestHost.CreateInstance<PackageViewModel>(package.Object);

// Assert
Assert.AreEqual(expectedDescription, packageViewModel.PackageDescription);
Assert.AreEqual(expectedDescription, packageViewModel.PackageFullDescription);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ public void LoadCatalogs_Success_ReturnsWinGetCatalogs()
// Prepare expected package
var expectedPackages = new List<IWinGetPackage>
{
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<IList<Uri>>())).ReturnsAsync(expectedPackages);
WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny<IList<WinGetPackageUri>>())).ReturnsAsync(expectedPackages);
WindowsPackageManager.Setup(wpm => wpm.CreatePackageUri(It.IsAny<IWinGetPackage>())).Returns<IWinGetPackage>(p => new WinGetPackageUri(p.CatalogName, p.Id));

// Act
var loadedPackages = LoadCatalogsFromJsonDataSource("AppManagementPackages_Success.json");
Expand All @@ -35,28 +36,30 @@ 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]
public void LoadCatalogs_EmptyPackages_ReturnsNoCatalogs()
{
// Prepare expected package
var expectedPackages = new List<IWinGetPackage>();
WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny<IList<Uri>>())).ReturnsAsync(expectedPackages);
WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny<IList<WinGetPackageUri>>())).ReturnsAsync(expectedPackages);

// Act
var loadedPackages = LoadCatalogsFromJsonDataSource("AppManagementPackages_Empty.json");

// Assert
Assert.AreEqual(0, loadedPackages.Count);
WindowsPackageManager.Verify(c => c.GetPackagesAsync(It.IsAny<IList<Uri>>()), Times.Never());
WindowsPackageManager.Verify(c => c.GetPackagesAsync(It.IsAny<IList<WinGetPackageUri>>()), Times.Never());
}

[TestMethod]
public void LoadCatalogs_ExceptionThrownWhenGettingPackages_ReturnsNoCatalogs()
{
// Configure package manager
WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny<IList<Uri>>())).ThrowsAsync(new FindPackagesException(FindPackagesResultStatus.CatalogError));
WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny<IList<WinGetPackageUri>>())).ThrowsAsync(new FindPackagesException(FindPackagesResultStatus.CatalogError));

// Act
var loadedPackages = LoadCatalogsFromJsonDataSource("AppManagementPackages_Success.json");
Expand All @@ -70,7 +73,7 @@ public void LoadCatalogs_ExceptionThrownWhenOpeningFile_ThrowsException()
{
// Prepare expected package
var expectedPackages = new List<IWinGetPackage>();
WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny<IList<Uri>>())).ReturnsAsync(expectedPackages);
WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny<IList<WinGetPackageUri>>())).ReturnsAsync(expectedPackages);

// Act/Assert
var fileName = TestHelpers.GetTestFilePath("file_not_found");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,22 @@ public void LoadCatalogs_EmptyPackages_ReturnsNoCatalogs()
// Arrange
var expectedPackages = new List<IWinGetPackage>();
var restoreApplicationInfoList = expectedPackages.Select(p => CreateRestoreApplicationInfo(p.Id).Object).ToList();
WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny<IList<Uri>>())).ReturnsAsync(expectedPackages);
WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny<IList<WinGetPackageUri>>())).ReturnsAsync(expectedPackages);
ConfigureRestoreDeviceInfo(RestoreDeviceInfoStatus.Ok, restoreApplicationInfoList);

// Act
var loadedPackages = LoadCatalogsFromRestoreDataSource();

// Assert
Assert.AreEqual(0, loadedPackages.Count);
WindowsPackageManager.Verify(wpm => wpm.GetPackagesAsync(It.IsAny<IList<Uri>>()), Times.Never());
WindowsPackageManager.Verify(wpm => wpm.GetPackagesAsync(It.IsAny<IList<WinGetPackageUri>>()), Times.Never());
}

[TestMethod]
public void LoadCatalogs_ExceptionThrownWhenGettingPackages_ReturnsNoCatalogs()
{
// Arrange
WindowsPackageManager!.Setup(wpm => wpm.GetPackagesAsync(It.IsAny<IList<Uri>>())).ThrowsAsync(new FindPackagesException(FindPackagesResultStatus.CatalogError));
WindowsPackageManager!.Setup(wpm => wpm.GetPackagesAsync(It.IsAny<IList<WinGetPackageUri>>())).ThrowsAsync(new FindPackagesException(FindPackagesResultStatus.CatalogError));
ConfigureRestoreDeviceInfo(RestoreDeviceInfoStatus.Ok, new List<IRestoreApplicationInfo>());

// Act
Expand Down Expand Up @@ -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<IList<Uri>>())).ReturnsAsync(expectedPackages);
WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny<IList<WinGetPackageUri>>())).ReturnsAsync(expectedPackages);
ConfigureRestoreDeviceInfo(RestoreDeviceInfoStatus.Ok, restoreApplicationInfoList);

// Act
Expand All @@ -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<IList<Uri>>())).ReturnsAsync(expectedPackages);
WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny<IList<WinGetPackageUri>>())).ReturnsAsync(expectedPackages);
ConfigureRestoreDeviceInfo(RestoreDeviceInfoStatus.Ok, restoreApplicationInfoList);

// Act
Expand Down Expand Up @@ -140,7 +140,7 @@ public void LoadCatalogs_ExceptionThrownWhenGettingRestoreApplicationIcon_Return

return restoreAppInfo.Object;
}).ToList();
WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny<IList<Uri>>())).ReturnsAsync(expectedPackages);
WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny<IList<WinGetPackageUri>>())).ReturnsAsync(expectedPackages);
ConfigureRestoreDeviceInfo(RestoreDeviceInfoStatus.Ok, restoreApplicationInfoList);

// Act
Expand All @@ -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<IList<Uri>>())).ReturnsAsync(expectedPackages);
WindowsPackageManager.Setup(wpm => wpm.CreateWinGetCatalogPackageUri(It.IsAny<string>())).Returns(new Uri("http://mock"));
WindowsPackageManager.Setup(wpm => wpm.GetPackagesAsync(It.IsAny<IList<WinGetPackageUri>>())).ReturnsAsync(expectedPackages);
WindowsPackageManager.Setup(wpm => wpm.CreateWinGetCatalogPackageUri(It.IsAny<string>())).Returns(new WinGetPackageUri("x-ms-winget://mock/mock"));
ConfigureRestoreDeviceInfo(RestoreDeviceInfoStatus.Ok, restoreApplicationInfoList);

// Act
Expand Down
Loading
Loading