Skip to content

Commit

Permalink
Select app version to install (#2272)
Browse files Browse the repository at this point in the history
  • Loading branch information
AmelBawa-msft authored Feb 21, 2024
1 parent d67d0d0 commit bebb21c
Show file tree
Hide file tree
Showing 45 changed files with 1,080 additions and 383 deletions.
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)
{
// 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

0 comments on commit bebb21c

Please sign in to comment.